Skip to content

Commit f377ef8

Browse files
committed
wip
1 parent 989a1bf commit f377ef8

1 file changed

Lines changed: 57 additions & 8 deletions

File tree

src/main/java/dev/braintrust/instrumentation/openai/otel/ChatCompletionEventsHelper.java

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)