@@ -19,6 +19,10 @@ const AI_USAGE_PROMPT_TOKENS = "ai.usage.promptTokens";
1919const AI_USAGE_COMPLETION_TOKENS = "ai.usage.completionTokens" ;
2020const AI_MODEL_PROVIDER = "ai.model.provider" ;
2121const AI_PROMPT_TOOLS = "ai.prompt.tools" ;
22+ const TYPE_TEXT = "text" ;
23+ const TYPE_TOOL_CALL = "tool_call" ;
24+ const ROLE_ASSISTANT = "assistant" ;
25+ const ROLE_USER = "user" ;
2226
2327// Vendor mapping from AI SDK provider prefixes to standardized LLM_SYSTEM values
2428// Uses prefixes to match AI SDK patterns like "openai.chat", "anthropic.messages", etc.
@@ -55,7 +59,21 @@ const transformResponseText = (attributes: Record<string, any>): void => {
5559 if ( AI_RESPONSE_TEXT in attributes ) {
5660 attributes [ `${ SpanAttributes . LLM_COMPLETIONS } .0.content` ] =
5761 attributes [ AI_RESPONSE_TEXT ] ;
58- attributes [ `${ SpanAttributes . LLM_COMPLETIONS } .0.role` ] = "assistant" ;
62+ attributes [ `${ SpanAttributes . LLM_COMPLETIONS } .0.role` ] = ROLE_ASSISTANT ;
63+
64+ const outputMessage = {
65+ role : ROLE_ASSISTANT ,
66+ parts : [
67+ {
68+ type : TYPE_TEXT ,
69+ content : attributes [ AI_RESPONSE_TEXT ] ,
70+ } ,
71+ ] ,
72+ } ;
73+ attributes [ SpanAttributes . LLM_OUTPUT_MESSAGES ] = JSON . stringify ( [
74+ outputMessage ,
75+ ] ) ;
76+
5977 delete attributes [ AI_RESPONSE_TEXT ] ;
6078 }
6179} ;
@@ -64,7 +82,21 @@ const transformResponseObject = (attributes: Record<string, any>): void => {
6482 if ( AI_RESPONSE_OBJECT in attributes ) {
6583 attributes [ `${ SpanAttributes . LLM_COMPLETIONS } .0.content` ] =
6684 attributes [ AI_RESPONSE_OBJECT ] ;
67- attributes [ `${ SpanAttributes . LLM_COMPLETIONS } .0.role` ] = "assistant" ;
85+ attributes [ `${ SpanAttributes . LLM_COMPLETIONS } .0.role` ] = ROLE_ASSISTANT ;
86+
87+ const outputMessage = {
88+ role : ROLE_ASSISTANT ,
89+ parts : [
90+ {
91+ type : TYPE_TEXT ,
92+ content : attributes [ AI_RESPONSE_OBJECT ] ,
93+ } ,
94+ ] ,
95+ } ;
96+ attributes [ SpanAttributes . LLM_OUTPUT_MESSAGES ] = JSON . stringify ( [
97+ outputMessage ,
98+ ] ) ;
99+
68100 delete attributes [ AI_RESPONSE_OBJECT ] ;
69101 }
70102} ;
@@ -76,8 +108,9 @@ const transformResponseToolCalls = (attributes: Record<string, any>): void => {
76108 attributes [ AI_RESPONSE_TOOL_CALLS ] as string ,
77109 ) ;
78110
79- attributes [ `${ SpanAttributes . LLM_COMPLETIONS } .0.role` ] = "assistant" ;
111+ attributes [ `${ SpanAttributes . LLM_COMPLETIONS } .0.role` ] = ROLE_ASSISTANT ;
80112
113+ const toolCallParts : any [ ] = [ ] ;
81114 toolCalls . forEach ( ( toolCall : any , index : number ) => {
82115 if ( toolCall . toolCallType === "function" ) {
83116 attributes [
@@ -86,9 +119,27 @@ const transformResponseToolCalls = (attributes: Record<string, any>): void => {
86119 attributes [
87120 `${ SpanAttributes . LLM_COMPLETIONS } .0.tool_calls.${ index } .arguments`
88121 ] = toolCall . args ;
122+
123+ toolCallParts . push ( {
124+ type : TYPE_TOOL_CALL ,
125+ tool_call : {
126+ name : toolCall . toolName ,
127+ arguments : toolCall . args ,
128+ } ,
129+ } ) ;
89130 }
90131 } ) ;
91132
133+ if ( toolCallParts . length > 0 ) {
134+ const outputMessage = {
135+ role : ROLE_ASSISTANT ,
136+ parts : toolCallParts ,
137+ } ;
138+ attributes [ SpanAttributes . LLM_OUTPUT_MESSAGES ] = JSON . stringify ( [
139+ outputMessage ,
140+ ] ) ;
141+ }
142+
92143 delete attributes [ AI_RESPONSE_TOOL_CALLS ] ;
93144 } catch {
94145 // Ignore parsing errors
@@ -100,7 +151,10 @@ const processMessageContent = (content: any): string => {
100151 if ( Array . isArray ( content ) ) {
101152 const textItems = content . filter (
102153 ( item : any ) =>
103- item && typeof item === "object" && item . type === "text" && item . text ,
154+ item &&
155+ typeof item === "object" &&
156+ item . type === TYPE_TEXT &&
157+ item . text ,
104158 ) ;
105159
106160 if ( textItems . length > 0 ) {
@@ -112,7 +166,7 @@ const processMessageContent = (content: any): string => {
112166 }
113167
114168 if ( content && typeof content === "object" ) {
115- if ( content . type === "text" && content . text ) {
169+ if ( content . type === TYPE_TEXT && content . text ) {
116170 return content . text ;
117171 }
118172 return JSON . stringify ( content ) ;
@@ -126,7 +180,7 @@ const processMessageContent = (content: any): string => {
126180 ( item : any ) =>
127181 item &&
128182 typeof item === "object" &&
129- item . type === "text" &&
183+ item . type === TYPE_TEXT &&
130184 item . text ,
131185 ) ;
132186
@@ -205,12 +259,32 @@ const transformPrompts = (attributes: Record<string, any>): void => {
205259 }
206260
207261 const messages = JSON . parse ( jsonString ) ;
262+ const inputMessages : any [ ] = [ ] ;
263+
208264 messages . forEach ( ( msg : { role : string ; content : any } , index : number ) => {
209265 const processedContent = processMessageContent ( msg . content ) ;
210266 const contentKey = `${ SpanAttributes . LLM_PROMPTS } .${ index } .content` ;
211267 attributes [ contentKey ] = processedContent ;
212268 attributes [ `${ SpanAttributes . LLM_PROMPTS } .${ index } .role` ] = msg . role ;
269+
270+ // Add to OpenTelemetry standard gen_ai.input.messages format
271+ inputMessages . push ( {
272+ role : msg . role ,
273+ parts : [
274+ {
275+ type : TYPE_TEXT ,
276+ content : processedContent ,
277+ } ,
278+ ] ,
279+ } ) ;
213280 } ) ;
281+
282+ // Set the OpenTelemetry standard input messages attribute
283+ if ( inputMessages . length > 0 ) {
284+ attributes [ SpanAttributes . LLM_INPUT_MESSAGES ] =
285+ JSON . stringify ( inputMessages ) ;
286+ }
287+
214288 delete attributes [ AI_PROMPT_MESSAGES ] ;
215289 } catch {
216290 // Ignore parsing errors
@@ -223,7 +297,21 @@ const transformPrompts = (attributes: Record<string, any>): void => {
223297 if ( promptData . prompt && typeof promptData . prompt === "string" ) {
224298 attributes [ `${ SpanAttributes . LLM_PROMPTS } .0.content` ] =
225299 promptData . prompt ;
226- attributes [ `${ SpanAttributes . LLM_PROMPTS } .0.role` ] = "user" ;
300+ attributes [ `${ SpanAttributes . LLM_PROMPTS } .0.role` ] = ROLE_USER ;
301+
302+ const inputMessage = {
303+ role : ROLE_USER ,
304+ parts : [
305+ {
306+ type : TYPE_TEXT ,
307+ content : promptData . prompt ,
308+ } ,
309+ ] ,
310+ } ;
311+ attributes [ SpanAttributes . LLM_INPUT_MESSAGES ] = JSON . stringify ( [
312+ inputMessage ,
313+ ] ) ;
314+
227315 delete attributes [ AI_PROMPT ] ;
228316 }
229317 } catch {
0 commit comments