Skip to content

Commit 1ebb1ff

Browse files
authored
Fix instrumentation support for OpenAI client 0.14+ (#531)
1 parent 21912b6 commit 1ebb1ff

File tree

44 files changed

+759
-102
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+759
-102
lines changed

CHANGELOG.next-release.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1+
* Add support for OpenAI client 0.14+ - #531

custom/build.gradle.kts

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ plugins {
33
}
44

55
val instrumentations = listOf<String>(
6-
":instrumentation:openai-client-instrumentation"
6+
":instrumentation:openai-client-instrumentation:instrumentation-0.2",
7+
":instrumentation:openai-client-instrumentation:instrumentation-0.14"
78
)
89

910
dependencies {

gradle/libs.versions.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ ant = "org.apache.ant:ant:1.10.15"
7777
asm = "org.ow2.asm:asm:9.7"
7878

7979
# Instrumented libraries
80-
openaiClient = "com.openai:openai-java:0.13.0"
80+
openaiClient = "com.openai:openai-java:0.21.0"
8181

8282
[bundles]
8383

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
plugins {
2+
id("elastic-otel.java-conventions")
3+
}
4+
5+
dependencies {
6+
compileOnly(catalog.openaiClient)
7+
compileOnly("io.opentelemetry:opentelemetry-sdk")
8+
compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api")
9+
compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api")
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package co.elastic.otel.openai.wrappers;
20+
21+
import com.openai.models.ChatCompletionAssistantMessageParam;
22+
import com.openai.models.ChatCompletionContentPart;
23+
import com.openai.models.ChatCompletionCreateParams;
24+
import com.openai.models.ChatCompletionMessageParam;
25+
import com.openai.models.ChatCompletionSystemMessageParam;
26+
import com.openai.models.ChatCompletionToolMessageParam;
27+
import com.openai.models.ChatCompletionUserMessageParam;
28+
import java.util.function.Supplier;
29+
30+
/**
31+
* Api Adapter to encapsulate breaking changes across openai-client versions. If e.g. methods are
32+
* renamed we add a adapter method here, so that we can provide per-version implementations. These
33+
* implementations have to be added to instrumentations as helpers, which also ensures muzzle works
34+
* effectively.
35+
*/
36+
public abstract class ApiAdapter {
37+
38+
private static volatile ApiAdapter instance;
39+
40+
public static ApiAdapter get() {
41+
return instance;
42+
}
43+
44+
protected static void init(Supplier<ApiAdapter> implementation) {
45+
if (instance == null) {
46+
synchronized (ApiAdapter.class) {
47+
if (instance == null) {
48+
instance = implementation.get();
49+
}
50+
}
51+
}
52+
}
53+
54+
/**
55+
* Extracts the concrete message object e.g. ({@link ChatCompletionUserMessageParam}) from the
56+
* given encapsulating {@link ChatCompletionMessageParam}.
57+
*
58+
* @param base the encapsulating param
59+
* @return the unboxed concrete message param type
60+
*/
61+
public abstract Object extractConcreteCompletionMessageParam(ChatCompletionMessageParam base);
62+
63+
/**
64+
* @return the contained text, if the content is text. null otherwise.
65+
*/
66+
public abstract String asText(ChatCompletionToolMessageParam.Content content);
67+
68+
/**
69+
* @return the contained text, if the content is text. null otherwise.
70+
*/
71+
public abstract String asText(ChatCompletionAssistantMessageParam.Content content);
72+
73+
/**
74+
* @return the contained text, if the content is text. null otherwise.
75+
*/
76+
public abstract String asText(ChatCompletionSystemMessageParam.Content content);
77+
78+
/**
79+
* @return the contained text, if the content is text. null otherwise.
80+
*/
81+
public abstract String asText(ChatCompletionUserMessageParam.Content content);
82+
83+
/**
84+
* @return the text or refusal reason if either is available, otherwise null
85+
*/
86+
public abstract String extractTextOrRefusal(
87+
ChatCompletionAssistantMessageParam.Content.ChatCompletionRequestAssistantMessageContentPart
88+
part);
89+
90+
/**
91+
* @return the text if available, otherwise null
92+
*/
93+
public abstract String extractText(ChatCompletionContentPart part);
94+
95+
/**
96+
* @return the type if available, otherwise null
97+
*/
98+
public abstract String extractType(ChatCompletionCreateParams.ResponseFormat val);
99+
}

instrumentation/openai-client-instrumentation/src/main/java/co/elastic/otel/openai/wrappers/ChatCompletionEventsHelper.java instrumentation/openai-client-instrumentation/common/src/main/java/co/elastic/otel/openai/wrappers/ChatCompletionEventsHelper.java

+30-29
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222

2323
import com.openai.models.ChatCompletion;
2424
import com.openai.models.ChatCompletionAssistantMessageParam;
25-
import com.openai.models.ChatCompletionContentPart;
2625
import com.openai.models.ChatCompletionContentPartText;
2726
import com.openai.models.ChatCompletionCreateParams;
2827
import com.openai.models.ChatCompletionMessage;
@@ -40,6 +39,7 @@
4039
import java.util.HashMap;
4140
import java.util.List;
4241
import java.util.Map;
42+
import java.util.Objects;
4343
import java.util.stream.Collectors;
4444

4545
public class ChatCompletionEventsHelper {
@@ -54,24 +54,28 @@ public static void emitPromptLogEvents(
5454
if (!settings.emitEvents) {
5555
return;
5656
}
57+
5758
for (ChatCompletionMessageParam msg : request.messages()) {
5859
String eventType;
5960
MapValueBuilder bodyBuilder = new MapValueBuilder();
60-
if (msg.isChatCompletionSystemMessageParam()) {
61-
ChatCompletionSystemMessageParam sysMsg = msg.asChatCompletionSystemMessageParam();
61+
Object concreteMessageParam = ApiAdapter.get().extractConcreteCompletionMessageParam(msg);
62+
if (concreteMessageParam instanceof ChatCompletionSystemMessageParam) {
63+
ChatCompletionSystemMessageParam sysMsg =
64+
(ChatCompletionSystemMessageParam) concreteMessageParam;
6265
eventType = "gen_ai.system.message";
6366
if (settings.captureMessageContent) {
6467
putIfNotEmpty(bodyBuilder, "content", contentToString(sysMsg.content()));
6568
}
66-
} else if (msg.isChatCompletionUserMessageParam()) {
67-
ChatCompletionUserMessageParam userMsg = msg.asChatCompletionUserMessageParam();
69+
} else if (concreteMessageParam instanceof ChatCompletionUserMessageParam) {
70+
ChatCompletionUserMessageParam userMsg =
71+
(ChatCompletionUserMessageParam) concreteMessageParam;
6872
eventType = "gen_ai.user.message";
6973
if (settings.captureMessageContent) {
7074
putIfNotEmpty(bodyBuilder, "content", contentToString(userMsg.content()));
7175
}
72-
} else if (msg.isChatCompletionAssistantMessageParam()) {
76+
} else if (concreteMessageParam instanceof ChatCompletionAssistantMessageParam) {
7377
ChatCompletionAssistantMessageParam assistantMsg =
74-
msg.asChatCompletionAssistantMessageParam();
78+
(ChatCompletionAssistantMessageParam) concreteMessageParam;
7579
eventType = "gen_ai.assistant.message";
7680
if (settings.captureMessageContent) {
7781
assistantMsg
@@ -89,8 +93,9 @@ public static void emitPromptLogEvents(
8993
bodyBuilder.put("tool_calls", Value.of(toolCallsJson));
9094
});
9195
}
92-
} else if (msg.isChatCompletionToolMessageParam()) {
93-
ChatCompletionToolMessageParam toolMsg = msg.asChatCompletionToolMessageParam();
96+
} else if (concreteMessageParam instanceof ChatCompletionToolMessageParam) {
97+
ChatCompletionToolMessageParam toolMsg =
98+
(ChatCompletionToolMessageParam) concreteMessageParam;
9499
eventType = "gen_ai.tool.message";
95100
if (settings.captureMessageContent) {
96101
putIfNotEmpty(bodyBuilder, "content", contentToString(toolMsg.content()));
@@ -110,8 +115,9 @@ private static void putIfNotEmpty(MapValueBuilder bodyBuilder, String key, Strin
110115
}
111116

112117
private static String contentToString(ChatCompletionToolMessageParam.Content content) {
113-
if (content.isTextContent()) {
114-
return content.asTextContent();
118+
String text = ApiAdapter.get().asText(content);
119+
if (text != null) {
120+
return text;
115121
} else if (content.isArrayOfContentParts()) {
116122
return content.asArrayOfContentParts().stream()
117123
.map(ChatCompletionContentPartText::text)
@@ -122,28 +128,23 @@ private static String contentToString(ChatCompletionToolMessageParam.Content con
122128
}
123129

124130
private static String contentToString(ChatCompletionAssistantMessageParam.Content content) {
125-
if (content.isTextContent()) {
126-
return content.asTextContent();
131+
String text = ApiAdapter.get().asText(content);
132+
if (text != null) {
133+
return text;
127134
} else if (content.isArrayOfContentParts()) {
128135
return content.asArrayOfContentParts().stream()
129-
.map(
130-
cnt -> {
131-
if (cnt.isChatCompletionContentPartText()) {
132-
return cnt.asChatCompletionContentPartText().text();
133-
} else if (cnt.isChatCompletionContentPartRefusal()) {
134-
return cnt.asChatCompletionContentPartRefusal().refusal();
135-
}
136-
return "";
137-
})
136+
.map(ApiAdapter.get()::extractTextOrRefusal)
137+
.filter(Objects::nonNull)
138138
.collect(Collectors.joining());
139139
} else {
140140
throw new IllegalStateException("Unhandled content type for " + content);
141141
}
142142
}
143143

144144
private static String contentToString(ChatCompletionSystemMessageParam.Content content) {
145-
if (content.isTextContent()) {
146-
return content.asTextContent();
145+
String text = ApiAdapter.get().asText(content);
146+
if (text != null) {
147+
return text;
147148
} else if (content.isArrayOfContentParts()) {
148149
return content.asArrayOfContentParts().stream()
149150
.map(ChatCompletionContentPartText::text)
@@ -154,13 +155,13 @@ private static String contentToString(ChatCompletionSystemMessageParam.Content c
154155
}
155156

156157
private static String contentToString(ChatCompletionUserMessageParam.Content content) {
157-
if (content.isTextContent()) {
158-
return content.asTextContent();
158+
String text = ApiAdapter.get().asText(content);
159+
if (text != null) {
160+
return text;
159161
} else if (content.isArrayOfContentParts()) {
160162
return content.asArrayOfContentParts().stream()
161-
.filter(ChatCompletionContentPart::isChatCompletionContentPartText)
162-
.map(ChatCompletionContentPart::asChatCompletionContentPartText)
163-
.map(ChatCompletionContentPartText::text)
163+
.map(ApiAdapter.get()::extractText)
164+
.filter(Objects::nonNull)
164165
.collect(Collectors.joining());
165166
} else {
166167
throw new IllegalStateException("Unhandled content type for " + content);
+3-4
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,9 @@ public void onStart(
137137
.responseFormat()
138138
.ifPresent(
139139
val -> {
140-
if (val.isResponseFormatText()) {
141-
attributes.put(
142-
GEN_AI_OPENAI_REQUEST_RESPONSE_FORMAT,
143-
val.asResponseFormatText()._type().toString());
140+
String typeString = ApiAdapter.get().extractType(val);
141+
if (typeString != null) {
142+
attributes.put(GEN_AI_OPENAI_REQUEST_RESPONSE_FORMAT, typeString);
144143
}
145144
});
146145
}

instrumentation/openai-client-instrumentation/build.gradle.kts instrumentation/openai-client-instrumentation/instrumentation-0.14/build.gradle.kts

+4-6
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,18 @@ plugins {
66

77
dependencies {
88
compileOnly(catalog.openaiClient)
9-
testImplementation(catalog.openaiClient)
9+
implementation(project(":instrumentation:openai-client-instrumentation:common"))
1010

11-
testImplementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2")
12-
testImplementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.18.2")
13-
testImplementation("org.slf4j:slf4j-simple:2.0.16")
14-
testImplementation(catalog.wiremock)
11+
testImplementation(catalog.openaiClient)
12+
testImplementation(project(":instrumentation:openai-client-instrumentation:testing-common"))
1513
}
1614

1715
muzzle {
1816
pass {
1917
val openaiClientLib = catalog.openaiClient.get()
2018
group.set(openaiClientLib.group)
2119
module.set(openaiClientLib.name)
22-
versions.set("(,${openaiClientLib.version}]")
20+
versions.set("(0.13.0,${openaiClientLib.version}]")
2321
// no assertInverse.set(true) here because we don't want muzzle to fail for newer releases on our main branch
2422
// instead, renovate will bump the version and failures will be automatically detected on that bump PR
2523
}

0 commit comments

Comments
 (0)