Skip to content

feat(core): Add support for tracking provider metadata #16992

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 232 additions & 0 deletions packages/core/src/utils/vercel-ai-attributes.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
88 changes: 87 additions & 1 deletion packages/core/src/utils/vercel-ai.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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.')) {
Expand Down Expand Up @@ -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;
}
}
Loading