@@ -100,6 +100,24 @@ public SemconvChatMessage(String role, List<Object> parts) {
100100 }
101101 }
102102
103+ @ JsonInclude (JsonInclude .Include .NON_NULL )
104+ static class SemconvOutputChatMessage {
105+ @ JsonProperty ("role" )
106+ public final String role ;
107+
108+ @ JsonProperty ("parts" )
109+ public final List <Object > parts ;
110+
111+ @ JsonProperty ("finish_reason" )
112+ public final String finishReason ;
113+
114+ public SemconvOutputChatMessage (String role , List <Object > parts , String finishReason ) {
115+ this .role = role ;
116+ this .parts = parts ;
117+ this .finishReason = finishReason ;
118+ }
119+ }
120+
103121 @ JsonInclude (JsonInclude .Include .NON_NULL )
104122 static class TextPart {
105123 @ JsonProperty ("type" )
@@ -394,6 +412,38 @@ private static SemconvChatMessage transformToolMessage(ChatCompletionToolMessage
394412 return new SemconvChatMessage ("tool" , parts );
395413 }
396414
415+ // Transform ChatCompletionMessage (output) to OTel GenAI semconv format
416+ private static SemconvOutputChatMessage transformOutputMessage (
417+ ChatCompletionMessage message , String finishReason ) {
418+ List <Object > parts = new ArrayList <>();
419+
420+ // Handle text content
421+ message .content ()
422+ .ifPresent (content -> {
423+ if (!content .isEmpty ()) {
424+ parts .add (new TextPart (content ));
425+ }
426+ });
427+
428+ // Handle tool calls
429+ message .toolCalls ()
430+ .ifPresent (toolCalls -> {
431+ for (var toolCall : toolCalls ) {
432+ FunctionAccess functionAccess = getFunctionAccess (toolCall );
433+ if (functionAccess != null ) {
434+ parts .add (
435+ new ToolCallRequestPart (
436+ functionAccess .name (),
437+ functionAccess .id (),
438+ functionAccess .arguments ()));
439+ }
440+ }
441+ });
442+
443+ // The role from ChatCompletionMessage is always "assistant" for output messages
444+ return new SemconvOutputChatMessage ("assistant" , parts , finishReason );
445+ }
446+
397447 @ SneakyThrows
398448 public static void emitPromptLogEvents (
399449 Context context ,
@@ -488,14 +538,13 @@ public static void emitCompletionLogEvents(
488538 } else if (completion .choices ().size () > 1 ) {
489539 log .debug ("multiple choices in OAI response: {}" , completion .choices ().size ());
490540 } else {
491- Span .current ()
492- .setAttribute (
493- // "gen_ai.output.messages",
494- "braintrust.output_json" ,
495- JSON_MAPPER .writeValueAsString (
496- new ChatCompletionMessage [] {
497- completion .choices ().get (0 ).message ()
498- }));
541+ // Set gen_ai.output.messages attribute for single choice (most common case)
542+ ChatCompletion .Choice choice = completion .choices ().get (0 );
543+ SemconvOutputChatMessage outputMessage = transformOutputMessage (
544+ choice .message (),
545+ choice .finishReason ().toString ());
546+ String outputJson = JSON_MAPPER .writeValueAsString (new SemconvOutputChatMessage [] {outputMessage });
547+ Span .current ().setAttribute ("gen_ai.output.messages" , outputJson );
499548 }
500549 for (ChatCompletion .Choice choice : completion .choices ()) {
501550 ChatCompletionMessage choiceMsg = choice .message ();
0 commit comments