1212import com .fasterxml .jackson .databind .ObjectMapper ;
1313import com .fasterxml .jackson .databind .SerializerProvider ;
1414import com .fasterxml .jackson .databind .module .SimpleModule ;
15+ import com .fasterxml .jackson .annotation .JsonInclude ;
16+ import com .fasterxml .jackson .annotation .JsonProperty ;
1517import com .openai .models .chat .completions .ChatCompletion ;
1618import com .openai .models .chat .completions .ChatCompletionAssistantMessageParam ;
1719import com .openai .models .chat .completions .ChatCompletionContentPartImage ;
1820import com .openai .models .chat .completions .ChatCompletionContentPartText ;
1921import com .openai .models .chat .completions .ChatCompletionCreateParams ;
2022import com .openai .models .chat .completions .ChatCompletionDeveloperMessageParam ;
2123import com .openai .models .chat .completions .ChatCompletionMessage ;
24+ import com .openai .models .chat .completions .ChatCompletionMessageParam ;
2225import com .openai .models .chat .completions .ChatCompletionMessageToolCall ;
2326import com .openai .models .chat .completions .ChatCompletionSystemMessageParam ;
2427import com .openai .models .chat .completions .ChatCompletionToolMessageParam ;
3437import java .lang .invoke .MethodHandle ;
3538import java .lang .invoke .MethodHandles ;
3639import java .lang .invoke .MethodType ;
40+ import java .util .ArrayList ;
3741import java .util .HashMap ;
3842import java .util .List ;
3943import java .util .Map ;
@@ -54,6 +58,7 @@ private static ObjectMapper createObjectMapper() {
5458 final JsonSerializer <Base64Attachment > attachmentSerializer =
5559 Base64Attachment .createSerializer ();
5660 ObjectMapper mapper = new ObjectMapper ();
61+ mapper .setSerializationInclusion (JsonInclude .Include .NON_NULL );
5762 SimpleModule module = new SimpleModule ();
5863 module .addSerializer (
5964 ChatCompletionContentPartImage .class ,
@@ -81,16 +86,324 @@ public void serialize(
8186 return mapper ;
8287 }
8388
89+ // OTel GenAI Semantic Convention structures
90+ static class SemconvChatMessage {
91+ @ JsonProperty ("role" )
92+ public final String role ;
93+
94+ @ JsonProperty ("parts" )
95+ public final List <Object > parts ;
96+
97+ public SemconvChatMessage (String role , List <Object > parts ) {
98+ this .role = role ;
99+ this .parts = parts ;
100+ }
101+ }
102+
103+ @ JsonInclude (JsonInclude .Include .NON_NULL )
104+ static class TextPart {
105+ @ JsonProperty ("type" )
106+ public final String type = "text" ;
107+
108+ @ JsonProperty ("content" )
109+ public final String content ;
110+
111+ public TextPart (String content ) {
112+ this .content = content ;
113+ }
114+ }
115+
116+ @ JsonInclude (JsonInclude .Include .NON_NULL )
117+ static class ToolCallRequestPart {
118+ @ JsonProperty ("type" )
119+ public final String type = "tool_call" ;
120+
121+ @ JsonProperty ("name" )
122+ public final String name ;
123+
124+ @ JsonProperty ("id" )
125+ public final String id ;
126+
127+ @ JsonProperty ("arguments" )
128+ public final String arguments ;
129+
130+ public ToolCallRequestPart (String name , String id , String arguments ) {
131+ this .name = name ;
132+ this .id = id ;
133+ this .arguments = arguments ;
134+ }
135+ }
136+
137+ @ JsonInclude (JsonInclude .Include .NON_NULL )
138+ static class ToolCallResponsePart {
139+ @ JsonProperty ("type" )
140+ public final String type = "tool_call_response" ;
141+
142+ @ JsonProperty ("id" )
143+ public final String id ;
144+
145+ @ JsonProperty ("response" )
146+ public final String response ;
147+
148+ public ToolCallResponsePart (String id , String response ) {
149+ this .id = id ;
150+ this .response = response ;
151+ }
152+ }
153+
154+ @ JsonInclude (JsonInclude .Include .NON_NULL )
155+ static class GenericPart {
156+ @ JsonProperty ("type" )
157+ public final String type ;
158+
159+ @ JsonProperty ("data" )
160+ public final Object data ;
161+
162+ public GenericPart (String type , Object data ) {
163+ this .type = type ;
164+ this .data = data ;
165+ }
166+ }
167+
168+ // Transform OpenAI messages to OTel GenAI semconv format
169+ private static List <SemconvChatMessage > transformToSemconvMessages (
170+ List <ChatCompletionMessageParam > messages ) {
171+ return messages .stream ()
172+ .map (ChatCompletionEventsHelper ::transformMessage )
173+ .filter (Objects ::nonNull )
174+ .collect (Collectors .toList ());
175+ }
176+
177+ @ Nullable
178+ private static SemconvChatMessage transformMessage (ChatCompletionMessageParam message ) {
179+ // Try each union type accessor
180+ var userOpt = tryGetUser (message );
181+ if (userOpt .isPresent ()) {
182+ return transformUserMessage (userOpt .get ());
183+ }
184+
185+ var systemOpt = tryGetSystem (message );
186+ if (systemOpt .isPresent ()) {
187+ return transformSystemMessage (systemOpt .get ());
188+ }
189+
190+ var assistantOpt = tryGetAssistant (message );
191+ if (assistantOpt .isPresent ()) {
192+ return transformAssistantMessage (assistantOpt .get ());
193+ }
194+
195+ var toolOpt = tryGetTool (message );
196+ if (toolOpt .isPresent ()) {
197+ return transformToolMessage (toolOpt .get ());
198+ }
199+
200+ var developerOpt = tryGetDeveloper (message );
201+ if (developerOpt .isPresent ()) {
202+ return transformDeveloperMessage (developerOpt .get ());
203+ }
204+
205+ return null ;
206+ }
207+
208+ private static Optional <ChatCompletionUserMessageParam > tryGetUser (
209+ ChatCompletionMessageParam message ) {
210+ try {
211+ var method = message .getClass ().getMethod ("user" );
212+ @ SuppressWarnings ("unchecked" )
213+ var result = (Optional <ChatCompletionUserMessageParam >) method .invoke (message );
214+ return result ;
215+ } catch (Exception e ) {
216+ return Optional .empty ();
217+ }
218+ }
219+
220+ private static Optional <ChatCompletionSystemMessageParam > tryGetSystem (
221+ ChatCompletionMessageParam message ) {
222+ try {
223+ var method = message .getClass ().getMethod ("system" );
224+ @ SuppressWarnings ("unchecked" )
225+ var result = (Optional <ChatCompletionSystemMessageParam >) method .invoke (message );
226+ return result ;
227+ } catch (Exception e ) {
228+ return Optional .empty ();
229+ }
230+ }
231+
232+ private static Optional <ChatCompletionAssistantMessageParam > tryGetAssistant (
233+ ChatCompletionMessageParam message ) {
234+ try {
235+ var method = message .getClass ().getMethod ("assistant" );
236+ @ SuppressWarnings ("unchecked" )
237+ var result = (Optional <ChatCompletionAssistantMessageParam >) method .invoke (message );
238+ return result ;
239+ } catch (Exception e ) {
240+ return Optional .empty ();
241+ }
242+ }
243+
244+ private static Optional <ChatCompletionToolMessageParam > tryGetTool (
245+ ChatCompletionMessageParam message ) {
246+ try {
247+ var method = message .getClass ().getMethod ("tool" );
248+ @ SuppressWarnings ("unchecked" )
249+ var result = (Optional <ChatCompletionToolMessageParam >) method .invoke (message );
250+ return result ;
251+ } catch (Exception e ) {
252+ return Optional .empty ();
253+ }
254+ }
255+
256+ private static Optional <ChatCompletionDeveloperMessageParam > tryGetDeveloper (
257+ ChatCompletionMessageParam message ) {
258+ try {
259+ var method = message .getClass ().getMethod ("developer" );
260+ @ SuppressWarnings ("unchecked" )
261+ var result = (Optional <ChatCompletionDeveloperMessageParam >) method .invoke (message );
262+ return result ;
263+ } catch (Exception e ) {
264+ return Optional .empty ();
265+ }
266+ }
267+
268+ private static SemconvChatMessage transformUserMessage (ChatCompletionUserMessageParam message ) {
269+ List <Object > parts = new ArrayList <>();
270+ ChatCompletionUserMessageParam .Content content = message .content ();
271+
272+ if (content .isText ()) {
273+ parts .add (new TextPart (content .asText ()));
274+ } else if (content .isArrayOfContentParts ()) {
275+ for (var part : content .asArrayOfContentParts ()) {
276+ if (part .isText ()) {
277+ parts .add (new TextPart (part .asText ().text ()));
278+ } else {
279+ // Try to get image part using union type accessor
280+ var imageOpt = tryGetImagePart (part );
281+ if (imageOpt .isPresent ()) {
282+ try {
283+ var imageUrl = imageOpt .get ().imageUrl ().url ();
284+ var attachment = Base64Attachment .of (imageUrl );
285+ parts .add (attachment );
286+ } catch (Exception e ) {
287+ log .debug ("Failed to parse image URL" , e );
288+ }
289+ }
290+ }
291+ }
292+ }
293+
294+ return new SemconvChatMessage ("user" , parts );
295+ }
296+
297+ private static Optional <ChatCompletionContentPartImage > tryGetImagePart (
298+ com .openai .models .chat .completions .ChatCompletionContentPart part ) {
299+ try {
300+ var method = part .getClass ().getMethod ("imageUrl" );
301+ @ SuppressWarnings ("unchecked" )
302+ var result = (Optional <ChatCompletionContentPartImage >) method .invoke (part );
303+ return result ;
304+ } catch (Exception e ) {
305+ return Optional .empty ();
306+ }
307+ }
308+
309+ private static SemconvChatMessage transformSystemMessage (
310+ ChatCompletionSystemMessageParam message ) {
311+ List <Object > parts = new ArrayList <>();
312+ ChatCompletionSystemMessageParam .Content content = message .content ();
313+
314+ if (content .isText ()) {
315+ parts .add (new TextPart (content .asText ()));
316+ } else if (content .isArrayOfContentParts ()) {
317+ for (var part : content .asArrayOfContentParts ()) {
318+ parts .add (new TextPart (part .text ()));
319+ }
320+ }
321+
322+ return new SemconvChatMessage ("system" , parts );
323+ }
324+
325+ private static SemconvChatMessage transformDeveloperMessage (
326+ ChatCompletionDeveloperMessageParam message ) {
327+ List <Object > parts = new ArrayList <>();
328+ ChatCompletionDeveloperMessageParam .Content content = message .content ();
329+
330+ if (content .isText ()) {
331+ parts .add (new TextPart (content .asText ()));
332+ } else if (content .isArrayOfContentParts ()) {
333+ for (var part : content .asArrayOfContentParts ()) {
334+ parts .add (new TextPart (part .text ()));
335+ }
336+ }
337+
338+ return new SemconvChatMessage ("system" , parts );
339+ }
340+
341+ private static SemconvChatMessage transformAssistantMessage (
342+ ChatCompletionAssistantMessageParam message ) {
343+ List <Object > parts = new ArrayList <>();
344+
345+ // Handle text content
346+ message .content ()
347+ .ifPresent (
348+ content -> {
349+ if (content .isText ()) {
350+ parts .add (new TextPart (content .asText ()));
351+ } else if (content .isArrayOfContentParts ()) {
352+ for (var part : content .asArrayOfContentParts ()) {
353+ if (part .isText ()) {
354+ parts .add (new TextPart (part .asText ().text ()));
355+ } else if (part .isRefusal ()) {
356+ parts .add (new TextPart (part .asRefusal ().refusal ()));
357+ }
358+ }
359+ }
360+ });
361+
362+ // Handle tool calls
363+ message .toolCalls ()
364+ .ifPresent (
365+ toolCalls -> {
366+ for (var toolCall : toolCalls ) {
367+ FunctionAccess functionAccess = getFunctionAccess (toolCall );
368+ if (functionAccess != null ) {
369+ parts .add (
370+ new ToolCallRequestPart (
371+ functionAccess .name (),
372+ functionAccess .id (),
373+ functionAccess .arguments ()));
374+ }
375+ }
376+ });
377+
378+ return new SemconvChatMessage ("assistant" , parts );
379+ }
380+
381+ private static SemconvChatMessage transformToolMessage (ChatCompletionToolMessageParam message ) {
382+ List <Object > parts = new ArrayList <>();
383+ String toolCallId = message .toolCallId ();
384+ ChatCompletionToolMessageParam .Content content = message .content ();
385+
386+ String responseContent = "" ;
387+ if (content .isText ()) {
388+ responseContent = content .asText ();
389+ } else if (content .isArrayOfContentParts ()) {
390+ responseContent = joinContentParts (content .asArrayOfContentParts ());
391+ }
392+
393+ parts .add (new ToolCallResponsePart (toolCallId , responseContent ));
394+ return new SemconvChatMessage ("tool" , parts );
395+ }
396+
84397 @ SneakyThrows
85398 public static void emitPromptLogEvents (
86399 Context context ,
87400 Logger eventLogger ,
88401 ChatCompletionCreateParams request ,
89402 boolean captureMessageContent ) {
90- Span . current ()
91- . setAttribute (
92- "gen_ai.input.messages" ,
93- JSON_MAPPER . writeValueAsString ( request . messages ()) );
403+ List < SemconvChatMessage > semconvMessages = transformToSemconvMessages ( request . messages ());
404+ String semconvJson = JSON_MAPPER . writeValueAsString ( semconvMessages );
405+ Span span = Span . current ();
406+ span . setAttribute ( "gen_ai.input. messages" , semconvJson );
94407 }
95408
96409 private static String contentToString (ChatCompletionToolMessageParam .Content content ) {
0 commit comments