Skip to content

Commit 8a96366

Browse files
authored
feat(core): Add support for tracking provider metadata (#16992)
In https://github.com/vercel/ai/releases/tag/ai%404.3.17 of the `ai` SDK, provider metadata got exposed (vercel/ai@a288694) This is included in the `ai.response.providerMetadata` property. To get more detailed, info, we parse `ai.response.providerMetadata` and attach attributes as appropriate. Still need some help with the naming. resolves https://linear.app/getsentry/issue/JS-661/add-support-for-cached-and-reasoning-tokens
1 parent c23fa70 commit 8a96366

File tree

2 files changed

+319
-1
lines changed

2 files changed

+319
-1
lines changed

packages/core/src/utils/vercel-ai-attributes.ts

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable max-lines */
12
/**
23
* AI SDK Telemetry Attributes
34
* Based on https://ai-sdk.dev/docs/ai-sdk-core/telemetry#collected-data
@@ -269,6 +270,15 @@ export const AI_MODEL_PROVIDER_ATTRIBUTE = 'ai.model.provider';
269270
*/
270271
export const AI_REQUEST_HEADERS_ATTRIBUTE = 'ai.request.headers';
271272

273+
/**
274+
* Basic LLM span information
275+
* Multiple spans
276+
*
277+
* Provider specific metadata returned with the generation response
278+
* @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#basic-llm-span-information
279+
*/
280+
export const AI_RESPONSE_PROVIDER_METADATA_ATTRIBUTE = 'ai.response.providerMetadata';
281+
272282
/**
273283
* Basic LLM span information
274284
* Multiple spans
@@ -792,3 +802,225 @@ export const AI_TOOL_CALL_SPAN_ATTRIBUTES = {
792802
AI_TELEMETRY_FUNCTION_ID: AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE,
793803
AI_TELEMETRY_METADATA: AI_TELEMETRY_METADATA_ATTRIBUTE,
794804
} as const;
805+
806+
// =============================================================================
807+
// PROVIDER METADATA
808+
// =============================================================================
809+
810+
/**
811+
* OpenAI Provider Metadata
812+
* @see https://ai-sdk.dev/providers/ai-sdk-providers/openai
813+
* @see https://github.com/vercel/ai/blob/65e042afde6aad4da9d7a62526ece839eb34f9a5/packages/openai/src/openai-chat-language-model.ts#L397-L416
814+
* @see https://github.com/vercel/ai/blob/65e042afde6aad4da9d7a62526ece839eb34f9a5/packages/openai/src/responses/openai-responses-language-model.ts#L377C7-L384
815+
*/
816+
interface OpenAiProviderMetadata {
817+
/**
818+
* The number of predicted output tokens that were accepted.
819+
* @see https://ai-sdk.dev/providers/ai-sdk-providers/openai#predicted-outputs
820+
*/
821+
acceptedPredictionTokens?: number;
822+
823+
/**
824+
* The number of predicted output tokens that were rejected.
825+
* @see https://ai-sdk.dev/providers/ai-sdk-providers/openai#predicted-outputs
826+
*/
827+
rejectedPredictionTokens?: number;
828+
829+
/**
830+
* The number of reasoning tokens that the model generated.
831+
* @see https://ai-sdk.dev/providers/ai-sdk-providers/openai#responses-models
832+
*/
833+
reasoningTokens?: number;
834+
835+
/**
836+
* The number of prompt tokens that were a cache hit.
837+
* @see https://ai-sdk.dev/providers/ai-sdk-providers/openai#responses-models
838+
*/
839+
cachedPromptTokens?: number;
840+
841+
/**
842+
* @see https://ai-sdk.dev/providers/ai-sdk-providers/openai#responses-models
843+
*
844+
* The ID of the response. Can be used to continue a conversation.
845+
*/
846+
responseId?: string;
847+
}
848+
849+
/**
850+
* Anthropic Provider Metadata
851+
* @see https://ai-sdk.dev/providers/ai-sdk-providers/anthropic
852+
* @see https://github.com/vercel/ai/blob/65e042afde6aad4da9d7a62526ece839eb34f9a5/packages/anthropic/src/anthropic-messages-language-model.ts#L346-L352
853+
*/
854+
interface AnthropicProviderMetadata {
855+
/**
856+
* The number of tokens that were used to create the cache.
857+
* @see https://ai-sdk.dev/providers/ai-sdk-providers/anthropic#cache-control
858+
*/
859+
cacheCreationInputTokens?: number;
860+
861+
/**
862+
* The number of tokens that were read from the cache.
863+
* @see https://ai-sdk.dev/providers/ai-sdk-providers/anthropic#cache-control
864+
*/
865+
cacheReadInputTokens?: number;
866+
}
867+
868+
/**
869+
* Amazon Bedrock Provider Metadata
870+
* @see https://ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock
871+
* @see https://github.com/vercel/ai/blob/65e042afde6aad4da9d7a62526ece839eb34f9a5/packages/amazon-bedrock/src/bedrock-chat-language-model.ts#L263-L280
872+
*/
873+
interface AmazonBedrockProviderMetadata {
874+
/**
875+
* @see https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ConverseTrace.html
876+
*/
877+
trace?: {
878+
/**
879+
* The guardrail trace object.
880+
* @see https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_GuardrailTraceAssessment.html
881+
*
882+
* This was purposely left as unknown as it's a complex object. This can be typed in the future
883+
* if the SDK decides to support bedrock in a more advanced way.
884+
*/
885+
guardrail?: unknown;
886+
/**
887+
* The request's prompt router.
888+
* @see https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_PromptRouterTrace.html
889+
*/
890+
promptRouter?: {
891+
/**
892+
* The ID of the invoked model.
893+
*/
894+
invokedModelId?: string;
895+
};
896+
};
897+
usage?: {
898+
/**
899+
* The number of tokens that were read from the cache.
900+
* @see https://ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock#cache-points
901+
*/
902+
cacheReadInputTokens?: number;
903+
904+
/**
905+
* The number of tokens that were written to the cache.
906+
* @see https://ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock#cache-points
907+
*/
908+
cacheWriteInputTokens?: number;
909+
};
910+
}
911+
912+
/**
913+
* Google Generative AI Provider Metadata
914+
* @see https://ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai
915+
*/
916+
export interface GoogleGenerativeAIProviderMetadata {
917+
/**
918+
* @see https://github.com/vercel/ai/blob/65e042afde6aad4da9d7a62526ece839eb34f9a5/packages/google/src/google-generative-ai-prompt.ts#L28-L30
919+
*/
920+
groundingMetadata: null | {
921+
/**
922+
* Array of search queries used to retrieve information
923+
* @example ["What's the weather in Chicago this weekend?"]
924+
*
925+
* @see https://ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai#search-grounding
926+
*/
927+
webSearchQueries: string[] | null;
928+
/**
929+
* Contains the main search result content used as an entry point
930+
* The `renderedContent` field contains the formatted content
931+
* @see https://ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai#search-grounding
932+
*/
933+
searchEntryPoint?: {
934+
renderedContent: string;
935+
} | null;
936+
/**
937+
* Contains details about how specific response parts are supported by search results
938+
* @see https://ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai#search-grounding
939+
*/
940+
groundingSupports: Array<{
941+
/**
942+
* Information about the grounded text segment.
943+
*/
944+
segment: {
945+
/**
946+
* The start index of the text segment.
947+
*/
948+
startIndex?: number | null;
949+
/**
950+
* The end index of the text segment.
951+
*/
952+
endIndex?: number | null;
953+
/**
954+
* The actual text segment.
955+
*/
956+
text?: string | null;
957+
};
958+
/**
959+
* References to supporting search result chunks.
960+
*/
961+
groundingChunkIndices?: number[] | null;
962+
/**
963+
* Confidence scores (0-1) for each supporting chunk.
964+
*/
965+
confidenceScores?: number[] | null;
966+
}> | null;
967+
};
968+
/**
969+
* @see https://github.com/vercel/ai/blob/65e042afde6aad4da9d7a62526ece839eb34f9a5/packages/google/src/google-generative-ai-language-model.ts#L620-L627
970+
* @see https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/configure-safety-filters
971+
*/
972+
safetyRatings?: null | unknown;
973+
}
974+
975+
/**
976+
* DeepSeek Provider Metadata
977+
* @see https://ai-sdk.dev/providers/ai-sdk-providers/deepseek
978+
*/
979+
interface DeepSeekProviderMetadata {
980+
/**
981+
* The number of tokens that were cache hits.
982+
* @see https://ai-sdk.dev/providers/ai-sdk-providers/deepseek#cache-token-usage
983+
*/
984+
promptCacheHitTokens?: number;
985+
986+
/**
987+
* The number of tokens that were cache misses.
988+
* @see https://ai-sdk.dev/providers/ai-sdk-providers/deepseek#cache-token-usage
989+
*/
990+
promptCacheMissTokens?: number;
991+
}
992+
993+
/**
994+
* Perplexity Provider Metadata
995+
* @see https://ai-sdk.dev/providers/ai-sdk-providers/perplexity
996+
*/
997+
interface PerplexityProviderMetadata {
998+
/**
999+
* Object containing citationTokens and numSearchQueries metrics
1000+
*/
1001+
usage?: {
1002+
citationTokens?: number;
1003+
numSearchQueries?: number;
1004+
};
1005+
/**
1006+
* Array of image URLs when return_images is enabled.
1007+
*
1008+
* You can enable image responses by setting return_images: true in the provider options.
1009+
* This feature is only available to Perplexity Tier-2 users and above.
1010+
*/
1011+
images?: Array<{
1012+
imageUrl?: string;
1013+
originUrl?: string;
1014+
height?: number;
1015+
width?: number;
1016+
}>;
1017+
}
1018+
1019+
export interface ProviderMetadata {
1020+
openai?: OpenAiProviderMetadata;
1021+
anthropic?: AnthropicProviderMetadata;
1022+
bedrock?: AmazonBedrockProviderMetadata;
1023+
google?: GoogleGenerativeAIProviderMetadata;
1024+
deepseek?: DeepSeekProviderMetadata;
1025+
perplexity?: PerplexityProviderMetadata;
1026+
}

packages/core/src/utils/vercel-ai.ts

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import type { Client } from '../client';
22
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes';
33
import type { Event } from '../types-hoist/event';
4-
import type { Span, SpanAttributes, SpanJSON, SpanOrigin } from '../types-hoist/span';
4+
import type { Span, SpanAttributes, SpanAttributeValue, SpanJSON, SpanOrigin } from '../types-hoist/span';
55
import { spanToJSON } from './spanUtils';
6+
import type { ProviderMetadata } from './vercel-ai-attributes';
67
import {
78
AI_MODEL_ID_ATTRIBUTE,
89
AI_MODEL_PROVIDER_ATTRIBUTE,
910
AI_PROMPT_ATTRIBUTE,
1011
AI_PROMPT_MESSAGES_ATTRIBUTE,
1112
AI_PROMPT_TOOLS_ATTRIBUTE,
13+
AI_RESPONSE_PROVIDER_METADATA_ATTRIBUTE,
1214
AI_RESPONSE_TEXT_ATTRIBUTE,
1315
AI_RESPONSE_TOOL_CALLS_ATTRIBUTE,
1416
AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE,
@@ -96,6 +98,8 @@ function processEndedVercelAiSpan(span: SpanJSON): void {
9698
renameAttributeKey(attributes, AI_TOOL_CALL_ARGS_ATTRIBUTE, 'gen_ai.tool.input');
9799
renameAttributeKey(attributes, AI_TOOL_CALL_RESULT_ATTRIBUTE, 'gen_ai.tool.output');
98100

101+
addProviderMetadataToAttributes(attributes);
102+
99103
// Change attributes namespaced with `ai.X` to `vercel.ai.X`
100104
for (const key of Object.keys(attributes)) {
101105
if (key.startsWith('ai.')) {
@@ -234,3 +238,85 @@ export function addVercelAiProcessors(client: Client): void {
234238
// Note: We cannot do this on `spanEnd`, because the span cannot be mutated anymore at this point
235239
client.addEventProcessor(Object.assign(vercelAiEventProcessor, { id: 'VercelAiEventProcessor' }));
236240
}
241+
242+
function addProviderMetadataToAttributes(attributes: SpanAttributes): void {
243+
const providerMetadata = attributes[AI_RESPONSE_PROVIDER_METADATA_ATTRIBUTE] as string | undefined;
244+
if (providerMetadata) {
245+
try {
246+
const providerMetadataObject = JSON.parse(providerMetadata) as ProviderMetadata;
247+
if (providerMetadataObject.openai) {
248+
setAttributeIfDefined(
249+
attributes,
250+
'gen_ai.usage.input_tokens.cached',
251+
providerMetadataObject.openai.cachedPromptTokens,
252+
);
253+
setAttributeIfDefined(
254+
attributes,
255+
'gen_ai.usage.output_tokens.reasoning',
256+
providerMetadataObject.openai.reasoningTokens,
257+
);
258+
setAttributeIfDefined(
259+
attributes,
260+
'gen_ai.usage.output_tokens.prediction_accepted',
261+
providerMetadataObject.openai.acceptedPredictionTokens,
262+
);
263+
setAttributeIfDefined(
264+
attributes,
265+
'gen_ai.usage.output_tokens.prediction_rejected',
266+
providerMetadataObject.openai.rejectedPredictionTokens,
267+
);
268+
setAttributeIfDefined(attributes, 'gen_ai.conversation.id', providerMetadataObject.openai.responseId);
269+
}
270+
271+
if (providerMetadataObject.anthropic) {
272+
setAttributeIfDefined(
273+
attributes,
274+
'gen_ai.usage.input_tokens.cached',
275+
providerMetadataObject.anthropic.cacheReadInputTokens,
276+
);
277+
setAttributeIfDefined(
278+
attributes,
279+
'gen_ai.usage.input_tokens.cache_write',
280+
providerMetadataObject.anthropic.cacheCreationInputTokens,
281+
);
282+
}
283+
284+
if (providerMetadataObject.bedrock?.usage) {
285+
setAttributeIfDefined(
286+
attributes,
287+
'gen_ai.usage.input_tokens.cached',
288+
providerMetadataObject.bedrock.usage.cacheReadInputTokens,
289+
);
290+
setAttributeIfDefined(
291+
attributes,
292+
'gen_ai.usage.input_tokens.cache_write',
293+
providerMetadataObject.bedrock.usage.cacheWriteInputTokens,
294+
);
295+
}
296+
297+
if (providerMetadataObject.deepseek) {
298+
setAttributeIfDefined(
299+
attributes,
300+
'gen_ai.usage.input_tokens.cached',
301+
providerMetadataObject.deepseek.promptCacheHitTokens,
302+
);
303+
setAttributeIfDefined(
304+
attributes,
305+
'gen_ai.usage.input_tokens.cache_miss',
306+
providerMetadataObject.deepseek.promptCacheMissTokens,
307+
);
308+
}
309+
} catch {
310+
// Ignore
311+
}
312+
}
313+
}
314+
315+
/**
316+
* Sets an attribute only if the value is not null or undefined.
317+
*/
318+
function setAttributeIfDefined(attributes: SpanAttributes, key: string, value: SpanAttributeValue | undefined): void {
319+
if (value != null) {
320+
attributes[key] = value;
321+
}
322+
}

0 commit comments

Comments
 (0)