11/**
22 * Shared utils for AI integrations (OpenAI, Anthropic, Verce.AI, etc.)
33 */
4+ import {
5+ GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS ,
6+ GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS ,
7+ GEN_AI_USAGE_REASONING_OUTPUT_TOKENS ,
8+ } from '@sentry/conventions/attributes' ;
49import { getClient } from '../../currentScopes' ;
510import { hasSpanStreamingEnabled } from '../spans/hasSpanStreamingEnabled' ;
611import type { Span } from '../../types/span' ;
@@ -85,47 +90,37 @@ export function buildMethodPath(currentPath: string, prop: string): string {
8590}
8691
8792/**
88- * Set token usage attributes
89- * @param span - The span to add attributes to
90- * @param promptTokens - The number of prompt tokens
91- * @param completionTokens - The number of completion tokens
92- * @param cachedInputTokens - The number of cached input tokens
93- * @param cachedOutputTokens - The number of cached output tokens
93+ * Build token usage attributes. Input tokens include cache reads and writes, which are subsets.
9494 */
95- export function setTokenUsageAttributes (
96- span : Span ,
95+ export function getTokenUsageAttributes (
9796 promptTokens ?: number ,
9897 completionTokens ?: number ,
99- cachedInputTokens ?: number ,
100- cachedOutputTokens ?: number ,
101- ) : void {
102- if ( promptTokens !== undefined ) {
103- span . setAttributes ( {
104- [ GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE ] : promptTokens ,
105- } ) ;
98+ cacheCreationInputTokens ?: number ,
99+ cacheReadInputTokens ?: number ,
100+ totalTokens ?: number ,
101+ ) : Record < string , number > {
102+ const attributes : Record < string , number > = { } ;
103+
104+ if ( typeof promptTokens === 'number' ) {
105+ attributes [ GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE ] = promptTokens ;
106+ }
107+ if ( typeof cacheCreationInputTokens === 'number' ) {
108+ attributes [ GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS ] = cacheCreationInputTokens ;
106109 }
107- if ( completionTokens !== undefined ) {
108- span . setAttributes ( {
109- [ GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE ] : completionTokens ,
110- } ) ;
110+ if ( typeof cacheReadInputTokens === 'number' ) {
111+ attributes [ GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS ] = cacheReadInputTokens ;
111112 }
112- if (
113- promptTokens !== undefined ||
114- completionTokens !== undefined ||
115- cachedInputTokens !== undefined ||
116- cachedOutputTokens !== undefined
117- ) {
118- /**
119- * Total input tokens in a request is the summation of `input_tokens`,
120- * `cache_creation_input_tokens`, and `cache_read_input_tokens`.
121- */
122- const totalTokens =
123- ( promptTokens ?? 0 ) + ( completionTokens ?? 0 ) + ( cachedInputTokens ?? 0 ) + ( cachedOutputTokens ?? 0 ) ;
124-
125- span . setAttributes ( {
126- [ GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE ] : totalTokens ,
127- } ) ;
113+ if ( typeof completionTokens === 'number' ) {
114+ attributes [ GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE ] = completionTokens ;
128115 }
116+ if ( typeof totalTokens === 'number' ) {
117+ attributes [ GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE ] = totalTokens ;
118+ } else if ( typeof promptTokens === 'number' || typeof completionTokens === 'number' ) {
119+ attributes [ GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE ] =
120+ ( typeof promptTokens === 'number' ? promptTokens : 0 ) +
121+ ( typeof completionTokens === 'number' ? completionTokens : 0 ) ;
122+ }
123+ return attributes ;
129124}
130125
131126export interface StreamResponseState {
@@ -139,6 +134,7 @@ export interface StreamResponseState {
139134 totalTokens ?: number ;
140135 cacheCreationInputTokens ?: number ;
141136 cacheReadInputTokens ?: number ;
137+ reasoningOutputTokens ?: number ;
142138}
143139
144140/**
@@ -157,23 +153,18 @@ export function endStreamSpan(span: Span, state: StreamResponseState, recordOutp
157153 if ( state . responseId ) attrs [ GEN_AI_RESPONSE_ID_ATTRIBUTE ] = state . responseId ;
158154 if ( state . responseModel ) attrs [ GEN_AI_RESPONSE_MODEL_ATTRIBUTE ] = state . responseModel ;
159155
160- if ( state . promptTokens !== undefined ) attrs [ GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE ] = state . promptTokens ;
161- if ( state . completionTokens !== undefined ) attrs [ GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE ] = state . completionTokens ;
162-
163- // Use explicit total if provided (OpenAI, Google), otherwise compute from cache tokens (Anthropic)
164- if ( state . totalTokens !== undefined ) {
165- attrs [ GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE ] = state . totalTokens ;
166- } else if (
167- state . promptTokens !== undefined ||
168- state . completionTokens !== undefined ||
169- state . cacheCreationInputTokens !== undefined ||
170- state . cacheReadInputTokens !== undefined
171- ) {
172- attrs [ GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE ] =
173- ( state . promptTokens ?? 0 ) +
174- ( state . completionTokens ?? 0 ) +
175- ( state . cacheCreationInputTokens ?? 0 ) +
176- ( state . cacheReadInputTokens ?? 0 ) ;
156+ Object . assign (
157+ attrs ,
158+ getTokenUsageAttributes (
159+ state . promptTokens ,
160+ state . completionTokens ,
161+ state . cacheCreationInputTokens ,
162+ state . cacheReadInputTokens ,
163+ state . totalTokens ,
164+ ) ,
165+ ) ;
166+ if ( typeof state . reasoningOutputTokens === 'number' ) {
167+ attrs [ GEN_AI_USAGE_REASONING_OUTPUT_TOKENS ] = state . reasoningOutputTokens ;
177168 }
178169
179170 if ( state . finishReasons . length ) {
0 commit comments