@@ -27,9 +27,22 @@ const GEN_AI_RESPONSE_FINISH_REASONS_FIELD = "gen_ai.response.finish_reasons";
2727const AI_RESPONSE_TEXT_FIELD = "gen_ai.response.text" ;
2828const AI_RESPONSE_TOOL_CALLS_FIELD = "vercel.ai.response.toolCalls" ;
2929
30+ // v1 operation names
3031const AI_STREAM_TEXT_OPERATION = "ai.streamText" ;
3132const AI_GENERATE_TEXT_OPERATION = "ai.generateText" ;
3233
34+ // v2 operation names (OpenTelemetry semantic conventions)
35+ const AI_INVOKE_AGENT_OPERATION = "gen_ai.invoke_agent" ;
36+ const AI_STREAM_TEXT_V2_OPERATION = "gen_ai.stream_text" ;
37+
38+ // v2 field names
39+ const GEN_AI_PROMPT_FIELD = "gen_ai.prompt" ;
40+ const GEN_AI_FUNCTION_ID_FIELD = "gen_ai.function_id" ;
41+ const GEN_AI_REQUEST_MODEL_FIELD = "gen_ai.request.model" ;
42+ const GEN_AI_RESPONSE_MODEL_FIELD = "gen_ai.response.model" ;
43+ const GEN_AI_SYSTEM_FIELD = "gen_ai.system" ;
44+ const GEN_AI_OPERATION_NAME_FIELD = "gen_ai.operation.name" ;
45+
3346const AI_USAGE_PROMPT_TOKENS_FIELD = "vercel.ai.usage.promptTokens" ;
3447const AI_USAGE_COMPLETION_TOKENS_FIELD = "vercel.ai.usage.completionTokens" ;
3548const GEN_AI_USAGE_INPUT_TOKENS_FIELD = "gen_ai.usage.input_tokens" ;
@@ -44,6 +57,41 @@ const TOKEN_FIELDS = {
4457const DEFAULT_TRACE_NAME = "AI Interaction" ;
4558const UNKNOWN_OPERATION = "N/A" ;
4659
60+ // Operation to badge mapping
61+ const OPERATION_BADGE_MAP = new Map < string , string > ( [
62+ [ AI_STREAM_TEXT_OPERATION , "Stream Text" ] ,
63+ [ AI_GENERATE_TEXT_OPERATION , "Generate Text" ] ,
64+ [ AI_INVOKE_AGENT_OPERATION , "Invoke Agent" ] ,
65+ [ AI_STREAM_TEXT_V2_OPERATION , "Stream Text" ] ,
66+ ] ) ;
67+
68+ // Field mappings: first element is v2, subsequent are v1 alternatives
69+ const AI_FIELD_MAPPINGS = {
70+ MODEL_ID : [ AI_MODEL_ID_FIELD , GEN_AI_REQUEST_MODEL_FIELD , GEN_AI_RESPONSE_MODEL_FIELD ] ,
71+ MODEL_PROVIDER : [ AI_MODEL_PROVIDER_FIELD , GEN_AI_SYSTEM_FIELD ] ,
72+ FUNCTION_ID : [ AI_TELEMETRY_FUNCTION_ID_FIELD , GEN_AI_FUNCTION_ID_FIELD ] ,
73+ PROMPT_TOKENS : [ AI_USAGE_PROMPT_TOKENS_FIELD , GEN_AI_USAGE_INPUT_TOKENS_FIELD ] ,
74+ COMPLETION_TOKENS : [ AI_USAGE_COMPLETION_TOKENS_FIELD , GEN_AI_USAGE_OUTPUT_TOKENS_FIELD ] ,
75+ PROMPT : [ GEN_AI_PROMPT_FIELD , AI_PROMPT_FIELD ] ,
76+ OPERATION_ID : [ AI_OPERATION_ID_FIELD , AI_OPERATION_NAME_FIELD , GEN_AI_OPERATION_NAME_FIELD ] ,
77+ } as const ;
78+
79+ /**
80+ * Extracts a field value from span data, checking v1 fields first then v2 alternatives.
81+ */
82+ function extractFieldFromSpan (
83+ data : Record < string , unknown > ,
84+ fieldKey : keyof typeof AI_FIELD_MAPPINGS ,
85+ ) : unknown | undefined {
86+ const fields = AI_FIELD_MAPPINGS [ fieldKey ] ;
87+ for ( const field of fields ) {
88+ if ( data [ field ] !== undefined ) {
89+ return data [ field ] ;
90+ }
91+ }
92+ return undefined ;
93+ }
94+
4795export const vercelAISDKHandler : AILibraryHandler = {
4896 id : "vercel-ai-sdk" ,
4997 name : "Vercel AI SDK" ,
@@ -110,15 +158,7 @@ export const vercelAISDKHandler: AILibraryHandler = {
110158 return "Tool-Call" ;
111159 }
112160
113- if ( trace . operation === AI_STREAM_TEXT_OPERATION ) {
114- return "Stream Text" ;
115- }
116-
117- if ( trace . operation === AI_GENERATE_TEXT_OPERATION ) {
118- return "Generate Text" ;
119- }
120-
121- return trace . operation . replace ( / ^ ( a i \. | g e n _ a i \. ) / , "" ) ;
161+ return OPERATION_BADGE_MAP . get ( trace . operation ) ?? trace . operation . replace ( / ^ ( a i \. | g e n _ a i \. ) / , "" ) ;
122162 } ,
123163
124164 getTokensDisplay : ( trace : SpotlightAITrace ) : string => {
@@ -177,7 +217,7 @@ function determineOperation(
177217
178218 if ( ! span . data ) continue ;
179219
180- const operationId = ( span . data [ AI_OPERATION_ID_FIELD ] || span . data [ AI_OPERATION_NAME_FIELD ] ) as string | undefined ;
220+ const operationId = extractFieldFromSpan ( span . data , "OPERATION_ID" ) as string | undefined ;
181221
182222 // Handle tool call operation
183223 if ( operationId === AI_TOOL_CALL_OPERATION ) {
@@ -234,6 +274,28 @@ function formatTokensDisplay(promptTokens?: number, completionTokens?: number):
234274 return UNKNOWN_OPERATION ;
235275}
236276
277+ /**
278+ * Normalizes message content from v1 (string) or v2 (array of content blocks) format to a string.
279+ * v1: content = "text string"
280+ * v2: content = [{ type: "text", text: "text string" }]
281+ *
282+ * Only extracts text content blocks. Other block types (image, file, tool-call)
283+ * would require UI changes to display properly and are filtered out for now.
284+ */
285+ function normalizeMessageContent ( content : unknown ) : string {
286+ if ( typeof content === "string" ) return content ;
287+ if ( Array . isArray ( content ) ) {
288+ // Only extract text content - images/files would need UI changes to display
289+ return content
290+ . filter (
291+ ( block ) : block is { type : string ; text : string } => block . type === "text" && typeof block . text === "string" ,
292+ )
293+ . map ( block => block . text )
294+ . join ( "" ) ;
295+ }
296+ return "" ;
297+ }
298+
237299function parseSpanData ( spans : Span [ ] , trace : SpotlightAITrace ) {
238300 for ( const span of spans ) {
239301 if ( ! span . data ) continue ;
@@ -249,16 +311,28 @@ function parseSpanData(spans: Span[], trace: SpotlightAITrace) {
249311function extractAIMetadata ( span : Span , trace : SpotlightAITrace ) {
250312 if ( ! span . data ) return ;
251313
252- if ( span . data [ AI_MODEL_ID_FIELD ] ) {
253- trace . metadata . modelId = String ( span . data [ AI_MODEL_ID_FIELD ] ) ;
314+ // Model ID
315+ if ( trace . metadata . modelId === undefined ) {
316+ const modelId = extractFieldFromSpan ( span . data , "MODEL_ID" ) ;
317+ if ( modelId !== undefined ) {
318+ trace . metadata . modelId = String ( modelId ) ;
319+ }
254320 }
255321
256- if ( span . data [ AI_MODEL_PROVIDER_FIELD ] ) {
257- trace . metadata . modelProvider = String ( span . data [ AI_MODEL_PROVIDER_FIELD ] ) ;
322+ // Model Provider
323+ if ( trace . metadata . modelProvider === undefined ) {
324+ const provider = extractFieldFromSpan ( span . data , "MODEL_PROVIDER" ) ;
325+ if ( provider !== undefined ) {
326+ trace . metadata . modelProvider = String ( provider ) ;
327+ }
258328 }
259329
260- if ( span . data [ AI_TELEMETRY_FUNCTION_ID_FIELD ] ) {
261- trace . metadata . functionId = String ( span . data [ AI_TELEMETRY_FUNCTION_ID_FIELD ] ) ;
330+ // Function ID
331+ if ( trace . metadata . functionId === undefined ) {
332+ const functionId = extractFieldFromSpan ( span . data , "FUNCTION_ID" ) ;
333+ if ( functionId !== undefined ) {
334+ trace . metadata . functionId = String ( functionId ) ;
335+ }
262336 }
263337
264338 if ( span . data [ AI_SETTINGS_MAX_RETRIES_FIELD ] ) {
@@ -269,12 +343,19 @@ function extractAIMetadata(span: Span, trace: SpotlightAITrace) {
269343 trace . metadata . maxSteps = Number ( span . data [ AI_SETTINGS_MAX_STEPS_FIELD ] ) ;
270344 }
271345
272- if ( span . data [ AI_USAGE_PROMPT_TOKENS_FIELD ] ) {
273- trace . metadata . promptTokens = Number ( span . data [ AI_USAGE_PROMPT_TOKENS_FIELD ] ) ;
346+ // Token usage - use === undefined to allow 0 values
347+ if ( trace . metadata . promptTokens === undefined ) {
348+ const promptTokens = extractFieldFromSpan ( span . data , "PROMPT_TOKENS" ) ;
349+ if ( promptTokens !== undefined ) {
350+ trace . metadata . promptTokens = Number ( promptTokens ) ;
351+ }
274352 }
275353
276- if ( span . data [ AI_USAGE_COMPLETION_TOKENS_FIELD ] ) {
277- trace . metadata . completionTokens = Number ( span . data [ AI_USAGE_COMPLETION_TOKENS_FIELD ] ) ;
354+ if ( trace . metadata . completionTokens === undefined ) {
355+ const completionTokens = extractFieldFromSpan ( span . data , "COMPLETION_TOKENS" ) ;
356+ if ( completionTokens !== undefined ) {
357+ trace . metadata . completionTokens = Number ( completionTokens ) ;
358+ }
278359 }
279360}
280361
@@ -292,10 +373,18 @@ function extractTelemetryMetadata(span: Span, trace: SpotlightAITrace) {
292373function extractPromptData ( span : Span , trace : SpotlightAITrace ) {
293374 if ( ! span . data ) return ;
294375
295- const promptField = span . data [ AI_PROMPT_FIELD ] ;
296- if ( promptField ) {
376+ const promptField = extractFieldFromSpan ( span . data , "PROMPT" ) ;
377+ if ( promptField && ! trace . prompt ) {
297378 try {
298- trace . prompt = JSON . parse ( String ( promptField ) ) ;
379+ const parsed = JSON . parse ( String ( promptField ) ) ;
380+ // Normalize message content from v2 array format to string
381+ if ( parsed . messages && Array . isArray ( parsed . messages ) ) {
382+ parsed . messages = parsed . messages . map ( ( msg : { role : string ; content : unknown } ) => ( {
383+ ...msg ,
384+ content : normalizeMessageContent ( msg . content ) ,
385+ } ) ) ;
386+ }
387+ trace . prompt = parsed ;
299388 } catch {
300389 trace . prompt = { messages : [ { role : "unknown" , content : String ( promptField ) } ] } ;
301390 }
@@ -305,7 +394,12 @@ function extractPromptData(span: Span, trace: SpotlightAITrace) {
305394 if ( promptMessages && ! trace . prompt ) {
306395 try {
307396 const messages = JSON . parse ( String ( promptMessages ) ) ;
308- trace . prompt = { messages } ;
397+ // Normalize message content from v2 array format to string
398+ const normalizedMessages = messages . map ( ( msg : { role : string ; content : unknown } ) => ( {
399+ ...msg ,
400+ content : normalizeMessageContent ( msg . content ) ,
401+ } ) ) ;
402+ trace . prompt = { messages : normalizedMessages } ;
309403 } catch {
310404 trace . prompt = { messages : [ { role : "unknown" , content : String ( promptMessages ) } ] } ;
311405 }
0 commit comments