diff --git a/CHANGELOG.next-release.md b/CHANGELOG.next-release.md index 8b137891..34076562 100644 --- a/CHANGELOG.next-release.md +++ b/CHANGELOG.next-release.md @@ -1 +1 @@ - +* Add support for OpenAI client 0.14+ - #531 diff --git a/custom/build.gradle.kts b/custom/build.gradle.kts index e0eb65d6..688e8233 100644 --- a/custom/build.gradle.kts +++ b/custom/build.gradle.kts @@ -3,7 +3,8 @@ plugins { } val instrumentations = listOf( - ":instrumentation:openai-client-instrumentation" + ":instrumentation:openai-client-instrumentation:instrumentation-0.2", + ":instrumentation:openai-client-instrumentation:instrumentation-0.14" ) dependencies { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5d151235..58072603 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -77,7 +77,7 @@ ant = "org.apache.ant:ant:1.10.15" asm = "org.ow2.asm:asm:9.7" # Instrumented libraries -openaiClient = "com.openai:openai-java:0.13.0" +openaiClient = "com.openai:openai-java:0.21.0" [bundles] diff --git a/instrumentation/openai-client-instrumentation/common/build.gradle.kts b/instrumentation/openai-client-instrumentation/common/build.gradle.kts new file mode 100644 index 00000000..0b652dd8 --- /dev/null +++ b/instrumentation/openai-client-instrumentation/common/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + id("elastic-otel.java-conventions") +} + +dependencies { + compileOnly(catalog.openaiClient) + compileOnly("io.opentelemetry:opentelemetry-sdk") + compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api") + compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api") +} diff --git a/instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/ApiAdapter.java b/instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/ApiAdapter.java new file mode 100644 index 00000000..7ae10668 --- /dev/null +++ b/instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/ApiAdapter.java @@ -0,0 +1,99 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.otel.openai.wrappers; + +import com.openai.models.ChatCompletionAssistantMessageParam; +import com.openai.models.ChatCompletionContentPart; +import com.openai.models.ChatCompletionCreateParams; +import com.openai.models.ChatCompletionMessageParam; +import com.openai.models.ChatCompletionSystemMessageParam; +import com.openai.models.ChatCompletionToolMessageParam; +import com.openai.models.ChatCompletionUserMessageParam; +import java.util.function.Supplier; + +/** + * Api Adapter to encapsulate breaking changes across openai-client versions. If e.g. methods are + * renamed we add a adapter method here, so that we can provide per-version implementations. These + * implementations have to be added to instrumentations as helpers, which also ensures muzzle works + * effectively. + */ +public abstract class ApiAdapter { + + private static volatile ApiAdapter instance; + + public static ApiAdapter get() { + return instance; + } + + protected static void init(Supplier implementation) { + if (instance == null) { + synchronized (ApiAdapter.class) { + if (instance == null) { + instance = implementation.get(); + } + } + } + } + + /** + * Extracts the concrete message object e.g. ({@link ChatCompletionUserMessageParam}) from the + * given encapsulating {@link ChatCompletionMessageParam}. + * + * @param base the encapsulating param + * @return the unboxed concrete message param type + */ + public abstract Object extractConcreteCompletionMessageParam(ChatCompletionMessageParam base); + + /** + * @return the contained text, if the content is text. null otherwise. + */ + public abstract String asText(ChatCompletionToolMessageParam.Content content); + + /** + * @return the contained text, if the content is text. null otherwise. + */ + public abstract String asText(ChatCompletionAssistantMessageParam.Content content); + + /** + * @return the contained text, if the content is text. null otherwise. + */ + public abstract String asText(ChatCompletionSystemMessageParam.Content content); + + /** + * @return the contained text, if the content is text. null otherwise. + */ + public abstract String asText(ChatCompletionUserMessageParam.Content content); + + /** + * @return the text or refusal reason if either is available, otherwise null + */ + public abstract String extractTextOrRefusal( + ChatCompletionAssistantMessageParam.Content.ChatCompletionRequestAssistantMessageContentPart + part); + + /** + * @return the text if available, otherwise null + */ + public abstract String extractText(ChatCompletionContentPart part); + + /** + * @return the type if available, otherwise null + */ + public abstract String extractType(ChatCompletionCreateParams.ResponseFormat val); +} diff --git a/instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/ChatCompletionEventsHelper.java b/instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/ChatCompletionEventsHelper.java similarity index 84% rename from instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/ChatCompletionEventsHelper.java rename to instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/ChatCompletionEventsHelper.java index 015b736b..683390b6 100644 --- a/instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/ChatCompletionEventsHelper.java +++ b/instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/ChatCompletionEventsHelper.java @@ -22,7 +22,6 @@ import com.openai.models.ChatCompletion; import com.openai.models.ChatCompletionAssistantMessageParam; -import com.openai.models.ChatCompletionContentPart; import com.openai.models.ChatCompletionContentPartText; import com.openai.models.ChatCompletionCreateParams; import com.openai.models.ChatCompletionMessage; @@ -40,6 +39,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; public class ChatCompletionEventsHelper { @@ -54,24 +54,28 @@ public static void emitPromptLogEvents( if (!settings.emitEvents) { return; } + for (ChatCompletionMessageParam msg : request.messages()) { String eventType; MapValueBuilder bodyBuilder = new MapValueBuilder(); - if (msg.isChatCompletionSystemMessageParam()) { - ChatCompletionSystemMessageParam sysMsg = msg.asChatCompletionSystemMessageParam(); + Object concreteMessageParam = ApiAdapter.get().extractConcreteCompletionMessageParam(msg); + if (concreteMessageParam instanceof ChatCompletionSystemMessageParam) { + ChatCompletionSystemMessageParam sysMsg = + (ChatCompletionSystemMessageParam) concreteMessageParam; eventType = "gen_ai.system.message"; if (settings.captureMessageContent) { putIfNotEmpty(bodyBuilder, "content", contentToString(sysMsg.content())); } - } else if (msg.isChatCompletionUserMessageParam()) { - ChatCompletionUserMessageParam userMsg = msg.asChatCompletionUserMessageParam(); + } else if (concreteMessageParam instanceof ChatCompletionUserMessageParam) { + ChatCompletionUserMessageParam userMsg = + (ChatCompletionUserMessageParam) concreteMessageParam; eventType = "gen_ai.user.message"; if (settings.captureMessageContent) { putIfNotEmpty(bodyBuilder, "content", contentToString(userMsg.content())); } - } else if (msg.isChatCompletionAssistantMessageParam()) { + } else if (concreteMessageParam instanceof ChatCompletionAssistantMessageParam) { ChatCompletionAssistantMessageParam assistantMsg = - msg.asChatCompletionAssistantMessageParam(); + (ChatCompletionAssistantMessageParam) concreteMessageParam; eventType = "gen_ai.assistant.message"; if (settings.captureMessageContent) { assistantMsg @@ -89,8 +93,9 @@ public static void emitPromptLogEvents( bodyBuilder.put("tool_calls", Value.of(toolCallsJson)); }); } - } else if (msg.isChatCompletionToolMessageParam()) { - ChatCompletionToolMessageParam toolMsg = msg.asChatCompletionToolMessageParam(); + } else if (concreteMessageParam instanceof ChatCompletionToolMessageParam) { + ChatCompletionToolMessageParam toolMsg = + (ChatCompletionToolMessageParam) concreteMessageParam; eventType = "gen_ai.tool.message"; if (settings.captureMessageContent) { putIfNotEmpty(bodyBuilder, "content", contentToString(toolMsg.content())); @@ -110,8 +115,9 @@ private static void putIfNotEmpty(MapValueBuilder bodyBuilder, String key, Strin } private static String contentToString(ChatCompletionToolMessageParam.Content content) { - if (content.isTextContent()) { - return content.asTextContent(); + String text = ApiAdapter.get().asText(content); + if (text != null) { + return text; } else if (content.isArrayOfContentParts()) { return content.asArrayOfContentParts().stream() .map(ChatCompletionContentPartText::text) @@ -122,19 +128,13 @@ private static String contentToString(ChatCompletionToolMessageParam.Content con } private static String contentToString(ChatCompletionAssistantMessageParam.Content content) { - if (content.isTextContent()) { - return content.asTextContent(); + String text = ApiAdapter.get().asText(content); + if (text != null) { + return text; } else if (content.isArrayOfContentParts()) { return content.asArrayOfContentParts().stream() - .map( - cnt -> { - if (cnt.isChatCompletionContentPartText()) { - return cnt.asChatCompletionContentPartText().text(); - } else if (cnt.isChatCompletionContentPartRefusal()) { - return cnt.asChatCompletionContentPartRefusal().refusal(); - } - return ""; - }) + .map(ApiAdapter.get()::extractTextOrRefusal) + .filter(Objects::nonNull) .collect(Collectors.joining()); } else { throw new IllegalStateException("Unhandled content type for " + content); @@ -142,8 +142,9 @@ private static String contentToString(ChatCompletionAssistantMessageParam.Conten } private static String contentToString(ChatCompletionSystemMessageParam.Content content) { - if (content.isTextContent()) { - return content.asTextContent(); + String text = ApiAdapter.get().asText(content); + if (text != null) { + return text; } else if (content.isArrayOfContentParts()) { return content.asArrayOfContentParts().stream() .map(ChatCompletionContentPartText::text) @@ -154,13 +155,13 @@ private static String contentToString(ChatCompletionSystemMessageParam.Content c } private static String contentToString(ChatCompletionUserMessageParam.Content content) { - if (content.isTextContent()) { - return content.asTextContent(); + String text = ApiAdapter.get().asText(content); + if (text != null) { + return text; } else if (content.isArrayOfContentParts()) { return content.asArrayOfContentParts().stream() - .filter(ChatCompletionContentPart::isChatCompletionContentPartText) - .map(ChatCompletionContentPart::asChatCompletionContentPartText) - .map(ChatCompletionContentPartText::text) + .map(ApiAdapter.get()::extractText) + .filter(Objects::nonNull) .collect(Collectors.joining()); } else { throw new IllegalStateException("Unhandled content type for " + content); diff --git a/instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/Constants.java b/instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/Constants.java similarity index 100% rename from instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/Constants.java rename to instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/Constants.java diff --git a/instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/DelegatingInvocationHandler.java b/instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/DelegatingInvocationHandler.java similarity index 100% rename from instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/DelegatingInvocationHandler.java rename to instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/DelegatingInvocationHandler.java diff --git a/instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/EventLoggingStreamedResponse.java b/instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/EventLoggingStreamedResponse.java similarity index 100% rename from instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/EventLoggingStreamedResponse.java rename to instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/EventLoggingStreamedResponse.java diff --git a/instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/GenAiAttributes.java b/instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/GenAiAttributes.java similarity index 100% rename from instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/GenAiAttributes.java rename to instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/GenAiAttributes.java diff --git a/instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/GenAiClientMetrics.java b/instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/GenAiClientMetrics.java similarity index 100% rename from instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/GenAiClientMetrics.java rename to instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/GenAiClientMetrics.java diff --git a/instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/InstrumentationSettings.java b/instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/InstrumentationSettings.java similarity index 100% rename from instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/InstrumentationSettings.java rename to instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/InstrumentationSettings.java diff --git a/instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/InstrumentedChatCompletionService.java b/instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/InstrumentedChatCompletionService.java similarity index 97% rename from instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/InstrumentedChatCompletionService.java rename to instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/InstrumentedChatCompletionService.java index 412045d6..90c8d684 100644 --- a/instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/InstrumentedChatCompletionService.java +++ b/instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/InstrumentedChatCompletionService.java @@ -137,10 +137,9 @@ public void onStart( .responseFormat() .ifPresent( val -> { - if (val.isResponseFormatText()) { - attributes.put( - GEN_AI_OPENAI_REQUEST_RESPONSE_FORMAT, - val.asResponseFormatText()._type().toString()); + String typeString = ApiAdapter.get().extractType(val); + if (typeString != null) { + attributes.put(GEN_AI_OPENAI_REQUEST_RESPONSE_FORMAT, typeString); } }); } diff --git a/instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/InstrumentedChatService.java b/instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/InstrumentedChatService.java similarity index 100% rename from instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/InstrumentedChatService.java rename to instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/InstrumentedChatService.java diff --git a/instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/InstrumentedEmbeddingsService.java b/instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/InstrumentedEmbeddingsService.java similarity index 100% rename from instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/InstrumentedEmbeddingsService.java rename to instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/InstrumentedEmbeddingsService.java diff --git a/instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/InstrumentedOpenAiClient.java b/instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/InstrumentedOpenAiClient.java similarity index 100% rename from instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/InstrumentedOpenAiClient.java rename to instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/InstrumentedOpenAiClient.java diff --git a/instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/MapValueBuilder.java b/instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/MapValueBuilder.java similarity index 100% rename from instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/MapValueBuilder.java rename to instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/MapValueBuilder.java diff --git a/instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/StreamedMessageBuffer.java b/instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/StreamedMessageBuffer.java similarity index 100% rename from instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/StreamedMessageBuffer.java rename to instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/StreamedMessageBuffer.java diff --git a/instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/TracingStreamedResponse.java b/instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/TracingStreamedResponse.java similarity index 100% rename from instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/TracingStreamedResponse.java rename to instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/TracingStreamedResponse.java diff --git a/instrumentation/openai-client-instrumentation/build.gradle.kts b/instrumentation/openai-client-instrumentation/instrumentation-0.14/build.gradle.kts similarity index 65% rename from instrumentation/openai-client-instrumentation/build.gradle.kts rename to instrumentation/openai-client-instrumentation/instrumentation-0.14/build.gradle.kts index 102a1f70..698bab67 100644 --- a/instrumentation/openai-client-instrumentation/build.gradle.kts +++ b/instrumentation/openai-client-instrumentation/instrumentation-0.14/build.gradle.kts @@ -6,12 +6,10 @@ plugins { dependencies { compileOnly(catalog.openaiClient) - testImplementation(catalog.openaiClient) + implementation(project(":instrumentation:openai-client-instrumentation:common")) - testImplementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2") - testImplementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.18.2") - testImplementation("org.slf4j:slf4j-simple:2.0.16") - testImplementation(catalog.wiremock) + testImplementation(catalog.openaiClient) + testImplementation(project(":instrumentation:openai-client-instrumentation:testing-common")) } muzzle { @@ -19,7 +17,7 @@ muzzle { val openaiClientLib = catalog.openaiClient.get() group.set(openaiClientLib.group) module.set(openaiClientLib.name) - versions.set("(,${openaiClientLib.version}]") + versions.set("(0.13.0,${openaiClientLib.version}]") // no assertInverse.set(true) here because we don't want muzzle to fail for newer releases on our main branch // instead, renovate will bump the version and failures will be automatically detected on that bump PR } diff --git a/instrumentation/openai-client-instrumentation/instrumentation-0.14/src/main/java/co/elastic/otel/openai/v0_14/ApiAdapterImpl.java b/instrumentation/openai-client-instrumentation/instrumentation-0.14/src/main/java/co/elastic/otel/openai/v0_14/ApiAdapterImpl.java new file mode 100644 index 00000000..9de26cc5 --- /dev/null +++ b/instrumentation/openai-client-instrumentation/instrumentation-0.14/src/main/java/co/elastic/otel/openai/v0_14/ApiAdapterImpl.java @@ -0,0 +1,104 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.otel.openai.v0_14; + +import co.elastic.otel.openai.wrappers.ApiAdapter; +import com.openai.models.ChatCompletionAssistantMessageParam; +import com.openai.models.ChatCompletionContentPart; +import com.openai.models.ChatCompletionCreateParams; +import com.openai.models.ChatCompletionMessageParam; +import com.openai.models.ChatCompletionSystemMessageParam; +import com.openai.models.ChatCompletionToolMessageParam; +import com.openai.models.ChatCompletionUserMessageParam; + +public class ApiAdapterImpl extends ApiAdapter { + + public static void init() { + ApiAdapter.init(ApiAdapterImpl::new); + } + + @Override + public Object extractConcreteCompletionMessageParam(ChatCompletionMessageParam base) { + if (base.isSystem()) { + return base.asSystem(); + } + if (base.isUser()) { + return base.asUser(); + } + if (base.isAssistant()) { + return base.asAssistant(); + } + if (base.isTool()) { + return base.asTool(); + } + throw new IllegalStateException("Unhandled message param type: " + base); + } + + @Override + public String asText(ChatCompletionToolMessageParam.Content content) { + return content.isText() ? content.asText() : null; + } + + @Override + public String asText(ChatCompletionAssistantMessageParam.Content content) { + return content.isText() ? content.asText() : null; + } + + @Override + public String asText(ChatCompletionSystemMessageParam.Content content) { + return content.isText() ? content.asText() : null; + } + + @Override + public String asText(ChatCompletionUserMessageParam.Content content) { + return content.isText() ? content.asText() : null; + } + + @Override + public String extractTextOrRefusal( + ChatCompletionAssistantMessageParam.Content.ChatCompletionRequestAssistantMessageContentPart + part) { + if (part.isText()) { + return part.asText().text(); + } + if (part.isRefusal()) { + return part.asRefusal().refusal(); + } + return null; + } + + @Override + public String extractText(ChatCompletionContentPart part) { + return part.isText() ? part.asText().text() : null; + } + + @Override + public String extractType(ChatCompletionCreateParams.ResponseFormat val) { + if (val.isText()) { + return val.asText()._type().toString(); + } + if (val.isJsonObject()) { + return val.asJsonObject()._type().toString(); + } + if (val.isJsonSchema()) { + return val.asJsonSchema()._type().toString(); + } + return null; + } +} diff --git a/instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/OpenAiClientInstrumentationModule.java b/instrumentation/openai-client-instrumentation/instrumentation-0.14/src/main/java/co/elastic/otel/openai/v0_14/OpenAiClientInstrumentationModule.java similarity index 74% rename from instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/OpenAiClientInstrumentationModule.java rename to instrumentation/openai-client-instrumentation/instrumentation-0.14/src/main/java/co/elastic/otel/openai/v0_14/OpenAiClientInstrumentationModule.java index 5e87fe7e..dd347256 100644 --- a/instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/OpenAiClientInstrumentationModule.java +++ b/instrumentation/openai-client-instrumentation/instrumentation-0.14/src/main/java/co/elastic/otel/openai/v0_14/OpenAiClientInstrumentationModule.java @@ -16,7 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.otel.openai; +package co.elastic.otel.openai.v0_14; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; import co.elastic.otel.openai.wrappers.Constants; import com.google.auto.service.AutoService; @@ -24,6 +26,7 @@ import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import java.util.Collections; import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; @AutoService(InstrumentationModule.class) public class OpenAiClientInstrumentationModule extends InstrumentationModule { @@ -32,6 +35,14 @@ public OpenAiClientInstrumentationModule() { super(Constants.INSTRUMENTATION_NAME); } + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // HandlerReferencingAsyncStreamResponse was added in 0.14.1, + // which is the next release after 0.13.0 + // 0.14.0 was a broken release which doesn't exist on maven central + return hasClassesNamed("com.openai.core.http.HandlerReferencingAsyncStreamResponse"); + } + @Override public List typeInstrumentations() { return Collections.singletonList(new OpenAiOkHttpClientBuilderInstrumentation()); diff --git a/instrumentation/openai-client-instrumentation/instrumentation-0.14/src/main/java/co/elastic/otel/openai/v0_14/OpenAiOkHttpClientBuilderInstrumentation.java b/instrumentation/openai-client-instrumentation/instrumentation-0.14/src/main/java/co/elastic/otel/openai/v0_14/OpenAiOkHttpClientBuilderInstrumentation.java new file mode 100644 index 00000000..26ee8728 --- /dev/null +++ b/instrumentation/openai-client-instrumentation/instrumentation-0.14/src/main/java/co/elastic/otel/openai/v0_14/OpenAiOkHttpClientBuilderInstrumentation.java @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.otel.openai.v0_14; + +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.returns; + +import co.elastic.otel.openai.wrappers.InstrumentedOpenAiClient; +import com.openai.client.OpenAIClient; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class OpenAiOkHttpClientBuilderInstrumentation implements TypeInstrumentation { + + @Override + public ElementMatcher typeMatcher() { + return named("com.openai.client.okhttp.OpenAIOkHttpClient$Builder"); + } + + @Override + public void transform(TypeTransformer typeTransformer) { + typeTransformer.applyAdviceToMethod( + named("build").and(returns(named("com.openai.client.OpenAIClient"))), + getClass().getName() + "$AdviceClass"); + } + + public static class AdviceClass { + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + @Advice.AssignReturned.ToReturned + public static OpenAIClient onExit( + @Advice.Return OpenAIClient result, @Advice.FieldValue("baseUrl") String baseUrl) { + // This initialization has two purposes: + // Initialize the correct adapter AND ensure that it is picked up by muzzle + ApiAdapterImpl.init(); + return InstrumentedOpenAiClient.wrap(result).baseUrl(baseUrl).build(); + } + } +} diff --git a/instrumentation/openai-client-instrumentation/instrumentation-0.14/src/test/java/co/elastic/otel/openai/v0_14/ChatTest.java b/instrumentation/openai-client-instrumentation/instrumentation-0.14/src/test/java/co/elastic/otel/openai/v0_14/ChatTest.java new file mode 100644 index 00000000..622412a6 --- /dev/null +++ b/instrumentation/openai-client-instrumentation/instrumentation-0.14/src/test/java/co/elastic/otel/openai/v0_14/ChatTest.java @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.otel.openai.v0_14; + +import co.elastic.otel.openai.ChatTestBase; +import com.openai.models.ChatCompletionAssistantMessageParam; +import com.openai.models.ChatCompletionMessageParam; +import com.openai.models.ChatCompletionMessageToolCall; +import com.openai.models.ChatCompletionSystemMessageParam; +import com.openai.models.ChatCompletionToolMessageParam; +import com.openai.models.ChatCompletionUserMessageParam; +import java.util.List; + +class ChatTest extends ChatTestBase { + + @Override + protected ChatCompletionMessageParam createAssistantMessage(String content) { + return ChatCompletionMessageParam.ofAssistant( + ChatCompletionAssistantMessageParam.builder() + .content(ChatCompletionAssistantMessageParam.Content.ofText(content)) + .build()); + } + + @Override + protected ChatCompletionMessageParam createAssistantMessage( + List toolCalls) { + return ChatCompletionMessageParam.ofAssistant( + ChatCompletionAssistantMessageParam.builder() + .content(ChatCompletionAssistantMessageParam.Content.ofText("")) + .toolCalls(toolCalls) + .build()); + } + + @Override + protected ChatCompletionMessageParam createUserMessage(String content) { + return ChatCompletionMessageParam.ofUser( + ChatCompletionUserMessageParam.builder() + .content(ChatCompletionUserMessageParam.Content.ofText(content)) + .build()); + } + + @Override + protected ChatCompletionMessageParam createSystemMessage(String content) { + return ChatCompletionMessageParam.ofSystem( + ChatCompletionSystemMessageParam.builder() + .content(ChatCompletionSystemMessageParam.Content.ofText(content)) + .build()); + } + + @Override + protected ChatCompletionMessageParam createToolMessage(String response, String id) { + return ChatCompletionMessageParam.ofTool( + ChatCompletionToolMessageParam.builder() + .toolCallId(id) + .content(ChatCompletionToolMessageParam.Content.ofText(response)) + .build()); + } +} diff --git a/instrumentation/openai-client-instrumentation/instrumentation-0.14/src/test/java/co/elastic/otel/openai/v0_14/EmbeddingsTest.java b/instrumentation/openai-client-instrumentation/instrumentation-0.14/src/test/java/co/elastic/otel/openai/v0_14/EmbeddingsTest.java new file mode 100644 index 00000000..bc8ed40e --- /dev/null +++ b/instrumentation/openai-client-instrumentation/instrumentation-0.14/src/test/java/co/elastic/otel/openai/v0_14/EmbeddingsTest.java @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.otel.openai.v0_14; + +import co.elastic.otel.openai.EmbeddingsTestBase; + +class EmbeddingsTest extends EmbeddingsTestBase {} diff --git a/instrumentation/openai-client-instrumentation/src/test/java/co/elastic/otel/openai/LiveAPIChatIntegrationTest.java b/instrumentation/openai-client-instrumentation/instrumentation-0.14/src/test/java/co/elastic/otel/openai/v0_14/LiveAPIChatIntegrationTest.java similarity index 98% rename from instrumentation/openai-client-instrumentation/src/test/java/co/elastic/otel/openai/LiveAPIChatIntegrationTest.java rename to instrumentation/openai-client-instrumentation/instrumentation-0.14/src/test/java/co/elastic/otel/openai/v0_14/LiveAPIChatIntegrationTest.java index 0367e7ce..1431153e 100644 --- a/instrumentation/openai-client-instrumentation/src/test/java/co/elastic/otel/openai/LiveAPIChatIntegrationTest.java +++ b/instrumentation/openai-client-instrumentation/instrumentation-0.14/src/test/java/co/elastic/otel/openai/v0_14/LiveAPIChatIntegrationTest.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.otel.openai; +package co.elastic.otel.openai.v0_14; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; @@ -32,6 +32,8 @@ import static io.opentelemetry.semconv.incubating.GenAiIncubatingAttributes.GEN_AI_USAGE_INPUT_TOKENS; import static io.opentelemetry.semconv.incubating.GenAiIncubatingAttributes.GEN_AI_USAGE_OUTPUT_TOKENS; +import co.elastic.otel.openai.ChatTestBase; +import co.elastic.otel.openai.ValAssert; import co.elastic.otel.openai.wrappers.InstrumentationSettingsAccessor; import com.openai.client.OpenAIClient; import com.openai.client.okhttp.OpenAIOkHttpClient; @@ -123,7 +125,7 @@ void toolCallsWithCaptureMessageContent() { "Hi there! I can help with that. Can you please provide your order ID?"), createUserMessage("i think it is order_12345"))) .model(TEST_CHAT_MODEL) - .addTool(ChatTest.buildGetDeliveryDateToolDefinition()) + .addTool(ChatTestBase.buildGetDeliveryDateToolDefinition()) .build(); long startTimeNanos = System.nanoTime(); @@ -576,31 +578,31 @@ void streamWithCaptureMessageContent() throws Exception { } private static ChatCompletionMessageParam createAssistantMessage(String content) { - return ChatCompletionMessageParam.ofChatCompletionAssistantMessageParam( + return ChatCompletionMessageParam.ofAssistant( ChatCompletionAssistantMessageParam.builder() - .content(ChatCompletionAssistantMessageParam.Content.ofTextContent(content)) + .content(ChatCompletionAssistantMessageParam.Content.ofText(content)) .build()); } private static ChatCompletionMessageParam createUserMessage(String content) { - return ChatCompletionMessageParam.ofChatCompletionUserMessageParam( + return ChatCompletionMessageParam.ofUser( ChatCompletionUserMessageParam.builder() - .content(ChatCompletionUserMessageParam.Content.ofTextContent(content)) + .content(ChatCompletionUserMessageParam.Content.ofText(content)) .build()); } private static ChatCompletionMessageParam createSystemMessage(String content) { - return ChatCompletionMessageParam.ofChatCompletionSystemMessageParam( + return ChatCompletionMessageParam.ofSystem( ChatCompletionSystemMessageParam.builder() - .content(ChatCompletionSystemMessageParam.Content.ofTextContent(content)) + .content(ChatCompletionSystemMessageParam.Content.ofText(content)) .build()); } private static ChatCompletionMessageParam createToolMessage(String response, String id) { - return ChatCompletionMessageParam.ofChatCompletionToolMessageParam( + return ChatCompletionMessageParam.ofTool( ChatCompletionToolMessageParam.builder() .toolCallId(id) - .content(ChatCompletionToolMessageParam.Content.ofTextContent(response)) + .content(ChatCompletionToolMessageParam.Content.ofText(response)) .build()); } diff --git a/instrumentation/openai-client-instrumentation/instrumentation-0.2/build.gradle.kts b/instrumentation/openai-client-instrumentation/instrumentation-0.2/build.gradle.kts new file mode 100644 index 00000000..63da0932 --- /dev/null +++ b/instrumentation/openai-client-instrumentation/instrumentation-0.2/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + alias(catalog.plugins.muzzleGeneration) + alias(catalog.plugins.muzzleCheck) + id("elastic-otel.instrumentation-conventions") +} + +val openAiVersion = "0.13.0"; // DO NOT UPGRADE + +dependencies { + compileOnly("com.openai:openai-java:${openAiVersion}") + implementation(project(":instrumentation:openai-client-instrumentation:common")) + + testImplementation("com.openai:openai-java:${openAiVersion}") + testImplementation(project(":instrumentation:openai-client-instrumentation:testing-common")) +} + +muzzle { + pass { + group.set("com.openai") + module.set("openai-java") + versions.set("(,${openAiVersion}]") + assertInverse.set(true) + } +} diff --git a/instrumentation/openai-client-instrumentation/instrumentation-0.2/src/main/java/co/elastic/otel/openai/v0_2/ApiAdapterImpl.java b/instrumentation/openai-client-instrumentation/instrumentation-0.2/src/main/java/co/elastic/otel/openai/v0_2/ApiAdapterImpl.java new file mode 100644 index 00000000..f00ac3cc --- /dev/null +++ b/instrumentation/openai-client-instrumentation/instrumentation-0.2/src/main/java/co/elastic/otel/openai/v0_2/ApiAdapterImpl.java @@ -0,0 +1,105 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.otel.openai.v0_2; + +import co.elastic.otel.openai.wrappers.ApiAdapter; +import com.openai.models.ChatCompletionAssistantMessageParam; +import com.openai.models.ChatCompletionContentPart; +import com.openai.models.ChatCompletionCreateParams; +import com.openai.models.ChatCompletionMessageParam; +import com.openai.models.ChatCompletionSystemMessageParam; +import com.openai.models.ChatCompletionToolMessageParam; +import com.openai.models.ChatCompletionUserMessageParam; + +public class ApiAdapterImpl extends ApiAdapter { + + public static void init() { + ApiAdapter.init(ApiAdapterImpl::new); + } + + @Override + public Object extractConcreteCompletionMessageParam(ChatCompletionMessageParam base) { + if (base.isChatCompletionSystemMessageParam()) { + return base.asChatCompletionSystemMessageParam(); + } + if (base.isChatCompletionUserMessageParam()) { + return base.asChatCompletionUserMessageParam(); + } + if (base.isChatCompletionAssistantMessageParam()) { + return base.asChatCompletionAssistantMessageParam(); + } + if (base.isChatCompletionToolMessageParam()) { + return base.asChatCompletionToolMessageParam(); + } + throw new IllegalStateException("Unhandled message param type: " + base); + } + + @Override + public String extractText(ChatCompletionContentPart part) { + if (part.isChatCompletionContentPartText()) { + return part.asChatCompletionContentPartText().text(); + } + return null; + } + + @Override + public String extractType(ChatCompletionCreateParams.ResponseFormat val) { + if (val.isResponseFormatText()) { + return val.asResponseFormatText()._type().toString(); + } else if (val.isResponseFormatJsonObject()) { + return val.asResponseFormatJsonObject()._type().toString(); + } else if (val.isResponseFormatJsonSchema()) { + return val.asResponseFormatJsonSchema()._type().toString(); + } + return null; + } + + @Override + public String extractTextOrRefusal( + ChatCompletionAssistantMessageParam.Content.ChatCompletionRequestAssistantMessageContentPart + part) { + if (part.isChatCompletionContentPartText()) { + return part.asChatCompletionContentPartText().text(); + } + if (part.isChatCompletionContentPartRefusal()) { + return part.asChatCompletionContentPartRefusal().refusal(); + } + return null; + } + + @Override + public String asText(ChatCompletionUserMessageParam.Content content) { + return content.isTextContent() ? content.asTextContent() : null; + } + + @Override + public String asText(ChatCompletionSystemMessageParam.Content content) { + return content.isTextContent() ? content.asTextContent() : null; + } + + @Override + public String asText(ChatCompletionAssistantMessageParam.Content content) { + return content.isTextContent() ? content.asTextContent() : null; + } + + @Override + public String asText(ChatCompletionToolMessageParam.Content content) { + return content.isTextContent() ? content.asTextContent() : null; + } +} diff --git a/instrumentation/openai-client-instrumentation/instrumentation-0.2/src/main/java/co/elastic/otel/openai/v0_2/OpenAiClientInstrumentationModule.java b/instrumentation/openai-client-instrumentation/instrumentation-0.2/src/main/java/co/elastic/otel/openai/v0_2/OpenAiClientInstrumentationModule.java new file mode 100644 index 00000000..ae170e8e --- /dev/null +++ b/instrumentation/openai-client-instrumentation/instrumentation-0.2/src/main/java/co/elastic/otel/openai/v0_2/OpenAiClientInstrumentationModule.java @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.otel.openai.v0_2; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.not; + +import co.elastic.otel.openai.wrappers.Constants; +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.Collections; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +@AutoService(InstrumentationModule.class) +public class OpenAiClientInstrumentationModule extends InstrumentationModule { + + public OpenAiClientInstrumentationModule() { + super(Constants.INSTRUMENTATION_NAME); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + // HandlerReferencingAsyncStreamResponse was added in 0.14.1, + // which is the next release after 0.13.0 + // 0.14.0 was a broken release which doesn't exist on maven central + return not(hasClassesNamed("com.openai.core.http.HandlerReferencingAsyncStreamResponse")); + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList(new OpenAiOkHttpClientBuilderInstrumentation()); + } + + @Override + public boolean isHelperClass(String className) { + return className.startsWith("co.elastic.otel.openai"); + } +} diff --git a/instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/OpenAiOkHttpClientBuilderInstrumentation.java b/instrumentation/openai-client-instrumentation/instrumentation-0.2/src/main/java/co/elastic/otel/openai/v0_2/OpenAiOkHttpClientBuilderInstrumentation.java similarity index 89% rename from instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/OpenAiOkHttpClientBuilderInstrumentation.java rename to instrumentation/openai-client-instrumentation/instrumentation-0.2/src/main/java/co/elastic/otel/openai/v0_2/OpenAiOkHttpClientBuilderInstrumentation.java index 17504559..d0ec84c5 100644 --- a/instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/OpenAiOkHttpClientBuilderInstrumentation.java +++ b/instrumentation/openai-client-instrumentation/instrumentation-0.2/src/main/java/co/elastic/otel/openai/v0_2/OpenAiOkHttpClientBuilderInstrumentation.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package co.elastic.otel.openai; +package co.elastic.otel.openai.v0_2; import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.returns; @@ -40,7 +40,7 @@ public ElementMatcher typeMatcher() { public void transform(TypeTransformer typeTransformer) { typeTransformer.applyAdviceToMethod( named("build").and(returns(named("com.openai.client.OpenAIClient"))), - "co.elastic.otel.openai.OpenAiOkHttpClientBuilderInstrumentation$AdviceClass"); + getClass().getName() + "$AdviceClass"); } public static class AdviceClass { @@ -49,6 +49,9 @@ public static class AdviceClass { @Advice.AssignReturned.ToReturned public static OpenAIClient onExit( @Advice.Return OpenAIClient result, @Advice.FieldValue("baseUrl") String baseUrl) { + // This initialization has two purposes: + // Initialize the correct adapter AND ensure that it is picked up by muzzle + ApiAdapterImpl.init(); return InstrumentedOpenAiClient.wrap(result).baseUrl(baseUrl).build(); } } diff --git a/instrumentation/openai-client-instrumentation/instrumentation-0.2/src/test/java/co/elastic/otel/openai/v0_2/ChatTest.java b/instrumentation/openai-client-instrumentation/instrumentation-0.2/src/test/java/co/elastic/otel/openai/v0_2/ChatTest.java new file mode 100644 index 00000000..b386f5af --- /dev/null +++ b/instrumentation/openai-client-instrumentation/instrumentation-0.2/src/test/java/co/elastic/otel/openai/v0_2/ChatTest.java @@ -0,0 +1,74 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.otel.openai.v0_2; + +import co.elastic.otel.openai.ChatTestBase; +import com.openai.models.ChatCompletionAssistantMessageParam; +import com.openai.models.ChatCompletionMessageParam; +import com.openai.models.ChatCompletionMessageToolCall; +import com.openai.models.ChatCompletionSystemMessageParam; +import com.openai.models.ChatCompletionToolMessageParam; +import com.openai.models.ChatCompletionUserMessageParam; +import java.util.List; + +class ChatTest extends ChatTestBase { + + @Override + protected ChatCompletionMessageParam createAssistantMessage(String content) { + return ChatCompletionMessageParam.ofChatCompletionAssistantMessageParam( + ChatCompletionAssistantMessageParam.builder() + .content(ChatCompletionAssistantMessageParam.Content.ofTextContent(content)) + .build()); + } + + @Override + protected ChatCompletionMessageParam createAssistantMessage( + List toolCalls) { + return ChatCompletionMessageParam.ofChatCompletionAssistantMessageParam( + ChatCompletionAssistantMessageParam.builder() + .content(ChatCompletionAssistantMessageParam.Content.ofTextContent("")) + .toolCalls(toolCalls) + .build()); + } + + @Override + protected ChatCompletionMessageParam createUserMessage(String content) { + return ChatCompletionMessageParam.ofChatCompletionUserMessageParam( + ChatCompletionUserMessageParam.builder() + .content(ChatCompletionUserMessageParam.Content.ofTextContent(content)) + .build()); + } + + @Override + protected ChatCompletionMessageParam createSystemMessage(String content) { + return ChatCompletionMessageParam.ofChatCompletionSystemMessageParam( + ChatCompletionSystemMessageParam.builder() + .content(ChatCompletionSystemMessageParam.Content.ofTextContent(content)) + .build()); + } + + @Override + protected ChatCompletionMessageParam createToolMessage(String response, String id) { + return ChatCompletionMessageParam.ofChatCompletionToolMessageParam( + ChatCompletionToolMessageParam.builder() + .toolCallId(id) + .content(ChatCompletionToolMessageParam.Content.ofTextContent(response)) + .build()); + } +} diff --git a/instrumentation/openai-client-instrumentation/instrumentation-0.2/src/test/java/co/elastic/otel/openai/v0_2/EmbeddingsTest.java b/instrumentation/openai-client-instrumentation/instrumentation-0.2/src/test/java/co/elastic/otel/openai/v0_2/EmbeddingsTest.java new file mode 100644 index 00000000..814bc53d --- /dev/null +++ b/instrumentation/openai-client-instrumentation/instrumentation-0.2/src/test/java/co/elastic/otel/openai/v0_2/EmbeddingsTest.java @@ -0,0 +1,23 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package co.elastic.otel.openai.v0_2; + +import co.elastic.otel.openai.EmbeddingsTestBase; + +class EmbeddingsTest extends EmbeddingsTestBase {} diff --git a/instrumentation/openai-client-instrumentation/testing-common/build.gradle.kts b/instrumentation/openai-client-instrumentation/testing-common/build.gradle.kts new file mode 100644 index 00000000..6fdff9f0 --- /dev/null +++ b/instrumentation/openai-client-instrumentation/testing-common/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("elastic-otel.java-conventions") +} + +dependencies { + compileOnly(catalog.openaiClient) + compileOnly(project(":instrumentation:openai-client-instrumentation:common")) + + implementation("io.opentelemetry.javaagent:opentelemetry-testing-common") + implementation("io.opentelemetry:opentelemetry-sdk-testing") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.18.2") + implementation("org.slf4j:slf4j-simple:2.0.16") + implementation(catalog.wiremock) +} diff --git a/instrumentation/openai-client-instrumentation/src/test/java/co/elastic/otel/openai/ChatTest.java b/instrumentation/openai-client-instrumentation/testing-common/src/main/java/co/elastic/otel/openai/ChatTestBase.java similarity index 97% rename from instrumentation/openai-client-instrumentation/src/test/java/co/elastic/otel/openai/ChatTest.java rename to instrumentation/openai-client-instrumentation/testing-common/src/main/java/co/elastic/otel/openai/ChatTestBase.java index d035b987..662a38b6 100644 --- a/instrumentation/openai-client-instrumentation/src/test/java/co/elastic/otel/openai/ChatTest.java +++ b/instrumentation/openai-client-instrumentation/testing-common/src/main/java/co/elastic/otel/openai/ChatTestBase.java @@ -53,16 +53,12 @@ import com.openai.errors.NotFoundException; import com.openai.errors.OpenAIIoException; import com.openai.models.ChatCompletion; -import com.openai.models.ChatCompletionAssistantMessageParam; import com.openai.models.ChatCompletionChunk; import com.openai.models.ChatCompletionCreateParams; import com.openai.models.ChatCompletionMessageParam; import com.openai.models.ChatCompletionMessageToolCall; import com.openai.models.ChatCompletionStreamOptions; -import com.openai.models.ChatCompletionSystemMessageParam; import com.openai.models.ChatCompletionTool; -import com.openai.models.ChatCompletionToolMessageParam; -import com.openai.models.ChatCompletionUserMessageParam; import com.openai.models.FunctionDefinition; import com.openai.models.FunctionParameters; import com.openai.models.ResponseFormatText; @@ -86,7 +82,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -class ChatTest { +public abstract class ChatTestBase { private static final String TEST_CHAT_MODEL = "gpt-4o-mini"; private static final String TEST_CHAT_RESPONSE_MODEL = "gpt-4o-mini-2024-07-18"; private static final String TEST_CHAT_INPUT = @@ -583,7 +579,7 @@ void toolCalls() { assertThat(attr) .containsEntry("event.name", "gen_ai.system.message") .containsEntry(GEN_AI_SYSTEM, "openai")); - assertThat(log.getBodyValue()).satisfies(ChatTest::assertThatValueIsEmptyMap); + assertThat(log.getBodyValue()).satisfies(ChatTestBase::assertThatValueIsEmptyMap); }) .anySatisfy( log -> { @@ -593,7 +589,7 @@ void toolCalls() { assertThat(attr) .containsEntry("event.name", "gen_ai.user.message") .containsEntry(GEN_AI_SYSTEM, "openai")); - assertThat(log.getBodyValue()).satisfies(ChatTest::assertThatValueIsEmptyMap); + assertThat(log.getBodyValue()).satisfies(ChatTestBase::assertThatValueIsEmptyMap); }) .anySatisfy( log -> { @@ -603,7 +599,7 @@ void toolCalls() { assertThat(attr) .containsEntry("event.name", "gen_ai.assistant.message") .containsEntry(GEN_AI_SYSTEM, "openai")); - assertThat(log.getBodyValue()).satisfies(ChatTest::assertThatValueIsEmptyMap); + assertThat(log.getBodyValue()).satisfies(ChatTestBase::assertThatValueIsEmptyMap); }) .anySatisfy( log -> { @@ -613,7 +609,7 @@ void toolCalls() { assertThat(attr) .containsEntry("event.name", "gen_ai.user.message") .containsEntry(GEN_AI_SYSTEM, "openai")); - assertThat(log.getBodyValue()).satisfies(ChatTest::assertThatValueIsEmptyMap); + assertThat(log.getBodyValue()).satisfies(ChatTestBase::assertThatValueIsEmptyMap); }) .anySatisfy( log -> { @@ -1196,7 +1192,7 @@ void stream() throws Exception { .containsEntry("event.name", "gen_ai.user.message") .containsEntry(GEN_AI_SYSTEM, "openai")) .hasSpanContext(spanCtx); - assertThat(log.getBodyValue()).satisfies(ChatTest::assertThatValueIsEmptyMap); + assertThat(log.getBodyValue()).satisfies(ChatTestBase::assertThatValueIsEmptyMap); }) .anySatisfy( log -> { @@ -1296,7 +1292,7 @@ void streamWithIncludeUsage() throws Exception { .containsEntry("event.name", "gen_ai.user.message") .containsEntry(GEN_AI_SYSTEM, "openai")) .hasSpanContext(spanCtx); - assertThat(log.getBodyValue()).satisfies(ChatTest::assertThatValueIsEmptyMap); + assertThat(log.getBodyValue()).satisfies(ChatTestBase::assertThatValueIsEmptyMap); }) .anySatisfy( log -> { @@ -1436,7 +1432,7 @@ void streamAllTheClientOptions() throws Exception { .containsEntry("event.name", "gen_ai.user.message") .containsEntry(GEN_AI_SYSTEM, "openai")) .hasSpanContext(spanCtx); - assertThat(log.getBodyValue()).satisfies(ChatTest::assertThatValueIsEmptyMap); + assertThat(log.getBodyValue()).satisfies(ChatTestBase::assertThatValueIsEmptyMap); }) .anySatisfy( log -> { @@ -2045,12 +2041,7 @@ void toolsWithFollowupAndCaptureContent() { equalTo(SERVER_PORT, (long) openai.getPort()))))); testing.clearData(); - ChatCompletionMessageParam assistantMessage = - ChatCompletionMessageParam.ofChatCompletionAssistantMessageParam( - ChatCompletionAssistantMessageParam.builder() - .content(ChatCompletionAssistantMessageParam.Content.ofTextContent("")) - .toolCalls(toolCalls) - .build()); + ChatCompletionMessageParam assistantMessage = createAssistantMessage(toolCalls); chatMessages.add(assistantMessage); chatMessages.add(createToolMessage("25 degrees and sunny", newYorkCallId)); @@ -2315,7 +2306,7 @@ private static ChatCompletionTool buildGetWeatherToolDefinition() { .build(); } - static ChatCompletionTool buildGetDeliveryDateToolDefinition() { + public static ChatCompletionTool buildGetDeliveryDateToolDefinition() { Map orderId = new HashMap<>(); orderId.put("type", JsonValue.from("string")); orderId.put("description", JsonValue.from("The customer's order ID.")); @@ -2343,32 +2334,14 @@ static ChatCompletionTool buildGetDeliveryDateToolDefinition() { .build(); } - private static ChatCompletionMessageParam createAssistantMessage(String content) { - return ChatCompletionMessageParam.ofChatCompletionAssistantMessageParam( - ChatCompletionAssistantMessageParam.builder() - .content(ChatCompletionAssistantMessageParam.Content.ofTextContent(content)) - .build()); - } + protected abstract ChatCompletionMessageParam createAssistantMessage(String content); - private static ChatCompletionMessageParam createUserMessage(String content) { - return ChatCompletionMessageParam.ofChatCompletionUserMessageParam( - ChatCompletionUserMessageParam.builder() - .content(ChatCompletionUserMessageParam.Content.ofTextContent(content)) - .build()); - } + protected abstract ChatCompletionMessageParam createAssistantMessage( + List toolCalls); - private static ChatCompletionMessageParam createSystemMessage(String content) { - return ChatCompletionMessageParam.ofChatCompletionSystemMessageParam( - ChatCompletionSystemMessageParam.builder() - .content(ChatCompletionSystemMessageParam.Content.ofTextContent(content)) - .build()); - } + protected abstract ChatCompletionMessageParam createUserMessage(String content); - private static ChatCompletionMessageParam createToolMessage(String response, String id) { - return ChatCompletionMessageParam.ofChatCompletionToolMessageParam( - ChatCompletionToolMessageParam.builder() - .toolCallId(id) - .content(ChatCompletionToolMessageParam.Content.ofTextContent(response)) - .build()); - } + protected abstract ChatCompletionMessageParam createSystemMessage(String content); + + protected abstract ChatCompletionMessageParam createToolMessage(String response, String id); } diff --git a/instrumentation/openai-client-instrumentation/src/test/java/co/elastic/otel/openai/EmbeddingsTest.java b/instrumentation/openai-client-instrumentation/testing-common/src/main/java/co/elastic/otel/openai/EmbeddingsTestBase.java similarity index 99% rename from instrumentation/openai-client-instrumentation/src/test/java/co/elastic/otel/openai/EmbeddingsTest.java rename to instrumentation/openai-client-instrumentation/testing-common/src/main/java/co/elastic/otel/openai/EmbeddingsTestBase.java index 856f81b8..dcf2ac67 100644 --- a/instrumentation/openai-client-instrumentation/src/test/java/co/elastic/otel/openai/EmbeddingsTest.java +++ b/instrumentation/openai-client-instrumentation/testing-common/src/main/java/co/elastic/otel/openai/EmbeddingsTestBase.java @@ -40,7 +40,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -class EmbeddingsTest { +public class EmbeddingsTestBase { private static final String MODEL = System.getenv().getOrDefault("OPENAI_MODEL", "text-embedding-3-small"); diff --git a/instrumentation/openai-client-instrumentation/src/test/java/co/elastic/otel/openai/OpenAIRecordingExtension.java b/instrumentation/openai-client-instrumentation/testing-common/src/main/java/co/elastic/otel/openai/OpenAIRecordingExtension.java similarity index 95% rename from instrumentation/openai-client-instrumentation/src/test/java/co/elastic/otel/openai/OpenAIRecordingExtension.java rename to instrumentation/openai-client-instrumentation/testing-common/src/main/java/co/elastic/otel/openai/OpenAIRecordingExtension.java index 73d45462..b2c32a18 100644 --- a/instrumentation/openai-client-instrumentation/src/test/java/co/elastic/otel/openai/OpenAIRecordingExtension.java +++ b/instrumentation/openai-client-instrumentation/testing-common/src/main/java/co/elastic/otel/openai/OpenAIRecordingExtension.java @@ -50,7 +50,8 @@ final class OpenAIRecordingExtension extends WireMockExtension { PrettyPrintEqualToJsonStubMappingTransformer.class) .mappingSource( new YamlFileMappingsSource( - new SingleRootFileSource("src/test/resources").child("mappings"))))); + new SingleRootFileSource("../testing-common/src/main/resources") + .child("mappings"))))); this.testName = testName; } diff --git a/instrumentation/openai-client-instrumentation/src/test/java/co/elastic/otel/openai/PrettyPrintEqualToJsonStubMappingTransformer.java b/instrumentation/openai-client-instrumentation/testing-common/src/main/java/co/elastic/otel/openai/PrettyPrintEqualToJsonStubMappingTransformer.java similarity index 100% rename from instrumentation/openai-client-instrumentation/src/test/java/co/elastic/otel/openai/PrettyPrintEqualToJsonStubMappingTransformer.java rename to instrumentation/openai-client-instrumentation/testing-common/src/main/java/co/elastic/otel/openai/PrettyPrintEqualToJsonStubMappingTransformer.java diff --git a/instrumentation/openai-client-instrumentation/src/test/java/co/elastic/otel/openai/ResponseHeaderScrubber.java b/instrumentation/openai-client-instrumentation/testing-common/src/main/java/co/elastic/otel/openai/ResponseHeaderScrubber.java similarity index 100% rename from instrumentation/openai-client-instrumentation/src/test/java/co/elastic/otel/openai/ResponseHeaderScrubber.java rename to instrumentation/openai-client-instrumentation/testing-common/src/main/java/co/elastic/otel/openai/ResponseHeaderScrubber.java diff --git a/instrumentation/openai-client-instrumentation/src/test/java/co/elastic/otel/openai/ValAssert.java b/instrumentation/openai-client-instrumentation/testing-common/src/main/java/co/elastic/otel/openai/ValAssert.java similarity index 100% rename from instrumentation/openai-client-instrumentation/src/test/java/co/elastic/otel/openai/ValAssert.java rename to instrumentation/openai-client-instrumentation/testing-common/src/main/java/co/elastic/otel/openai/ValAssert.java diff --git a/instrumentation/openai-client-instrumentation/src/test/java/co/elastic/otel/openai/YamlFileMappingsSource.java b/instrumentation/openai-client-instrumentation/testing-common/src/main/java/co/elastic/otel/openai/YamlFileMappingsSource.java similarity index 100% rename from instrumentation/openai-client-instrumentation/src/test/java/co/elastic/otel/openai/YamlFileMappingsSource.java rename to instrumentation/openai-client-instrumentation/testing-common/src/main/java/co/elastic/otel/openai/YamlFileMappingsSource.java diff --git a/instrumentation/openai-client-instrumentation/src/test/java/co/elastic/otel/openai/wrappers/InstrumentationSettingsAccessor.java b/instrumentation/openai-client-instrumentation/testing-common/src/main/java/co/elastic/otel/openai/wrappers/InstrumentationSettingsAccessor.java similarity index 100% rename from instrumentation/openai-client-instrumentation/src/test/java/co/elastic/otel/openai/wrappers/InstrumentationSettingsAccessor.java rename to instrumentation/openai-client-instrumentation/testing-common/src/main/java/co/elastic/otel/openai/wrappers/InstrumentationSettingsAccessor.java diff --git a/instrumentation/openai-client-instrumentation/src/test/resources/mappings/chattest.yaml b/instrumentation/openai-client-instrumentation/testing-common/src/main/resources/mappings/chattest.yaml similarity index 100% rename from instrumentation/openai-client-instrumentation/src/test/resources/mappings/chattest.yaml rename to instrumentation/openai-client-instrumentation/testing-common/src/main/resources/mappings/chattest.yaml diff --git a/instrumentation/openai-client-instrumentation/src/test/resources/mappings/embeddingstest.yaml b/instrumentation/openai-client-instrumentation/testing-common/src/main/resources/mappings/embeddingstest.yaml similarity index 100% rename from instrumentation/openai-client-instrumentation/src/test/resources/mappings/embeddingstest.yaml rename to instrumentation/openai-client-instrumentation/testing-common/src/main/resources/mappings/embeddingstest.yaml diff --git a/settings.gradle.kts b/settings.gradle.kts index e744603c..8edd172a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,7 +17,10 @@ include("agentextension") include("bootstrap") include("custom") include("instrumentation") -include("instrumentation:openai-client-instrumentation") +include("instrumentation:openai-client-instrumentation:common") +include("instrumentation:openai-client-instrumentation:testing-common") +include("instrumentation:openai-client-instrumentation:instrumentation-0.2") +include("instrumentation:openai-client-instrumentation:instrumentation-0.14") include("inferred-spans") include("resources") include("smoke-tests")