From 09d342b30b7db4c31c1cd4c82b701cd2da24ef8e Mon Sep 17 00:00:00 2001 From: cirilla-zmh Date: Mon, 8 Sep 2025 22:16:23 +0800 Subject: [PATCH 1/2] init genai messages --- .../build.gradle.kts | 1 + .../semconv/genai/messages/GenericPart.java | 28 ++ .../semconv/genai/messages/InputMessage.java | 31 ++ .../semconv/genai/messages/InputMessages.java | 39 ++ .../semconv/genai/messages/MessagePart.java | 13 + .../semconv/genai/messages/OutputMessage.java | 32 ++ .../genai/messages/OutputMessages.java | 104 +++++ .../semconv/genai/messages/Role.java | 19 + .../genai/messages/SystemInstructions.java | 32 ++ .../semconv/genai/messages/TextPart.java | 26 ++ .../genai/messages/ToolCallRequestPart.java | 41 ++ .../genai/messages/ToolCallResponsePart.java | 36 ++ .../v1_1/ChatCompletionMessagesConverter.java | 416 ++++++++++++++++++ 13 files changed, 818 insertions(+) create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/GenericPart.java create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/InputMessage.java create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/InputMessages.java create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/MessagePart.java create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/OutputMessage.java create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/OutputMessages.java create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/Role.java create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/SystemInstructions.java create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/TextPart.java create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/ToolCallRequestPart.java create mode 100644 instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/ToolCallResponsePart.java create mode 100644 instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/ChatCompletionMessagesConverter.java diff --git a/instrumentation-api-incubator/build.gradle.kts b/instrumentation-api-incubator/build.gradle.kts index c25f6aea378d..c9cf70932ee8 100644 --- a/instrumentation-api-incubator/build.gradle.kts +++ b/instrumentation-api-incubator/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { api("io.opentelemetry:opentelemetry-api-incubator") compileOnly("com.google.auto.value:auto-value-annotations") + compileOnly("com.fasterxml.jackson.core:jackson-databind") annotationProcessor("com.google.auto.value:auto-value") testImplementation(project(":testing-common")) diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/GenericPart.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/GenericPart.java new file mode 100644 index 000000000000..855a8cd1502b --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/GenericPart.java @@ -0,0 +1,28 @@ +package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages; + +import com.fasterxml.jackson.annotation.JsonClassDescription; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.google.auto.value.AutoValue; +import java.util.Map; + +/** + * Represents an arbitrary message part with any type and properties. + * This allows for extensibility with custom message part types. + */ +@AutoValue +@JsonClassDescription("Generic part") +public abstract class GenericPart implements MessagePart { + + @JsonProperty(required = true, value = "type") + @JsonPropertyDescription("The type of the content captured in this part") + public abstract String getType(); + + public static GenericPart create(String type) { + return new AutoValue_GenericPart(type); + } + + public static GenericPart create(String type, Map additionalProperties) { + return new AutoValue_GenericPart(type); + } +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/InputMessage.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/InputMessage.java new file mode 100644 index 000000000000..21269b59ffb7 --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/InputMessage.java @@ -0,0 +1,31 @@ +package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages; + +import com.fasterxml.jackson.annotation.JsonClassDescription; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.google.auto.value.AutoValue; +import java.util.List; + +/** + * Represents an input message sent to the model. + */ +@AutoValue +@JsonClassDescription("Input message") +public abstract class InputMessage { + + @JsonProperty(required = true, value = "role") + @JsonPropertyDescription("Role of the entity that created the message") + public abstract String getRole(); + + @JsonProperty(required = true, value = "parts") + @JsonPropertyDescription("List of message parts that make up the message content") + public abstract List getParts(); + + public static InputMessage create(String role, List parts) { + return new AutoValue_InputMessage(role, parts); + } + + public static InputMessage create(Role role, List parts) { + return new AutoValue_InputMessage(role.getValue(), parts); + } +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/InputMessages.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/InputMessages.java new file mode 100644 index 000000000000..a699748644c0 --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/InputMessages.java @@ -0,0 +1,39 @@ +package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.auto.value.AutoValue; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a collection of input messages sent to the model. + */ +@AutoValue +public abstract class InputMessages { + + public abstract List getMessages(); + + public static InputMessages create() { + return new AutoValue_InputMessages(new ArrayList<>()); + } + + public static InputMessages create(List messages) { + return new AutoValue_InputMessages(new ArrayList<>(messages)); + } + + public InputMessages append(InputMessage inputMessage) { + List messages = getMessages(); + messages.add(inputMessage); + return this; + } + + public String toJsonString() { + try { + ObjectMapper mapper = new ObjectMapper(); + return mapper.writeValueAsString(this); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to serialize InputMessages to JSON", e); + } + } +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/MessagePart.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/MessagePart.java new file mode 100644 index 000000000000..7b0dc6eaa048 --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/MessagePart.java @@ -0,0 +1,13 @@ +package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages; + +/** + * Interface for all message parts. + */ +public interface MessagePart { + + /** + * Get the type of this message part. + * @return the type string + */ + String getType(); +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/OutputMessage.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/OutputMessage.java new file mode 100644 index 000000000000..f44b79df93bd --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/OutputMessage.java @@ -0,0 +1,32 @@ +package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages; + +import com.fasterxml.jackson.annotation.JsonClassDescription; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.google.auto.value.AutoValue; +import java.util.List; + +@AutoValue +@JsonClassDescription("Output message") +public abstract class OutputMessage { + + @JsonProperty(required = true, value = "role") + @JsonPropertyDescription("Role of response") + public abstract String getRole(); + + @JsonProperty(required = true, value = "parts") + @JsonPropertyDescription("List of message parts that make up the message content") + public abstract List getParts(); + + @JsonProperty(required = true, value = "finish_reason") + @JsonPropertyDescription("Reason for finishing the generation") + public abstract String getFinishReason(); + + public static OutputMessage create(String role, List parts, String finishReason) { + return new AutoValue_OutputMessage(role, parts, finishReason); + } + + public static OutputMessage create(Role role, List parts, String finishReason) { + return new AutoValue_OutputMessage(role.getValue(), parts, finishReason); + } +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/OutputMessages.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/OutputMessages.java new file mode 100644 index 000000000000..83e588a8a730 --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/OutputMessages.java @@ -0,0 +1,104 @@ +package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.auto.value.AutoValue; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a collection of output messages from the model. + */ +@AutoValue +public abstract class OutputMessages { + + public abstract List getMessages(); + + public static OutputMessages create() { + return new AutoValue_OutputMessages(new ArrayList<>()); + } + + public static OutputMessages create(List messages) { + return new AutoValue_OutputMessages(new ArrayList<>(messages)); + } + + public OutputMessages append(OutputMessage outputMessage) { + List currentMessages = new ArrayList<>(getMessages()); + currentMessages.add(outputMessage); + return new AutoValue_OutputMessages(currentMessages); + } + + public String toJsonString() { + try { + ObjectMapper mapper = new ObjectMapper(); + return mapper.writeValueAsString(this); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to serialize OutputMessages to JSON", e); + } + } + + /** + * Merges a chunk OutputMessage into the existing messages at the specified index. + * This method is used for streaming responses where content is received in chunks. + * + * @param index the index of the message to merge into + * @param chunkMessage the chunk message to append + * @return a new OutputMessages instance with the merged content + */ + public OutputMessages merge(int index, OutputMessage chunkMessage) { + List currentMessages = new ArrayList<>(getMessages()); + + if (index < 0 || index >= currentMessages.size()) { + throw new IllegalArgumentException("Index " + index + " is out of bounds for messages list of size " + currentMessages.size()); + } + + OutputMessage existingMessage = currentMessages.get(index); + + // Merge the parts by appending text content from chunk to existing message + List mergedParts = new ArrayList<>(existingMessage.getParts()); + + // If the chunk message has text parts, append their content to the first text part of existing message + for (MessagePart chunkPart : chunkMessage.getParts()) { + if (chunkPart instanceof TextPart) { + TextPart chunkTextPart = (TextPart) chunkPart; + + // Find the first text part in existing message to append to + boolean appended = false; + for (int i = 0; i < mergedParts.size(); i++) { + MessagePart existingPart = mergedParts.get(i); + if (existingPart instanceof TextPart) { + TextPart existingTextPart = (TextPart) existingPart; + // Create a new TextPart with combined content + TextPart mergedTextPart = TextPart.create(existingTextPart.getContent() + chunkTextPart.getContent()); + mergedParts.set(i, mergedTextPart); + appended = true; + break; + } + } + + // If no existing text part found, add the chunk as a new part + if (!appended) { + mergedParts.add(chunkTextPart); + } + } else { + // For non-text parts, add them as new parts + mergedParts.add(chunkPart); + } + } + + // Create new OutputMessage with merged parts, using the chunk's finish reason if available + String finalFinishReason = chunkMessage.getFinishReason() != null ? + chunkMessage.getFinishReason() : existingMessage.getFinishReason(); + + OutputMessage mergedMessage = OutputMessage.create( + existingMessage.getRole(), + mergedParts, + finalFinishReason + ); + + // Replace the message at the specified index + currentMessages.set(index, mergedMessage); + + return new AutoValue_OutputMessages(currentMessages); + } +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/Role.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/Role.java new file mode 100644 index 000000000000..8938f4cf0966 --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/Role.java @@ -0,0 +1,19 @@ +package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages; + +public enum Role { + SYSTEM("system"), + USER("user"), + ASSISTANT("assistant"), + TOOL("tool"), + DEVELOPER("developer"); + + private final String value; + + public String getValue() { + return value; + } + + Role(String value) { + this.value = value; + } +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/SystemInstructions.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/SystemInstructions.java new file mode 100644 index 000000000000..c67eb1446274 --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/SystemInstructions.java @@ -0,0 +1,32 @@ +package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages; + +import com.fasterxml.jackson.annotation.JsonClassDescription; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.auto.value.AutoValue; +import java.util.List; + +/** + * Represents the list of system instructions sent to the model. + */ +@AutoValue +@JsonClassDescription("System instructions") +public abstract class SystemInstructions { + + @JsonPropertyDescription("List of message parts that make up the system instructions") + public abstract List getParts(); + + public static SystemInstructions create(List parts) { + return new AutoValue_SystemInstructions(parts); + } + + public String toJsonString() { + try { + ObjectMapper mapper = new ObjectMapper(); + return mapper.writeValueAsString(this); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to serialize SystemInstructions to JSON", e); + } + } +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/TextPart.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/TextPart.java new file mode 100644 index 000000000000..c40c9c1f53aa --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/TextPart.java @@ -0,0 +1,26 @@ +package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages; + +import com.fasterxml.jackson.annotation.JsonClassDescription; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.google.auto.value.AutoValue; + +/** + * Represents text content sent to or received from the model. + */ +@AutoValue +@JsonClassDescription("Text part") +public abstract class TextPart implements MessagePart { + + @JsonProperty(required = true, value = "type") + @JsonPropertyDescription("The type of the content captured in this part") + public abstract String getType(); + + @JsonProperty(required = true, value = "content") + @JsonPropertyDescription("Text content sent to or received from the model") + public abstract String getContent(); + + public static TextPart create(String content) { + return new AutoValue_TextPart("text", content); + } +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/ToolCallRequestPart.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/ToolCallRequestPart.java new file mode 100644 index 000000000000..613b537eab16 --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/ToolCallRequestPart.java @@ -0,0 +1,41 @@ +package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages; + +import com.fasterxml.jackson.annotation.JsonClassDescription; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.google.auto.value.AutoValue; +import javax.annotation.Nullable; + +/** + * Represents a tool call requested by the model. + */ +@AutoValue +@JsonClassDescription("Tool call request part") +public abstract class ToolCallRequestPart implements MessagePart { + + @JsonProperty(required = true, value = "type") + @JsonPropertyDescription("The type of the content captured in this part") + public abstract String getType(); + + @JsonProperty(value = "id") + @JsonPropertyDescription("Unique identifier for the tool call") + @Nullable + public abstract String getId(); + + @JsonProperty(required = true, value = "name") + @JsonPropertyDescription("Name of the tool") + public abstract String getName(); + + @JsonProperty(value = "arguments") + @JsonPropertyDescription("Arguments for the tool call") + @Nullable + public abstract Object getArguments(); + + public static ToolCallRequestPart create(String name) { + return new AutoValue_ToolCallRequestPart("tool_call", null, name, null); + } + + public static ToolCallRequestPart create(String id, String name, Object arguments) { + return new AutoValue_ToolCallRequestPart("tool_call", id, name, arguments); + } +} diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/ToolCallResponsePart.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/ToolCallResponsePart.java new file mode 100644 index 000000000000..dd7940e1e299 --- /dev/null +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/ToolCallResponsePart.java @@ -0,0 +1,36 @@ +package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages; + +import com.fasterxml.jackson.annotation.JsonClassDescription; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.google.auto.value.AutoValue; +import javax.annotation.Nullable; + +/** + * Represents a tool call result sent to the model or a built-in tool call outcome and details. + */ +@AutoValue +@JsonClassDescription("Tool call response part") +public abstract class ToolCallResponsePart implements MessagePart { + + @JsonProperty(required = true, value = "type") + @JsonPropertyDescription("The type of the content captured in this part") + public abstract String getType(); + + @JsonProperty(value = "id") + @JsonPropertyDescription("Unique tool call identifier") + @Nullable + public abstract String getId(); + + @JsonProperty(required = true, value = "response") + @JsonPropertyDescription("Tool call response") + public abstract Object getResponse(); + + public static ToolCallResponsePart create(Object response) { + return new AutoValue_ToolCallResponsePart("tool_call_response", null, response); + } + + public static ToolCallResponsePart create(String id, Object response) { + return new AutoValue_ToolCallResponsePart("tool_call_response", id, response); + } +} diff --git a/instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/ChatCompletionMessagesConverter.java b/instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/ChatCompletionMessagesConverter.java new file mode 100644 index 000000000000..b763c9cf1fd9 --- /dev/null +++ b/instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/ChatCompletionMessagesConverter.java @@ -0,0 +1,416 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.openai.v1_1; + +import com.openai.models.chat.completions.ChatCompletion; +import com.openai.models.chat.completions.ChatCompletionAssistantMessageParam; +import com.openai.models.chat.completions.ChatCompletionContentPartText; +import com.openai.models.chat.completions.ChatCompletionCreateParams; +import com.openai.models.chat.completions.ChatCompletionDeveloperMessageParam; +import com.openai.models.chat.completions.ChatCompletionMessage; +import com.openai.models.chat.completions.ChatCompletionMessageParam; +import com.openai.models.chat.completions.ChatCompletionMessageToolCall; +import com.openai.models.chat.completions.ChatCompletionSystemMessageParam; +import com.openai.models.chat.completions.ChatCompletionToolMessageParam; +import com.openai.models.chat.completions.ChatCompletionUserMessageParam; +import io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages.InputMessage; +import io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages.InputMessages; +import io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages.MessagePart; +import io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages.OutputMessage; +import io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages.OutputMessages; +import io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages.Role; +import io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages.TextPart; +import io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages.ToolCallRequestPart; +import io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages.ToolCallResponsePart; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import javax.annotation.Nullable; + +// as a field of Attributes Extractor +// replace the 'ChatCompletionEventsHelper' +public final class ChatCompletionMessagesConverter { + + private final boolean captureMessageContent; + + public ChatCompletionMessagesConverter(boolean captureMessageContent) { + this.captureMessageContent = captureMessageContent; + } + + public InputMessages createInputMessages( + ChatCompletionCreateParams request) { + if (!captureMessageContent) { + return null; + } + + InputMessages inputMessages = InputMessages.create(); + for (ChatCompletionMessageParam msg : request.messages()) { + + if (msg.isSystem()) { + inputMessages.append(InputMessage.create(Role.SYSTEM, contentToMessageParts(msg.asSystem().content()))); + } else if (msg.isDeveloper()) { + inputMessages.append(InputMessage.create(Role.DEVELOPER, contentToMessageParts(msg.asDeveloper().content()))); + } else if (msg.isUser()) { + inputMessages.append(InputMessage.create(Role.USER, contentToMessageParts(msg.asUser().content()))); + } else if (msg.isAssistant()) { + ChatCompletionAssistantMessageParam assistantMsg = msg.asAssistant(); + List messageParts = new ArrayList<>(); + assistantMsg + .content() + .ifPresent(content -> messageParts.addAll(contentToMessageParts(content))); + + assistantMsg + .toolCalls().ifPresent(toolCalls -> { + messageParts.addAll(toolCalls.stream() + .map(ChatCompletionMessagesConverter::toolCallToMessagePart) + .filter(Objects::nonNull) + .collect(Collectors.toList())); + }); + inputMessages.append(InputMessage.create(Role.ASSISTANT, messageParts)); + } else if (msg.isTool()) { + ChatCompletionToolMessageParam toolMsg = msg.asTool(); + inputMessages.append(InputMessage.create(Role.TOOL, contentToMessageParts(toolMsg.toolCallId(), toolMsg.content()))); + } else { + continue; + } + } + return inputMessages; + } + + public OutputMessages createOutputMessages( + ChatCompletion completion) { + if (!captureMessageContent) { + return null; + } + + OutputMessages outputMessages = OutputMessages.create(); + for (ChatCompletion.Choice choice : completion.choices()) { + ChatCompletionMessage choiceMsg = choice.message(); + List messageParts = new ArrayList<>(); + + choiceMsg + .content() + .ifPresent( + content -> messageParts.add(TextPart.create(content))); + choiceMsg + .toolCalls() + .ifPresent( + toolCalls -> { + messageParts.addAll( + toolCalls.stream() + .map(ChatCompletionMessagesConverter::toolCallToMessagePart) + .filter(Objects::nonNull) + .collect(Collectors.toList())); + }); + + outputMessages.append( + OutputMessage.create( + Role.ASSISTANT, + messageParts, + choice.finishReason().asString())); + } + return outputMessages; + } + + private static List contentToMessageParts(String toolCallId, ChatCompletionToolMessageParam.Content content) { + if (content.isText()) { + return Collections.singletonList(ToolCallResponsePart.create(toolCallId, content.asText())); + } else if (content.isArrayOfContentParts()) { + return content.asArrayOfContentParts().stream() + .map(ChatCompletionContentPartText::text) + .map(response -> ToolCallResponsePart.create(toolCallId, response)) + .collect(Collectors.toList()); + } else { + return Collections.singletonList(ToolCallResponsePart.create("")); + } + } + + private static List contentToMessageParts(ChatCompletionAssistantMessageParam.Content content) { + if (content.isText()) { + return Collections.singletonList(TextPart.create(content.asText())); + } else if (content.isArrayOfContentParts()) { + return content.asArrayOfContentParts().stream() + .map( + part -> { + if (part.isText()) { + return part.asText().text(); + } + if (part.isRefusal()) { + return part.asRefusal().refusal(); + } + return null; + }) + .filter(Objects::nonNull) + .map(TextPart::create) + .collect(Collectors.toList()); + } else { + return Collections.singletonList(TextPart.create("")); + } + } + + private static List contentToMessageParts(ChatCompletionSystemMessageParam.Content content) { + if (content.isText()) { + return Collections.singletonList(TextPart.create(content.asText())); + } else if (content.isArrayOfContentParts()) { + return joinContentParts(content.asArrayOfContentParts()); + } else { + return Collections.singletonList(TextPart.create("")); + } + } + + private static List contentToMessageParts(ChatCompletionDeveloperMessageParam.Content content) { + if (content.isText()) { + return Collections.singletonList(TextPart.create(content.asText())); + } else if (content.isArrayOfContentParts()) { + return joinContentParts(content.asArrayOfContentParts()); + } else { + return Collections.singletonList(TextPart.create("")); + } + } + + private static List contentToMessageParts(ChatCompletionUserMessageParam.Content content) { + if (content.isText()) { + return Collections.singletonList(TextPart.create(content.asText())); + } else if (content.isArrayOfContentParts()) { + return content.asArrayOfContentParts().stream() + .map(part -> part.isText() ? part.asText().text() : null) + .filter(Objects::nonNull) + .map(TextPart::create) + .collect(Collectors.toList()); + } else { + return Collections.singletonList(TextPart.create("")); + } + } + + private static List joinContentParts(List contentParts) { + return contentParts.stream() + .map(ChatCompletionContentPartText::text) + .map(TextPart::create) + .collect(Collectors.toList()); + } + + private static MessagePart toolCallToMessagePart(ChatCompletionMessageToolCall call) { + FunctionAccess functionAccess = getFunctionAccess(call); + if (functionAccess != null) { + return ToolCallRequestPart.create(functionAccess.id(), functionAccess.name(), functionAccess.arguments()); + } + return null; + } + + @Nullable + private static FunctionAccess getFunctionAccess(ChatCompletionMessageToolCall call) { + if (V1FunctionAccess.isAvailable()) { + return V1FunctionAccess.create(call); + } + if (V3FunctionAccess.isAvailable()) { + return V3FunctionAccess.create(call); + } + + return null; + } + + private interface FunctionAccess { + String id(); + + String name(); + + String arguments(); + } + + private static String invokeStringHandle(@Nullable MethodHandle methodHandle, Object object) { + if (methodHandle == null) { + return ""; + } + + try { + return (String) methodHandle.invoke(object); + } catch (Throwable ignore) { + return ""; + } + } + + private static class V1FunctionAccess implements FunctionAccess { + @Nullable private static final MethodHandle idHandle; + @Nullable private static final MethodHandle functionHandle; + @Nullable private static final MethodHandle nameHandle; + @Nullable private static final MethodHandle argumentsHandle; + + static { + MethodHandle id; + MethodHandle function; + MethodHandle name; + MethodHandle arguments; + + try { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + id = + lookup.findVirtual( + ChatCompletionMessageToolCall.class, "id", MethodType.methodType(String.class)); + Class functionClass = + Class.forName( + "com.openai.models.chat.completions.ChatCompletionMessageToolCall$Function"); + function = + lookup.findVirtual( + ChatCompletionMessageToolCall.class, + "function", + MethodType.methodType(functionClass)); + name = lookup.findVirtual(functionClass, "name", MethodType.methodType(String.class)); + arguments = + lookup.findVirtual(functionClass, "arguments", MethodType.methodType(String.class)); + } catch (Exception exception) { + id = null; + function = null; + name = null; + arguments = null; + } + idHandle = id; + functionHandle = function; + nameHandle = name; + argumentsHandle = arguments; + } + + private final ChatCompletionMessageToolCall toolCall; + private final Object function; + + V1FunctionAccess(ChatCompletionMessageToolCall toolCall, Object function) { + this.toolCall = toolCall; + this.function = function; + } + + @Nullable + static FunctionAccess create(ChatCompletionMessageToolCall toolCall) { + if (functionHandle == null) { + return null; + } + + try { + return new V1FunctionAccess(toolCall, functionHandle.invoke(toolCall)); + } catch (Throwable ignore) { + return null; + } + } + + static boolean isAvailable() { + return idHandle != null; + } + + @Override + public String id() { + return invokeStringHandle(idHandle, toolCall); + } + + @Override + public String name() { + return invokeStringHandle(nameHandle, function); + } + + @Override + public String arguments() { + return invokeStringHandle(argumentsHandle, function); + } + } + + static class V3FunctionAccess implements FunctionAccess { + @Nullable private static final MethodHandle functionToolCallHandle; + @Nullable private static final MethodHandle idHandle; + @Nullable private static final MethodHandle functionHandle; + @Nullable private static final MethodHandle nameHandle; + @Nullable private static final MethodHandle argumentsHandle; + + static { + MethodHandle functionToolCall; + MethodHandle id; + MethodHandle function; + MethodHandle name; + MethodHandle arguments; + + try { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + functionToolCall = + lookup.findVirtual( + ChatCompletionMessageToolCall.class, + "function", + MethodType.methodType(Optional.class)); + Class functionToolCallClass = + Class.forName( + "com.openai.models.chat.completions.ChatCompletionMessageFunctionToolCall"); + id = lookup.findVirtual(functionToolCallClass, "id", MethodType.methodType(String.class)); + Class functionClass = + Class.forName( + "com.openai.models.chat.completions.ChatCompletionMessageFunctionToolCall$Function"); + function = + lookup.findVirtual( + functionToolCallClass, "function", MethodType.methodType(functionClass)); + name = lookup.findVirtual(functionClass, "name", MethodType.methodType(String.class)); + arguments = + lookup.findVirtual(functionClass, "arguments", MethodType.methodType(String.class)); + } catch (Exception exception) { + functionToolCall = null; + id = null; + function = null; + name = null; + arguments = null; + } + functionToolCallHandle = functionToolCall; + idHandle = id; + functionHandle = function; + nameHandle = name; + argumentsHandle = arguments; + } + + private final Object functionToolCall; + private final Object function; + + V3FunctionAccess(Object functionToolCall, Object function) { + this.functionToolCall = functionToolCall; + this.function = function; + } + + @Nullable + @SuppressWarnings("unchecked") + static FunctionAccess create(ChatCompletionMessageToolCall toolCall) { + if (functionToolCallHandle == null || functionHandle == null) { + return null; + } + + try { + Optional optional = (Optional) functionToolCallHandle.invoke(toolCall); + if (!optional.isPresent()) { + return null; + } + Object functionToolCall = optional.get(); + return new V3FunctionAccess(functionToolCall, functionHandle.invoke(functionToolCall)); + } catch (Throwable ignore) { + return null; + } + } + + static boolean isAvailable() { + return idHandle != null; + } + + @Override + public String id() { + return invokeStringHandle(idHandle, functionToolCall); + } + + @Override + public String name() { + return invokeStringHandle(nameHandle, function); + } + + @Override + public String arguments() { + return invokeStringHandle(argumentsHandle, function); + } + } +} From 9690b0a5da71f128e6591f34d567fec8bdada5f9 Mon Sep 17 00:00:00 2001 From: otelbot <197425009+otelbot@users.noreply.github.com> Date: Mon, 8 Sep 2025 14:22:10 +0000 Subject: [PATCH 2/2] ./gradlew spotlessApply --- .../semconv/genai/messages/GenericPart.java | 9 ++- .../semconv/genai/messages/InputMessage.java | 9 ++- .../semconv/genai/messages/InputMessages.java | 9 ++- .../semconv/genai/messages/MessagePart.java | 10 ++- .../semconv/genai/messages/OutputMessage.java | 5 ++ .../genai/messages/OutputMessages.java | 60 +++++++++------- .../semconv/genai/messages/Role.java | 5 ++ .../genai/messages/SystemInstructions.java | 9 ++- .../semconv/genai/messages/TextPart.java | 9 ++- .../genai/messages/ToolCallRequestPart.java | 9 ++- .../genai/messages/ToolCallResponsePart.java | 9 ++- .../v1_1/ChatCompletionMessagesConverter.java | 72 ++++++++++--------- 12 files changed, 134 insertions(+), 81 deletions(-) diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/GenericPart.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/GenericPart.java index 855a8cd1502b..1255dd5aac67 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/GenericPart.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/GenericPart.java @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages; import com.fasterxml.jackson.annotation.JsonClassDescription; @@ -7,8 +12,8 @@ import java.util.Map; /** - * Represents an arbitrary message part with any type and properties. - * This allows for extensibility with custom message part types. + * Represents an arbitrary message part with any type and properties. This allows for extensibility + * with custom message part types. */ @AutoValue @JsonClassDescription("Generic part") diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/InputMessage.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/InputMessage.java index 21269b59ffb7..4ed5c946f598 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/InputMessage.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/InputMessage.java @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages; import com.fasterxml.jackson.annotation.JsonClassDescription; @@ -6,9 +11,7 @@ import com.google.auto.value.AutoValue; import java.util.List; -/** - * Represents an input message sent to the model. - */ +/** Represents an input message sent to the model. */ @AutoValue @JsonClassDescription("Input message") public abstract class InputMessage { diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/InputMessages.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/InputMessages.java index a699748644c0..65391f1820af 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/InputMessages.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/InputMessages.java @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages; import com.fasterxml.jackson.core.JsonProcessingException; @@ -6,9 +11,7 @@ import java.util.ArrayList; import java.util.List; -/** - * Represents a collection of input messages sent to the model. - */ +/** Represents a collection of input messages sent to the model. */ @AutoValue public abstract class InputMessages { diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/MessagePart.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/MessagePart.java index 7b0dc6eaa048..07c325ce72e8 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/MessagePart.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/MessagePart.java @@ -1,12 +1,16 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages; -/** - * Interface for all message parts. - */ +/** Interface for all message parts. */ public interface MessagePart { /** * Get the type of this message part. + * * @return the type string */ String getType(); diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/OutputMessage.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/OutputMessage.java index f44b79df93bd..bd16a23b2d8f 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/OutputMessage.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/OutputMessage.java @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages; import com.fasterxml.jackson.annotation.JsonClassDescription; diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/OutputMessages.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/OutputMessages.java index 83e588a8a730..7de8e325c22b 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/OutputMessages.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/OutputMessages.java @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages; import com.fasterxml.jackson.core.JsonProcessingException; @@ -6,9 +11,7 @@ import java.util.ArrayList; import java.util.List; -/** - * Represents a collection of output messages from the model. - */ +/** Represents a collection of output messages from the model. */ @AutoValue public abstract class OutputMessages { @@ -38,30 +41,35 @@ public String toJsonString() { } /** - * Merges a chunk OutputMessage into the existing messages at the specified index. - * This method is used for streaming responses where content is received in chunks. - * + * Merges a chunk OutputMessage into the existing messages at the specified index. This method is + * used for streaming responses where content is received in chunks. + * * @param index the index of the message to merge into * @param chunkMessage the chunk message to append * @return a new OutputMessages instance with the merged content */ public OutputMessages merge(int index, OutputMessage chunkMessage) { List currentMessages = new ArrayList<>(getMessages()); - + if (index < 0 || index >= currentMessages.size()) { - throw new IllegalArgumentException("Index " + index + " is out of bounds for messages list of size " + currentMessages.size()); + throw new IllegalArgumentException( + "Index " + + index + + " is out of bounds for messages list of size " + + currentMessages.size()); } - + OutputMessage existingMessage = currentMessages.get(index); - + // Merge the parts by appending text content from chunk to existing message List mergedParts = new ArrayList<>(existingMessage.getParts()); - - // If the chunk message has text parts, append their content to the first text part of existing message + + // If the chunk message has text parts, append their content to the first text part of existing + // message for (MessagePart chunkPart : chunkMessage.getParts()) { if (chunkPart instanceof TextPart) { TextPart chunkTextPart = (TextPart) chunkPart; - + // Find the first text part in existing message to append to boolean appended = false; for (int i = 0; i < mergedParts.size(); i++) { @@ -69,13 +77,14 @@ public OutputMessages merge(int index, OutputMessage chunkMessage) { if (existingPart instanceof TextPart) { TextPart existingTextPart = (TextPart) existingPart; // Create a new TextPart with combined content - TextPart mergedTextPart = TextPart.create(existingTextPart.getContent() + chunkTextPart.getContent()); + TextPart mergedTextPart = + TextPart.create(existingTextPart.getContent() + chunkTextPart.getContent()); mergedParts.set(i, mergedTextPart); appended = true; break; } } - + // If no existing text part found, add the chunk as a new part if (!appended) { mergedParts.add(chunkTextPart); @@ -85,20 +94,19 @@ public OutputMessages merge(int index, OutputMessage chunkMessage) { mergedParts.add(chunkPart); } } - + // Create new OutputMessage with merged parts, using the chunk's finish reason if available - String finalFinishReason = chunkMessage.getFinishReason() != null ? - chunkMessage.getFinishReason() : existingMessage.getFinishReason(); - - OutputMessage mergedMessage = OutputMessage.create( - existingMessage.getRole(), - mergedParts, - finalFinishReason - ); - + String finalFinishReason = + chunkMessage.getFinishReason() != null + ? chunkMessage.getFinishReason() + : existingMessage.getFinishReason(); + + OutputMessage mergedMessage = + OutputMessage.create(existingMessage.getRole(), mergedParts, finalFinishReason); + // Replace the message at the specified index currentMessages.set(index, mergedMessage); - + return new AutoValue_OutputMessages(currentMessages); } } diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/Role.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/Role.java index 8938f4cf0966..e764fee197a6 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/Role.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/Role.java @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages; public enum Role { diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/SystemInstructions.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/SystemInstructions.java index c67eb1446274..6b4a655f9f95 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/SystemInstructions.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/SystemInstructions.java @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages; import com.fasterxml.jackson.annotation.JsonClassDescription; @@ -7,9 +12,7 @@ import com.google.auto.value.AutoValue; import java.util.List; -/** - * Represents the list of system instructions sent to the model. - */ +/** Represents the list of system instructions sent to the model. */ @AutoValue @JsonClassDescription("System instructions") public abstract class SystemInstructions { diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/TextPart.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/TextPart.java index c40c9c1f53aa..a899fbf10a5a 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/TextPart.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/TextPart.java @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages; import com.fasterxml.jackson.annotation.JsonClassDescription; @@ -5,9 +10,7 @@ import com.fasterxml.jackson.annotation.JsonPropertyDescription; import com.google.auto.value.AutoValue; -/** - * Represents text content sent to or received from the model. - */ +/** Represents text content sent to or received from the model. */ @AutoValue @JsonClassDescription("Text part") public abstract class TextPart implements MessagePart { diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/ToolCallRequestPart.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/ToolCallRequestPart.java index 613b537eab16..09b21fdc3b98 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/ToolCallRequestPart.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/ToolCallRequestPart.java @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages; import com.fasterxml.jackson.annotation.JsonClassDescription; @@ -6,9 +11,7 @@ import com.google.auto.value.AutoValue; import javax.annotation.Nullable; -/** - * Represents a tool call requested by the model. - */ +/** Represents a tool call requested by the model. */ @AutoValue @JsonClassDescription("Tool call request part") public abstract class ToolCallRequestPart implements MessagePart { diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/ToolCallResponsePart.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/ToolCallResponsePart.java index dd7940e1e299..91acad625eec 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/ToolCallResponsePart.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/messages/ToolCallResponsePart.java @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages; import com.fasterxml.jackson.annotation.JsonClassDescription; @@ -6,9 +11,7 @@ import com.google.auto.value.AutoValue; import javax.annotation.Nullable; -/** - * Represents a tool call result sent to the model or a built-in tool call outcome and details. - */ +/** Represents a tool call result sent to the model or a built-in tool call outcome and details. */ @AutoValue @JsonClassDescription("Tool call response part") public abstract class ToolCallResponsePart implements MessagePart { diff --git a/instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/ChatCompletionMessagesConverter.java b/instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/ChatCompletionMessagesConverter.java index b763c9cf1fd9..c01ad781fb2a 100644 --- a/instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/ChatCompletionMessagesConverter.java +++ b/instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/ChatCompletionMessagesConverter.java @@ -46,8 +46,7 @@ public ChatCompletionMessagesConverter(boolean captureMessageContent) { this.captureMessageContent = captureMessageContent; } - public InputMessages createInputMessages( - ChatCompletionCreateParams request) { + public InputMessages createInputMessages(ChatCompletionCreateParams request) { if (!captureMessageContent) { return null; } @@ -56,11 +55,15 @@ public InputMessages createInputMessages( for (ChatCompletionMessageParam msg : request.messages()) { if (msg.isSystem()) { - inputMessages.append(InputMessage.create(Role.SYSTEM, contentToMessageParts(msg.asSystem().content()))); + inputMessages.append( + InputMessage.create(Role.SYSTEM, contentToMessageParts(msg.asSystem().content()))); } else if (msg.isDeveloper()) { - inputMessages.append(InputMessage.create(Role.DEVELOPER, contentToMessageParts(msg.asDeveloper().content()))); + inputMessages.append( + InputMessage.create( + Role.DEVELOPER, contentToMessageParts(msg.asDeveloper().content()))); } else if (msg.isUser()) { - inputMessages.append(InputMessage.create(Role.USER, contentToMessageParts(msg.asUser().content()))); + inputMessages.append( + InputMessage.create(Role.USER, contentToMessageParts(msg.asUser().content()))); } else if (msg.isAssistant()) { ChatCompletionAssistantMessageParam assistantMsg = msg.asAssistant(); List messageParts = new ArrayList<>(); @@ -69,16 +72,21 @@ public InputMessages createInputMessages( .ifPresent(content -> messageParts.addAll(contentToMessageParts(content))); assistantMsg - .toolCalls().ifPresent(toolCalls -> { - messageParts.addAll(toolCalls.stream() - .map(ChatCompletionMessagesConverter::toolCallToMessagePart) - .filter(Objects::nonNull) - .collect(Collectors.toList())); - }); + .toolCalls() + .ifPresent( + toolCalls -> { + messageParts.addAll( + toolCalls.stream() + .map(ChatCompletionMessagesConverter::toolCallToMessagePart) + .filter(Objects::nonNull) + .collect(Collectors.toList())); + }); inputMessages.append(InputMessage.create(Role.ASSISTANT, messageParts)); } else if (msg.isTool()) { ChatCompletionToolMessageParam toolMsg = msg.asTool(); - inputMessages.append(InputMessage.create(Role.TOOL, contentToMessageParts(toolMsg.toolCallId(), toolMsg.content()))); + inputMessages.append( + InputMessage.create( + Role.TOOL, contentToMessageParts(toolMsg.toolCallId(), toolMsg.content()))); } else { continue; } @@ -86,8 +94,7 @@ public InputMessages createInputMessages( return inputMessages; } - public OutputMessages createOutputMessages( - ChatCompletion completion) { + public OutputMessages createOutputMessages(ChatCompletion completion) { if (!captureMessageContent) { return null; } @@ -97,31 +104,26 @@ public OutputMessages createOutputMessages( ChatCompletionMessage choiceMsg = choice.message(); List messageParts = new ArrayList<>(); - choiceMsg - .content() - .ifPresent( - content -> messageParts.add(TextPart.create(content))); + choiceMsg.content().ifPresent(content -> messageParts.add(TextPart.create(content))); choiceMsg .toolCalls() .ifPresent( toolCalls -> { messageParts.addAll( toolCalls.stream() - .map(ChatCompletionMessagesConverter::toolCallToMessagePart) - .filter(Objects::nonNull) - .collect(Collectors.toList())); + .map(ChatCompletionMessagesConverter::toolCallToMessagePart) + .filter(Objects::nonNull) + .collect(Collectors.toList())); }); outputMessages.append( - OutputMessage.create( - Role.ASSISTANT, - messageParts, - choice.finishReason().asString())); + OutputMessage.create(Role.ASSISTANT, messageParts, choice.finishReason().asString())); } return outputMessages; } - private static List contentToMessageParts(String toolCallId, ChatCompletionToolMessageParam.Content content) { + private static List contentToMessageParts( + String toolCallId, ChatCompletionToolMessageParam.Content content) { if (content.isText()) { return Collections.singletonList(ToolCallResponsePart.create(toolCallId, content.asText())); } else if (content.isArrayOfContentParts()) { @@ -134,7 +136,8 @@ private static List contentToMessageParts(String toolCallId, ChatCo } } - private static List contentToMessageParts(ChatCompletionAssistantMessageParam.Content content) { + private static List contentToMessageParts( + ChatCompletionAssistantMessageParam.Content content) { if (content.isText()) { return Collections.singletonList(TextPart.create(content.asText())); } else if (content.isArrayOfContentParts()) { @@ -157,7 +160,8 @@ private static List contentToMessageParts(ChatCompletionAssistantMe } } - private static List contentToMessageParts(ChatCompletionSystemMessageParam.Content content) { + private static List contentToMessageParts( + ChatCompletionSystemMessageParam.Content content) { if (content.isText()) { return Collections.singletonList(TextPart.create(content.asText())); } else if (content.isArrayOfContentParts()) { @@ -167,7 +171,8 @@ private static List contentToMessageParts(ChatCompletionSystemMessa } } - private static List contentToMessageParts(ChatCompletionDeveloperMessageParam.Content content) { + private static List contentToMessageParts( + ChatCompletionDeveloperMessageParam.Content content) { if (content.isText()) { return Collections.singletonList(TextPart.create(content.asText())); } else if (content.isArrayOfContentParts()) { @@ -177,7 +182,8 @@ private static List contentToMessageParts(ChatCompletionDeveloperMe } } - private static List contentToMessageParts(ChatCompletionUserMessageParam.Content content) { + private static List contentToMessageParts( + ChatCompletionUserMessageParam.Content content) { if (content.isText()) { return Collections.singletonList(TextPart.create(content.asText())); } else if (content.isArrayOfContentParts()) { @@ -191,7 +197,8 @@ private static List contentToMessageParts(ChatCompletionUserMessage } } - private static List joinContentParts(List contentParts) { + private static List joinContentParts( + List contentParts) { return contentParts.stream() .map(ChatCompletionContentPartText::text) .map(TextPart::create) @@ -201,7 +208,8 @@ private static List joinContentParts(List