diff --git a/packages/core/src/utils/vercel-ai-attributes.ts b/packages/core/src/utils/vercel-ai-attributes.ts index 8d7b6913a636..ac6774b08a02 100644 --- a/packages/core/src/utils/vercel-ai-attributes.ts +++ b/packages/core/src/utils/vercel-ai-attributes.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ /** * AI SDK Telemetry Attributes * 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'; */ export const AI_REQUEST_HEADERS_ATTRIBUTE = 'ai.request.headers'; +/** + * Basic LLM span information + * Multiple spans + * + * Provider specific metadata returned with the generation response + * @see https://ai-sdk.dev/docs/ai-sdk-core/telemetry#basic-llm-span-information + */ +export const AI_RESPONSE_PROVIDER_METADATA_ATTRIBUTE = 'ai.response.providerMetadata'; + /** * Basic LLM span information * Multiple spans @@ -792,3 +802,225 @@ export const AI_TOOL_CALL_SPAN_ATTRIBUTES = { AI_TELEMETRY_FUNCTION_ID: AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE, AI_TELEMETRY_METADATA: AI_TELEMETRY_METADATA_ATTRIBUTE, } as const; + +// ============================================================================= +// PROVIDER METADATA +// ============================================================================= + +/** + * OpenAI Provider Metadata + * @see https://ai-sdk.dev/providers/ai-sdk-providers/openai + * @see https://github.com/vercel/ai/blob/65e042afde6aad4da9d7a62526ece839eb34f9a5/packages/openai/src/openai-chat-language-model.ts#L397-L416 + * @see https://github.com/vercel/ai/blob/65e042afde6aad4da9d7a62526ece839eb34f9a5/packages/openai/src/responses/openai-responses-language-model.ts#L377C7-L384 + */ +interface OpenAiProviderMetadata { + /** + * The number of predicted output tokens that were accepted. + * @see https://ai-sdk.dev/providers/ai-sdk-providers/openai#predicted-outputs + */ + acceptedPredictionTokens?: number; + + /** + * The number of predicted output tokens that were rejected. + * @see https://ai-sdk.dev/providers/ai-sdk-providers/openai#predicted-outputs + */ + rejectedPredictionTokens?: number; + + /** + * The number of reasoning tokens that the model generated. + * @see https://ai-sdk.dev/providers/ai-sdk-providers/openai#responses-models + */ + reasoningTokens?: number; + + /** + * The number of prompt tokens that were a cache hit. + * @see https://ai-sdk.dev/providers/ai-sdk-providers/openai#responses-models + */ + cachedPromptTokens?: number; + + /** + * @see https://ai-sdk.dev/providers/ai-sdk-providers/openai#responses-models + * + * The ID of the response. Can be used to continue a conversation. + */ + responseId?: string; +} + +/** + * Anthropic Provider Metadata + * @see https://ai-sdk.dev/providers/ai-sdk-providers/anthropic + * @see https://github.com/vercel/ai/blob/65e042afde6aad4da9d7a62526ece839eb34f9a5/packages/anthropic/src/anthropic-messages-language-model.ts#L346-L352 + */ +interface AnthropicProviderMetadata { + /** + * The number of tokens that were used to create the cache. + * @see https://ai-sdk.dev/providers/ai-sdk-providers/anthropic#cache-control + */ + cacheCreationInputTokens?: number; + + /** + * The number of tokens that were read from the cache. + * @see https://ai-sdk.dev/providers/ai-sdk-providers/anthropic#cache-control + */ + cacheReadInputTokens?: number; +} + +/** + * Amazon Bedrock Provider Metadata + * @see https://ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock + * @see https://github.com/vercel/ai/blob/65e042afde6aad4da9d7a62526ece839eb34f9a5/packages/amazon-bedrock/src/bedrock-chat-language-model.ts#L263-L280 + */ +interface AmazonBedrockProviderMetadata { + /** + * @see https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ConverseTrace.html + */ + trace?: { + /** + * The guardrail trace object. + * @see https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_GuardrailTraceAssessment.html + * + * This was purposely left as unknown as it's a complex object. This can be typed in the future + * if the SDK decides to support bedrock in a more advanced way. + */ + guardrail?: unknown; + /** + * The request's prompt router. + * @see https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_PromptRouterTrace.html + */ + promptRouter?: { + /** + * The ID of the invoked model. + */ + invokedModelId?: string; + }; + }; + usage?: { + /** + * The number of tokens that were read from the cache. + * @see https://ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock#cache-points + */ + cacheReadInputTokens?: number; + + /** + * The number of tokens that were written to the cache. + * @see https://ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock#cache-points + */ + cacheWriteInputTokens?: number; + }; +} + +/** + * Google Generative AI Provider Metadata + * @see https://ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai + */ +export interface GoogleGenerativeAIProviderMetadata { + /** + * @see https://github.com/vercel/ai/blob/65e042afde6aad4da9d7a62526ece839eb34f9a5/packages/google/src/google-generative-ai-prompt.ts#L28-L30 + */ + groundingMetadata: null | { + /** + * Array of search queries used to retrieve information + * @example ["What's the weather in Chicago this weekend?"] + * + * @see https://ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai#search-grounding + */ + webSearchQueries: string[] | null; + /** + * Contains the main search result content used as an entry point + * The `renderedContent` field contains the formatted content + * @see https://ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai#search-grounding + */ + searchEntryPoint?: { + renderedContent: string; + } | null; + /** + * Contains details about how specific response parts are supported by search results + * @see https://ai-sdk.dev/providers/ai-sdk-providers/google-generative-ai#search-grounding + */ + groundingSupports: Array<{ + /** + * Information about the grounded text segment. + */ + segment: { + /** + * The start index of the text segment. + */ + startIndex?: number | null; + /** + * The end index of the text segment. + */ + endIndex?: number | null; + /** + * The actual text segment. + */ + text?: string | null; + }; + /** + * References to supporting search result chunks. + */ + groundingChunkIndices?: number[] | null; + /** + * Confidence scores (0-1) for each supporting chunk. + */ + confidenceScores?: number[] | null; + }> | null; + }; + /** + * @see https://github.com/vercel/ai/blob/65e042afde6aad4da9d7a62526ece839eb34f9a5/packages/google/src/google-generative-ai-language-model.ts#L620-L627 + * @see https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/configure-safety-filters + */ + safetyRatings?: null | unknown; +} + +/** + * DeepSeek Provider Metadata + * @see https://ai-sdk.dev/providers/ai-sdk-providers/deepseek + */ +interface DeepSeekProviderMetadata { + /** + * The number of tokens that were cache hits. + * @see https://ai-sdk.dev/providers/ai-sdk-providers/deepseek#cache-token-usage + */ + promptCacheHitTokens?: number; + + /** + * The number of tokens that were cache misses. + * @see https://ai-sdk.dev/providers/ai-sdk-providers/deepseek#cache-token-usage + */ + promptCacheMissTokens?: number; +} + +/** + * Perplexity Provider Metadata + * @see https://ai-sdk.dev/providers/ai-sdk-providers/perplexity + */ +interface PerplexityProviderMetadata { + /** + * Object containing citationTokens and numSearchQueries metrics + */ + usage?: { + citationTokens?: number; + numSearchQueries?: number; + }; + /** + * Array of image URLs when return_images is enabled. + * + * You can enable image responses by setting return_images: true in the provider options. + * This feature is only available to Perplexity Tier-2 users and above. + */ + images?: Array<{ + imageUrl?: string; + originUrl?: string; + height?: number; + width?: number; + }>; +} + +export interface ProviderMetadata { + openai?: OpenAiProviderMetadata; + anthropic?: AnthropicProviderMetadata; + bedrock?: AmazonBedrockProviderMetadata; + google?: GoogleGenerativeAIProviderMetadata; + deepseek?: DeepSeekProviderMetadata; + perplexity?: PerplexityProviderMetadata; +} diff --git a/packages/core/src/utils/vercel-ai.ts b/packages/core/src/utils/vercel-ai.ts index b1a2feedb454..c5491376d7c4 100644 --- a/packages/core/src/utils/vercel-ai.ts +++ b/packages/core/src/utils/vercel-ai.ts @@ -1,14 +1,16 @@ import type { Client } from '../client'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; import type { Event } from '../types-hoist/event'; -import type { Span, SpanAttributes, SpanJSON, SpanOrigin } from '../types-hoist/span'; +import type { Span, SpanAttributes, SpanAttributeValue, SpanJSON, SpanOrigin } from '../types-hoist/span'; import { spanToJSON } from './spanUtils'; +import type { ProviderMetadata } from './vercel-ai-attributes'; import { AI_MODEL_ID_ATTRIBUTE, AI_MODEL_PROVIDER_ATTRIBUTE, AI_PROMPT_ATTRIBUTE, AI_PROMPT_MESSAGES_ATTRIBUTE, AI_PROMPT_TOOLS_ATTRIBUTE, + AI_RESPONSE_PROVIDER_METADATA_ATTRIBUTE, AI_RESPONSE_TEXT_ATTRIBUTE, AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, AI_TELEMETRY_FUNCTION_ID_ATTRIBUTE, @@ -96,6 +98,8 @@ function processEndedVercelAiSpan(span: SpanJSON): void { renameAttributeKey(attributes, AI_TOOL_CALL_ARGS_ATTRIBUTE, 'gen_ai.tool.input'); renameAttributeKey(attributes, AI_TOOL_CALL_RESULT_ATTRIBUTE, 'gen_ai.tool.output'); + addProviderMetadataToAttributes(attributes); + // Change attributes namespaced with `ai.X` to `vercel.ai.X` for (const key of Object.keys(attributes)) { if (key.startsWith('ai.')) { @@ -234,3 +238,85 @@ export function addVercelAiProcessors(client: Client): void { // Note: We cannot do this on `spanEnd`, because the span cannot be mutated anymore at this point client.addEventProcessor(Object.assign(vercelAiEventProcessor, { id: 'VercelAiEventProcessor' })); } + +function addProviderMetadataToAttributes(attributes: SpanAttributes): void { + const providerMetadata = attributes[AI_RESPONSE_PROVIDER_METADATA_ATTRIBUTE] as string | undefined; + if (providerMetadata) { + try { + const providerMetadataObject = JSON.parse(providerMetadata) as ProviderMetadata; + if (providerMetadataObject.openai) { + setAttributeIfDefined( + attributes, + 'gen_ai.usage.input_tokens.cached', + providerMetadataObject.openai.cachedPromptTokens, + ); + setAttributeIfDefined( + attributes, + 'gen_ai.usage.output_tokens.reasoning', + providerMetadataObject.openai.reasoningTokens, + ); + setAttributeIfDefined( + attributes, + 'gen_ai.usage.output_tokens.prediction_accepted', + providerMetadataObject.openai.acceptedPredictionTokens, + ); + setAttributeIfDefined( + attributes, + 'gen_ai.usage.output_tokens.prediction_rejected', + providerMetadataObject.openai.rejectedPredictionTokens, + ); + setAttributeIfDefined(attributes, 'gen_ai.conversation.id', providerMetadataObject.openai.responseId); + } + + if (providerMetadataObject.anthropic) { + setAttributeIfDefined( + attributes, + 'gen_ai.usage.input_tokens.cached', + providerMetadataObject.anthropic.cacheReadInputTokens, + ); + setAttributeIfDefined( + attributes, + 'gen_ai.usage.input_tokens.cache_write', + providerMetadataObject.anthropic.cacheCreationInputTokens, + ); + } + + if (providerMetadataObject.bedrock?.usage) { + setAttributeIfDefined( + attributes, + 'gen_ai.usage.input_tokens.cached', + providerMetadataObject.bedrock.usage.cacheReadInputTokens, + ); + setAttributeIfDefined( + attributes, + 'gen_ai.usage.input_tokens.cache_write', + providerMetadataObject.bedrock.usage.cacheWriteInputTokens, + ); + } + + if (providerMetadataObject.deepseek) { + setAttributeIfDefined( + attributes, + 'gen_ai.usage.input_tokens.cached', + providerMetadataObject.deepseek.promptCacheHitTokens, + ); + setAttributeIfDefined( + attributes, + 'gen_ai.usage.input_tokens.cache_miss', + providerMetadataObject.deepseek.promptCacheMissTokens, + ); + } + } catch { + // Ignore + } + } +} + +/** + * Sets an attribute only if the value is not null or undefined. + */ +function setAttributeIfDefined(attributes: SpanAttributes, key: string, value: SpanAttributeValue | undefined): void { + if (value != null) { + attributes[key] = value; + } +}