diff --git a/docs/supported-libraries.md b/docs/supported-libraries.md index 8f514258e75e..cc87ac34c04e 100644 --- a/docs/supported-libraries.md +++ b/docs/supported-libraries.md @@ -106,7 +106,7 @@ These are the supported libraries and frameworks: | [MongoDB Driver](https://mongodb.github.io/mongo-java-driver/) | 3.1+ | [opentelemetry-mongo-3.1](../instrumentation/mongo/mongo-3.1/library) | [Database Client Spans], [Database Client Metrics] [6] | | [MyBatis](https://mybatis.org/mybatis-3/) | 3.2+ | N/A | none | | [Netty HTTP codec [5]](https://github.com/netty/netty) | 3.8+ | [opentelemetry-netty-4.1](../instrumentation/netty/netty-4.1/library) | [HTTP Client Spans], [HTTP Client Metrics], [HTTP Server Spans], [HTTP Server Metrics] | -| [OpenAI Java SDK](https://github.com/openai/openai-java) | 1.1+ (not including 3.0+ yet) | [openai-java-1.1](../instrumentation/openai/openai-java-1.1/library) | [GenAI Client Spans], [GenAI Client Metrics] | +| [OpenAI Java SDK](https://github.com/openai/openai-java) | 1.1+ | [openai-java-1.1](../instrumentation/openai/openai-java-1.1/library) | [GenAI Client Spans], [GenAI Client Metrics] | | [OpenSearch Rest Client](https://github.com/opensearch-project/opensearch-java) | 1.0+ | | [Database Client Spans], [Database Client Metrics] [6] | | [OkHttp](https://github.com/square/okhttp/) | 2.2+ | [opentelemetry-okhttp-3.0](../instrumentation/okhttp/okhttp-3.0/library) | [HTTP Client Spans], [HTTP Client Metrics] | | [Oracle UCP](https://docs.oracle.com/database/121/JJUCP/) | 11.2+ | [opentelemetry-oracle-ucp-11.2](../instrumentation/oracle-ucp-11.2/library) | [Database Pool Metrics] | diff --git a/instrumentation/openai/openai-java-1.1/javaagent/build.gradle.kts b/instrumentation/openai/openai-java-1.1/javaagent/build.gradle.kts index 0a1560c2d74f..0240568ef8c6 100644 --- a/instrumentation/openai/openai-java-1.1/javaagent/build.gradle.kts +++ b/instrumentation/openai/openai-java-1.1/javaagent/build.gradle.kts @@ -19,8 +19,6 @@ dependencies { library("com.openai:openai-java:1.1.0") testImplementation(project(":instrumentation:openai:openai-java-1.1:testing")) - - latestDepTestLibrary("com.openai:openai-java:2.+") // documented limitation } tasks { diff --git a/instrumentation/openai/openai-java-1.1/library/README.md b/instrumentation/openai/openai-java-1.1/library/README.md index 1a90fba0fd3e..4451ec510262 100644 --- a/instrumentation/openai/openai-java-1.1/library/README.md +++ b/instrumentation/openai/openai-java-1.1/library/README.md @@ -1,7 +1,6 @@ # Library Instrumentation for OpenAI Java SDK version 1.1.0 and higher Provides OpenTelemetry instrumentation for [openai-java](https://github.com/openai/openai-java/). -Versions 1.1 through 2.x are supported. ## Quickstart diff --git a/instrumentation/openai/openai-java-1.1/library/build.gradle.kts b/instrumentation/openai/openai-java-1.1/library/build.gradle.kts index 9df052f414af..27d8b0c24c66 100644 --- a/instrumentation/openai/openai-java-1.1/library/build.gradle.kts +++ b/instrumentation/openai/openai-java-1.1/library/build.gradle.kts @@ -7,8 +7,6 @@ dependencies { library("com.openai:openai-java:1.1.0") testImplementation(project(":instrumentation:openai:openai-java-1.1:testing")) - - latestDepTestLibrary("com.openai:openai-java:2.+") // documented limitation } tasks { diff --git a/instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/ChatCompletionEventsHelper.java b/instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/ChatCompletionEventsHelper.java index 219bd86b4e6d..66d337bdfbab 100644 --- a/instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/ChatCompletionEventsHelper.java +++ b/instrumentation/openai/openai-java-1.1/library/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/ChatCompletionEventsHelper.java @@ -24,11 +24,16 @@ import io.opentelemetry.api.logs.LogRecordBuilder; import io.opentelemetry.api.logs.Logger; import io.opentelemetry.context.Context; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; +import javax.annotation.Nullable; final class ChatCompletionEventsHelper { @@ -215,21 +220,232 @@ private static LogRecordBuilder newEvent(Logger eventLogger, String name) { private static Value buildToolCallEventObject( ChatCompletionMessageToolCall call, boolean captureMessageContent) { Map> result = new HashMap<>(); - result.put("id", Value.of(call.id())); - result.put("type", Value.of("function")); // "function" is the only currently supported type - result.put("function", buildFunctionEventObject(call.function(), captureMessageContent)); + FunctionAccess functionAccess = getFunctionAccess(call); + if (functionAccess != null) { + result.put("id", Value.of(functionAccess.id())); + result.put("type", Value.of("function")); // "function" is the only currently supported type + result.put("function", buildFunctionEventObject(functionAccess, captureMessageContent)); + } return Value.of(result); } private static Value buildFunctionEventObject( - ChatCompletionMessageToolCall.Function function, boolean captureMessageContent) { + FunctionAccess functionAccess, boolean captureMessageContent) { Map> result = new HashMap<>(); - result.put("name", Value.of(function.name())); + result.put("name", Value.of(functionAccess.name())); if (captureMessageContent) { - result.put("arguments", Value.of(function.arguments())); + result.put("arguments", Value.of(functionAccess.arguments())); } return Value.of(result); } + @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); + } + } + private ChatCompletionEventsHelper() {} } diff --git a/instrumentation/openai/openai-java-1.1/library/src/test/java/io/opentelemetry/instrumentation/openai/v1_1/ChatTest.java b/instrumentation/openai/openai-java-1.1/library/src/test/java/io/opentelemetry/instrumentation/openai/v1_1/ChatTest.java index 1937c11f75d7..2ae72f19ba33 100644 --- a/instrumentation/openai/openai-java-1.1/library/src/test/java/io/opentelemetry/instrumentation/openai/v1_1/ChatTest.java +++ b/instrumentation/openai/openai-java-1.1/library/src/test/java/io/opentelemetry/instrumentation/openai/v1_1/ChatTest.java @@ -20,7 +20,6 @@ import static io.opentelemetry.semconv.incubating.GenAiIncubatingAttributes.GenAiSystemIncubatingValues.OPENAI; import static io.opentelemetry.semconv.incubating.GenAiIncubatingAttributes.GenAiTokenTypeIncubatingValues.COMPLETION; import static io.opentelemetry.semconv.incubating.GenAiIncubatingAttributes.GenAiTokenTypeIncubatingValues.INPUT; -import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; @@ -318,14 +317,14 @@ void toolCallsNoCaptureContent() { assertThat(toolCalls).hasSize(2); String newYorkCallId = toolCalls.stream() - .filter(call -> call.function().arguments().contains("New York")) - .map(ChatCompletionMessageToolCall::id) + .filter(call -> testHelper.arguments(call).contains("New York")) + .map(call -> testHelper.id(call)) .findFirst() .get(); String londonCallId = toolCalls.stream() - .filter(call -> call.function().arguments().contains("London")) - .map(ChatCompletionMessageToolCall::id) + .filter(call -> testHelper.arguments(call).contains("London")) + .map(call -> testHelper.id(call)) .findFirst() .get(); @@ -774,63 +773,18 @@ void streamToolCallsNoCaptureContent() { List chunks = doCompletionsStreaming(params, clientNoCaptureContent(), clientAsyncNoCaptureContent()); - List toolCalls = new ArrayList<>(); - - ChatCompletionMessageToolCall.Builder currentToolCall = null; - ChatCompletionMessageToolCall.Function.Builder currentFunction = null; - StringBuilder currentArgs = null; - - for (ChatCompletionChunk chunk : chunks) { - List calls = - chunk.choices().get(0).delta().toolCalls().orElse(emptyList()); - if (calls.isEmpty()) { - continue; - } - for (ChatCompletionChunk.Choice.Delta.ToolCall call : calls) { - if (call.id().isPresent()) { - if (currentToolCall != null) { - if (currentFunction != null && currentArgs != null) { - currentFunction.arguments(currentArgs.toString()); - currentToolCall.function(currentFunction.build()); - } - toolCalls.add(currentToolCall.build()); - } - currentToolCall = ChatCompletionMessageToolCall.builder().id(call.id().get()); - currentFunction = ChatCompletionMessageToolCall.Function.builder(); - currentArgs = new StringBuilder(); - } - if (call.function().isPresent()) { - if (call.function().get().name().isPresent()) { - if (currentFunction != null) { - currentFunction.name(call.function().get().name().get()); - } - } - if (call.function().get().arguments().isPresent()) { - if (currentArgs != null) { - currentArgs.append(call.function().get().arguments().get()); - } - } - } - } - } - if (currentToolCall != null) { - if (currentFunction != null && currentArgs != null) { - currentFunction.arguments(currentArgs.toString()); - currentToolCall.function(currentFunction.build()); - } - toolCalls.add(currentToolCall.build()); - } + List toolCalls = getToolCalls(chunks); String newYorkCallId = toolCalls.stream() - .filter(call -> call.function().arguments().contains("New York")) - .map(ChatCompletionMessageToolCall::id) + .filter(call -> testHelper.arguments(call).contains("New York")) + .map(call -> testHelper.id(call)) .findFirst() .get(); String londonCallId = toolCalls.stream() - .filter(call -> call.function().arguments().contains("London")) - .map(ChatCompletionMessageToolCall::id) + .filter(call -> testHelper.arguments(call).contains("London")) + .map(call -> testHelper.id(call)) .findFirst() .get(); diff --git a/instrumentation/openai/openai-java-1.1/openai3-testing/build.gradle.kts b/instrumentation/openai/openai-java-1.1/openai3-testing/build.gradle.kts new file mode 100644 index 000000000000..a78fafddd76b --- /dev/null +++ b/instrumentation/openai/openai-java-1.1/openai3-testing/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + id("otel.java-conventions") +} + +dependencies { + api(project(":testing-common")) + compileOnly("com.openai:openai-java:3.0.0") +} diff --git a/instrumentation/openai/openai-java-1.1/openai3-testing/src/main/java/io/opentelemetry/instrumentation/openai/TestHelper.java b/instrumentation/openai/openai-java-1.1/openai3-testing/src/main/java/io/opentelemetry/instrumentation/openai/TestHelper.java new file mode 100644 index 000000000000..c8a06edcdc37 --- /dev/null +++ b/instrumentation/openai/openai-java-1.1/openai3-testing/src/main/java/io/opentelemetry/instrumentation/openai/TestHelper.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.openai; + +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.openai.models.FunctionDefinition; +import com.openai.models.chat.completions.ChatCompletionMessageToolCall; +import com.openai.models.chat.completions.ChatCompletionTool; + +public interface TestHelper { + + String id(ChatCompletionMessageToolCall toolCall); + + String arguments(ChatCompletionMessageToolCall toolCall); + + ChatCompletionTool chatCompletionTool(FunctionDefinition functionDefinition); + + MessageToolCallBuilder messageToolCallBuilder(); + + MessageToolCallBuilder.FunctionBuilder messageToolCallFunctionBuilder(); + + interface MessageToolCallBuilder { + @CanIgnoreReturnValue + MessageToolCallBuilder id(String id); + + @CanIgnoreReturnValue + MessageToolCallBuilder function(FunctionBuilder functionBuilder); + + ChatCompletionMessageToolCall build(); + + interface FunctionBuilder { + @CanIgnoreReturnValue + FunctionBuilder name(String name); + + @CanIgnoreReturnValue + FunctionBuilder arguments(String arguments); + } + } +} diff --git a/instrumentation/openai/openai-java-1.1/openai3-testing/src/main/java/io/opentelemetry/instrumentation/openai/v3_0/OpenAi3TestHelper.java b/instrumentation/openai/openai-java-1.1/openai3-testing/src/main/java/io/opentelemetry/instrumentation/openai/v3_0/OpenAi3TestHelper.java new file mode 100644 index 000000000000..94f9098e645f --- /dev/null +++ b/instrumentation/openai/openai-java-1.1/openai3-testing/src/main/java/io/opentelemetry/instrumentation/openai/v3_0/OpenAi3TestHelper.java @@ -0,0 +1,79 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.openai.v3_0; + +import com.openai.models.FunctionDefinition; +import com.openai.models.chat.completions.ChatCompletionFunctionTool; +import com.openai.models.chat.completions.ChatCompletionMessageFunctionToolCall; +import com.openai.models.chat.completions.ChatCompletionMessageToolCall; +import com.openai.models.chat.completions.ChatCompletionTool; +import io.opentelemetry.instrumentation.openai.TestHelper; +import io.opentelemetry.instrumentation.openai.TestHelper.MessageToolCallBuilder.FunctionBuilder; + +public class OpenAi3TestHelper implements TestHelper { + @Override + public String id(ChatCompletionMessageToolCall toolCall) { + return toolCall.function().get().id(); + } + + @Override + public String arguments(ChatCompletionMessageToolCall toolCall) { + return toolCall.function().get().function().arguments(); + } + + @Override + public ChatCompletionTool chatCompletionTool(FunctionDefinition functionDefinition) { + return ChatCompletionTool.ofFunction( + ChatCompletionFunctionTool.builder().function(functionDefinition).build()); + } + + @Override + public MessageToolCallBuilder messageToolCallBuilder() { + return new MessageToolCallBuilder() { + final ChatCompletionMessageFunctionToolCall.Builder builder = + ChatCompletionMessageFunctionToolCall.builder(); + + @Override + public MessageToolCallBuilder id(String id) { + builder.id(id); + return this; + } + + @Override + public MessageToolCallBuilder function(FunctionBuilder functionBuilder) { + builder.function(((FunctionBuilderImpl) functionBuilder).builder.build()); + return null; + } + + @Override + public ChatCompletionMessageToolCall build() { + return ChatCompletionMessageToolCall.ofFunction(builder.build()); + } + }; + } + + @Override + public FunctionBuilder messageToolCallFunctionBuilder() { + return new FunctionBuilderImpl(); + } + + private static class FunctionBuilderImpl implements FunctionBuilder { + final ChatCompletionMessageFunctionToolCall.Function.Builder builder = + ChatCompletionMessageFunctionToolCall.Function.builder(); + + @Override + public FunctionBuilder name(String name) { + builder.name(name); + return this; + } + + @Override + public FunctionBuilder arguments(String arguments) { + builder.arguments(arguments); + return this; + } + } +} diff --git a/instrumentation/openai/openai-java-1.1/testing/build.gradle.kts b/instrumentation/openai/openai-java-1.1/testing/build.gradle.kts index 033a85e43070..7d66b7f229d7 100644 --- a/instrumentation/openai/openai-java-1.1/testing/build.gradle.kts +++ b/instrumentation/openai/openai-java-1.1/testing/build.gradle.kts @@ -4,6 +4,6 @@ plugins { dependencies { api(project(":testing-common")) - api("com.openai:openai-java:1.1.0") + api(project(":instrumentation:openai:openai-java-1.1:openai3-testing")) } diff --git a/instrumentation/openai/openai-java-1.1/testing/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/AbstractChatTest.java b/instrumentation/openai/openai-java-1.1/testing/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/AbstractChatTest.java index a3e8dc56ff9b..12369e4ccee2 100644 --- a/instrumentation/openai/openai-java-1.1/testing/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/AbstractChatTest.java +++ b/instrumentation/openai/openai-java-1.1/testing/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/AbstractChatTest.java @@ -62,6 +62,7 @@ import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.openai.TestHelper; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -545,14 +546,14 @@ void toolCalls() { assertThat(toolCalls).hasSize(2); String newYorkCallId = toolCalls.stream() - .filter(call -> call.function().arguments().contains("New York")) - .map(ChatCompletionMessageToolCall::id) + .filter(call -> testHelper.arguments(call).contains("New York")) + .map(call -> testHelper.id(call)) .findFirst() .get(); String londonCallId = toolCalls.stream() - .filter(call -> call.function().arguments().contains("London")) - .map(ChatCompletionMessageToolCall::id) + .filter(call -> testHelper.arguments(call).contains("London")) + .map(call -> testHelper.id(call)) .findFirst() .get(); @@ -1217,25 +1218,11 @@ void streamMultipleChoices() { "message", Value.of(KeyValue.of("content", Value.of(content2))))))); } - @Test - void streamToolCalls() { - List chatMessages = new ArrayList<>(); - chatMessages.add(createSystemMessage("You are a helpful assistant providing weather updates.")); - chatMessages.add(createUserMessage("What is the weather in New York City and London?")); - - ChatCompletionCreateParams params = - ChatCompletionCreateParams.builder() - .messages(chatMessages) - .model(TEST_CHAT_MODEL) - .addTool(buildGetWeatherToolDefinition()) - .build(); - - List chunks = doCompletionsStreaming(params); - + protected List getToolCalls(List chunks) { List toolCalls = new ArrayList<>(); - ChatCompletionMessageToolCall.Builder currentToolCall = null; - ChatCompletionMessageToolCall.Function.Builder currentFunction = null; + TestHelper.MessageToolCallBuilder currentToolCall = null; + TestHelper.MessageToolCallBuilder.FunctionBuilder currentFunction = null; StringBuilder currentArgs = null; for (ChatCompletionChunk chunk : chunks) { @@ -1247,40 +1234,68 @@ void streamToolCalls() { for (ChatCompletionChunk.Choice.Delta.ToolCall call : calls) { if (call.id().isPresent()) { if (currentToolCall != null) { - currentFunction.arguments(currentArgs.toString()); - currentToolCall.function(currentFunction.build()); + if (currentFunction != null && currentArgs != null) { + currentFunction.arguments(currentArgs.toString()); + currentToolCall.function(currentFunction); + } toolCalls.add(currentToolCall.build()); } - currentToolCall = ChatCompletionMessageToolCall.builder().id(call.id().get()); - currentFunction = ChatCompletionMessageToolCall.Function.builder(); + currentToolCall = testHelper.messageToolCallBuilder().id(call.id().get()); + currentFunction = testHelper.messageToolCallFunctionBuilder(); currentArgs = new StringBuilder(); } if (call.function().isPresent()) { if (call.function().get().name().isPresent()) { - currentFunction.name(call.function().get().name().get()); + if (currentFunction != null) { + currentFunction.name(call.function().get().name().get()); + } } if (call.function().get().arguments().isPresent()) { - currentArgs.append(call.function().get().arguments().get()); + if (currentArgs != null) { + currentArgs.append(call.function().get().arguments().get()); + } } } } } if (currentToolCall != null) { - currentFunction.arguments(currentArgs.toString()); - currentToolCall.function(currentFunction.build()); + if (currentFunction != null && currentArgs != null) { + currentFunction.arguments(currentArgs.toString()); + currentToolCall.function(currentFunction); + } toolCalls.add(currentToolCall.build()); } + return toolCalls; + } + + @Test + void streamToolCalls() { + List chatMessages = new ArrayList<>(); + chatMessages.add(createSystemMessage("You are a helpful assistant providing weather updates.")); + chatMessages.add(createUserMessage("What is the weather in New York City and London?")); + + ChatCompletionCreateParams params = + ChatCompletionCreateParams.builder() + .messages(chatMessages) + .model(TEST_CHAT_MODEL) + .addTool(buildGetWeatherToolDefinition()) + .build(); + + List chunks = doCompletionsStreaming(params); + + List toolCalls = getToolCalls(chunks); + String newYorkCallId = toolCalls.stream() - .filter(call -> call.function().arguments().contains("New York")) - .map(ChatCompletionMessageToolCall::id) + .filter(call -> testHelper.arguments(call).contains("New York")) + .map(call -> testHelper.id(call)) .findFirst() .get(); String londonCallId = toolCalls.stream() - .filter(call -> call.function().arguments().contains("London")) - .map(ChatCompletionMessageToolCall::id) + .filter(call -> testHelper.arguments(call).contains("London")) + .map(call -> testHelper.id(call)) .findFirst() .get(); @@ -1637,20 +1652,18 @@ protected static ChatCompletionTool buildGetWeatherToolDefinition() { Map properties = new HashMap<>(); properties.put("location", JsonObject.of(location)); - return ChatCompletionTool.builder() - .function( - FunctionDefinition.builder() - .name("get_weather") - .parameters( - FunctionParameters.builder() - .putAdditionalProperty("type", JsonValue.from("object")) - .putAdditionalProperty( - "required", JsonValue.from(Collections.singletonList("location"))) - .putAdditionalProperty("additionalProperties", JsonValue.from(false)) - .putAdditionalProperty("properties", JsonObject.of(properties)) - .build()) - .build()) - .build(); + return testHelper.chatCompletionTool( + FunctionDefinition.builder() + .name("get_weather") + .parameters( + FunctionParameters.builder() + .putAdditionalProperty("type", JsonValue.from("object")) + .putAdditionalProperty( + "required", JsonValue.from(Collections.singletonList("location"))) + .putAdditionalProperty("additionalProperties", JsonValue.from(false)) + .putAdditionalProperty("properties", JsonObject.of(properties)) + .build()) + .build()); } protected static ChatCompletionMessageParam createToolMessage(String response, String id) { diff --git a/instrumentation/openai/openai-java-1.1/testing/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/AbstractOpenAiTest.java b/instrumentation/openai/openai-java-1.1/testing/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/AbstractOpenAiTest.java index 9fd4123971a2..87d30bda68b4 100644 --- a/instrumentation/openai/openai-java-1.1/testing/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/AbstractOpenAiTest.java +++ b/instrumentation/openai/openai-java-1.1/testing/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/AbstractOpenAiTest.java @@ -9,6 +9,8 @@ import com.openai.client.OpenAIClientAsync; import com.openai.client.okhttp.OpenAIOkHttpClient; import com.openai.client.okhttp.OpenAIOkHttpClientAsync; +import io.opentelemetry.instrumentation.openai.TestHelper; +import io.opentelemetry.instrumentation.openai.v3_0.OpenAi3TestHelper; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.recording.RecordingExtension; import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; @@ -35,6 +37,9 @@ enum TestType { @RegisterExtension static final RecordingExtension recording = new RecordingExtension(API_URL); + protected static TestHelper testHelper = + Boolean.getBoolean("testLatestDeps") ? new OpenAi3TestHelper() : new OpenAi1TestHelper(); + protected abstract InstrumentationExtension getTesting(); protected abstract OpenAIClient wrap(OpenAIClient client); diff --git a/instrumentation/openai/openai-java-1.1/testing/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/OpenAi1TestHelper.java b/instrumentation/openai/openai-java-1.1/testing/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/OpenAi1TestHelper.java new file mode 100644 index 000000000000..5ade4a6c8e89 --- /dev/null +++ b/instrumentation/openai/openai-java-1.1/testing/src/main/java/io/opentelemetry/instrumentation/openai/v1_1/OpenAi1TestHelper.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.openai.v1_1; + +import com.openai.models.FunctionDefinition; +import com.openai.models.chat.completions.ChatCompletionMessageToolCall; +import com.openai.models.chat.completions.ChatCompletionTool; +import io.opentelemetry.instrumentation.openai.TestHelper; +import io.opentelemetry.instrumentation.openai.TestHelper.MessageToolCallBuilder.FunctionBuilder; + +public class OpenAi1TestHelper implements TestHelper { + @Override + public String id(ChatCompletionMessageToolCall toolCall) { + return toolCall.id(); + } + + @Override + public String arguments(ChatCompletionMessageToolCall toolCall) { + return toolCall.function().arguments(); + } + + @Override + public ChatCompletionTool chatCompletionTool(FunctionDefinition functionDefinition) { + return ChatCompletionTool.builder().function(functionDefinition).build(); + } + + @Override + public MessageToolCallBuilder messageToolCallBuilder() { + return new MessageToolCallBuilder() { + final ChatCompletionMessageToolCall.Builder builder = ChatCompletionMessageToolCall.builder(); + + @Override + public MessageToolCallBuilder id(String id) { + builder.id(id); + return this; + } + + @Override + public MessageToolCallBuilder function(FunctionBuilder functionBuilder) { + builder.function(((FunctionBuilderImpl) functionBuilder).builder.build()); + return null; + } + + @Override + public ChatCompletionMessageToolCall build() { + return builder.build(); + } + }; + } + + @Override + public FunctionBuilder messageToolCallFunctionBuilder() { + return new FunctionBuilderImpl(); + } + + private static class FunctionBuilderImpl implements FunctionBuilder { + final ChatCompletionMessageToolCall.Function.Builder builder = + ChatCompletionMessageToolCall.Function.builder(); + + @Override + public FunctionBuilder name(String name) { + builder.name(name); + return this; + } + + @Override + public FunctionBuilder arguments(String arguments) { + builder.arguments(arguments); + return this; + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index c1f7eba32046..ea6ccc60a6f7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -425,6 +425,7 @@ include(":instrumentation:okhttp:okhttp-3.0:testing") include(":instrumentation:openai:openai-java-1.1:javaagent") include(":instrumentation:openai:openai-java-1.1:library") include(":instrumentation:openai:openai-java-1.1:testing") +include(":instrumentation:openai:openai-java-1.1:openai3-testing") include(":instrumentation:opencensus-shim:testing") include(":instrumentation:opensearch:opensearch-rest-1.0:javaagent") include(":instrumentation:opensearch:opensearch-rest-3.0:javaagent")