Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.next-release.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@

* Add support for OpenAI client 0.14+ - #531
3 changes: 2 additions & 1 deletion custom/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ plugins {
}

val instrumentations = listOf<String>(
":instrumentation:openai-client-instrumentation"
":instrumentation:openai-client-instrumentation:instrumentation-0.13.0",
":instrumentation:openai-client-instrumentation:instrumentation-latest"
)

dependencies {
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
Original file line number Diff line number Diff line change
@@ -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")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* 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<ApiAdapter> 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 next. null otherwise.
*/
public abstract String asText(ChatCompletionToolMessageParam.Content content);

/**
* @return the contained text, if the content is next. null otherwise.
*/
public abstract String asText(ChatCompletionAssistantMessageParam.Content content);

/**
* @return the contained text, if the content is next. null otherwise.
*/
public abstract String asText(ChatCompletionSystemMessageParam.Content content);

/**
* @return the contained text, if the content is next. 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);

public abstract String extractType(ChatCompletionCreateParams.ResponseFormat val);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -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()));
Expand All @@ -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)
Expand All @@ -122,28 +128,23 @@ 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);
}
}

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)
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,9 @@ public void onStart(
.responseFormat()
.ifPresent(
val -> {
if (val.isResponseFormatText()) {
attributes.put(
GEN_AI_OPENAI_REQUEST_RESPONSE_FORMAT,
val.asResponseFormatText()._type().toString());
}
attributes.put(
GEN_AI_OPENAI_REQUEST_RESPONSE_FORMAT,
ApiAdapter.get().extractType(val));
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it something that renovate/dependabot would attempt to upgrade ? If so I think there are ways like splitting the string in parts to prevent this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. I don't know if renovate is smart enough for this. We can just keep it like this for now and see if renovate detects it and attempts an 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)
}
}
Loading