Skip to content

Commit f74d8b6

Browse files
committed
chat/completion tool call for openai-java <v3.0+
1 parent d6110fc commit f74d8b6

File tree

7 files changed

+197
-3
lines changed

7 files changed

+197
-3
lines changed

dd-java-agent/instrumentation/openai-java/openai-java-1.0/build.gradle

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@ muzzle {
55
pass {
66
group = "com.openai"
77
module = "openai-java"
8-
versions = "[0.45.0,)"
9-
assertInverse = true
8+
versions = "[0.45.0,3)"
9+
// TODO com.openai.models.chat.completions.ChatCompletionMessageToolCall changes in v3
10+
// assertInverse = true
1011
}
1112
}
1213

1314
addTestSuiteForDir('latestDepTest', 'test')
1415

1516
dependencies {
16-
compileOnly group: 'com.openai', name: 'openai-java', version: '1.0.0'
17+
compileOnly group: 'com.openai', name: 'openai-java', version: '0.45.0'
1718
implementation project(':internal-api')
1819

1920
testImplementation group: 'com.openai', name: 'openai-java', version: '0.45.0'

dd-java-agent/instrumentation/openai-java/openai-java-1.0/src/main/java/datadog/trace/instrumentation/openai_java/OpenAiDecorator.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import com.openai.models.chat.completions.ChatCompletionCreateParams;
1111
import com.openai.models.chat.completions.ChatCompletionMessage;
1212
import com.openai.models.chat.completions.ChatCompletionMessageParam;
13+
import com.openai.models.chat.completions.ChatCompletionMessageToolCall;
1314
import com.openai.models.completions.Completion;
1415
import com.openai.models.completions.CompletionCreateParams;
1516
import com.openai.models.completions.CompletionUsage;
@@ -273,6 +274,22 @@ private static LLMObs.LLMMessage llmMessage(ChatCompletion.Choice choice) {
273274
role = String.valueOf(roleOpt.get());
274275
}
275276
String content = msg.content().orElse(null);
277+
278+
Optional<List<ChatCompletionMessageToolCall>> toolCallsOpt = msg.toolCalls();
279+
if (toolCallsOpt.isPresent() && !toolCallsOpt.get().isEmpty()) {
280+
List<LLMObs.ToolCall> toolCalls = new ArrayList<>();
281+
for (ChatCompletionMessageToolCall toolCall : toolCallsOpt.get()) {
282+
LLMObs.ToolCall llmObsToolCall = ToolCallExtractor.getToolCall(toolCall);
283+
if (llmObsToolCall != null) {
284+
toolCalls.add(llmObsToolCall);
285+
}
286+
}
287+
288+
if (!toolCalls.isEmpty()) {
289+
return LLMObs.LLMMessage.from(role, content, toolCalls);
290+
}
291+
}
292+
276293
return LLMObs.LLMMessage.from(role, content);
277294
}
278295

dd-java-agent/instrumentation/openai-java/openai-java-1.0/src/main/java/datadog/trace/instrumentation/openai_java/OpenAiModule.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ public String[] helperClassNames() {
2121
packageName + ".ResponseWrappers$1",
2222
packageName + ".ResponseWrappers$2",
2323
packageName + ".ResponseWrappers$2$1",
24+
packageName + ".ToolCallExtractor",
25+
packageName + ".ToolCallExtractor$1"
2426
};
2527
}
2628

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package datadog.trace.instrumentation.openai_java;
2+
3+
import com.fasterxml.jackson.core.type.TypeReference;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import com.openai.models.chat.completions.ChatCompletionMessageToolCall;
6+
import datadog.trace.api.llmobs.LLMObs;
7+
import java.util.Collections;
8+
import java.util.Map;
9+
import java.util.Optional;
10+
import org.slf4j.Logger;
11+
import org.slf4j.LoggerFactory;
12+
13+
public class ToolCallExtractor {
14+
private static final Logger log = LoggerFactory.getLogger(ToolCallExtractor.class);
15+
private static final ObjectMapper MAPPER = new ObjectMapper();
16+
private static final TypeReference<Map<String, Object>> MAP_TYPE_REF =
17+
new TypeReference<Map<String, Object>>() {};
18+
19+
// TODO add support for v3+
20+
public static LLMObs.ToolCall getToolCall(ChatCompletionMessageToolCall toolCall) {
21+
try {
22+
String toolId = toolCall.id();
23+
ChatCompletionMessageToolCall.Function function = toolCall.function();
24+
String name = function.name();
25+
String argumentsJson = function.arguments();
26+
27+
Map<String, Object> arguments = Collections.singletonMap("value", argumentsJson);
28+
try {
29+
arguments = MAPPER.readValue(argumentsJson, MAP_TYPE_REF);
30+
} catch (Exception e) {
31+
log.debug("Failed to parse tool call arguments as JSON: {}", argumentsJson, e);
32+
}
33+
34+
String type = "function";
35+
Optional<String> typeOpt = toolCall._type().asString();
36+
if (typeOpt.isPresent()) {
37+
type = typeOpt.get();
38+
}
39+
40+
return LLMObs.ToolCall.from(name, type, toolId, arguments);
41+
} catch (Exception e) {
42+
log.debug("Failed to extract tool call information", e);
43+
}
44+
return null;
45+
}
46+
}

dd-java-agent/instrumentation/openai-java/openai-java-1.0/src/test/groovy/ChatCompletionServiceTest.groovy

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,21 @@ class ChatCompletionServiceTest extends OpenAiTest {
118118
assertChatCompletionTrace(true)
119119
}
120120

121+
def "create chat/completion test with tool calls"() {
122+
ChatCompletion resp = runUnderTrace("parent") {
123+
openAiClient.chat().completions().create(chatCompletionCreateParamsWithTools())
124+
}
125+
126+
expect:
127+
resp != null
128+
resp.choices().size() == 1
129+
resp.choices().get(0).message().toolCalls().isPresent()
130+
resp.choices().get(0).message().toolCalls().get().size() == 1
131+
resp.choices().get(0).message().toolCalls().get().get(0).function().name() == "extract_student_info"
132+
and:
133+
assertChatCompletionTrace(false)
134+
}
135+
121136
private void assertChatCompletionTrace(boolean isStreaming) {
122137
assertTraces(1) {
123138
trace(3) {

dd-java-agent/instrumentation/openai-java/openai-java-1.0/src/test/groovy/OpenAiTest.groovy

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@ import com.openai.client.okhttp.OkHttpClient
55
import com.openai.client.okhttp.OpenAIOkHttpClient
66
import com.openai.core.ClientOptions
77
import com.openai.credential.BearerTokenCredential
8+
import com.openai.core.JsonValue
89
import com.openai.models.ChatModel
10+
import com.openai.models.FunctionDefinition
11+
import com.openai.models.FunctionParameters
912
import com.openai.models.chat.completions.ChatCompletionCreateParams
13+
import com.openai.models.chat.completions.ChatCompletionTool
1014
import com.openai.models.completions.CompletionCreateParams
1115
import com.openai.models.embeddings.EmbeddingCreateParams
1216
import com.openai.models.embeddings.EmbeddingModel
@@ -133,5 +137,35 @@ abstract class OpenAiTest extends LlmObsSpecification {
133137
.input("Do not continue the Evan Li slander!")
134138
.build()
135139
}
140+
141+
ChatCompletionCreateParams chatCompletionCreateParamsWithTools() {
142+
ChatCompletionCreateParams.builder()
143+
.model(ChatModel.GPT_4O_MINI)
144+
.addUserMessage("""David Nguyen is a sophomore majoring in computer science at Stanford University and has a GPA of 3.8.
145+
David is an active member of the university's Chess Club and the South Asian Student Association.
146+
He hopes to pursue a career in software engineering after graduating.""")
147+
.addTool(ChatCompletionTool.builder()
148+
.function(FunctionDefinition.builder()
149+
.name("extract_student_info")
150+
.description("Get the student information from the body of the input text")
151+
.parameters(FunctionParameters.builder()
152+
.putAdditionalProperty("type", JsonValue.from("object"))
153+
.putAdditionalProperty("properties", JsonValue.from([
154+
name: [type: "string", description: "Name of the person"],
155+
major: [type: "string", description: "Major subject."],
156+
school: [type: "string", description: "The university name."],
157+
grades: [type: "integer", description: "GPA of the student."],
158+
clubs: [
159+
type: "array",
160+
description: "School clubs for extracurricular activities. ",
161+
items: [type: "string", description: "Name of School Club"]
162+
]
163+
]))
164+
.build())
165+
.build())
166+
.build())
167+
.build()
168+
}
136169
}
137170

171+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
method: POST
2+
path: chat/completions
3+
-- begin request body --
4+
{"messages":[{"content":"David Nguyen is a sophomore majoring in computer science at Stanford University and has a GPA of 3.8.\nDavid is an active member of the university's Chess Club and the South Asian Student Association.\nHe hopes to pursue a career in software engineering after graduating.","role":"user"}],"model":"gpt-4o-mini","tools":[{"function":{"name":"extract_student_info","description":"Get the student information from the body of the input text","parameters":{"type":"object","properties":{"name":{"type":"string","description":"Name of the person"},"major":{"type":"string","description":"Major subject."},"school":{"type":"string","description":"The university name."},"grades":{"type":"integer","description":"GPA of the student."},"clubs":{"type":"array","description":"School clubs for extracurricular activities. ","items":{"type":"string","description":"Name of School Club"}}}}},"type":"function"}]}
5+
-- end request body --
6+
status code: 200
7+
-- begin response headers --
8+
access-control-expose-headers: X-Request-ID
9+
alt-svc: h3=":443"; ma=86400
10+
cf-cache-status: DYNAMIC
11+
cf-ray: 9a239477d81b30b7-SEA
12+
content-type: application/json
13+
date: Fri, 21 Nov 2025 22:21:26 GMT
14+
openai-organization: datadog-staging
15+
openai-processing-ms: 860
16+
openai-project: proj_gt6TQZPRbZfoY2J9AQlEJMpd
17+
openai-version: 2020-10-01
18+
server: cloudflare
19+
strict-transport-security: max-age=31536000; includeSubDomains; preload
20+
x-content-type-options: nosniff
21+
x-envoy-upstream-service-time: 878
22+
x-openai-proxy-wasm: v0.1
23+
x-ratelimit-limit-requests: 30000
24+
x-ratelimit-limit-tokens: 150000000
25+
x-ratelimit-remaining-requests: 29999
26+
x-ratelimit-remaining-tokens: 149999930
27+
x-ratelimit-reset-requests: 2ms
28+
x-ratelimit-reset-tokens: 0s
29+
x-request-id: req_b8e819d6978e4c74ab692e06904acd49
30+
-- end response headers --
31+
-- begin response body --
32+
{
33+
"id": "chatcmpl-CeTnKb8ckpcNU5H2cbnhcYUbwTU4W",
34+
"object": "chat.completion",
35+
"created": 1763763686,
36+
"model": "gpt-4o-mini-2024-07-18",
37+
"choices": [
38+
{
39+
"index": 0,
40+
"message": {
41+
"role": "assistant",
42+
"content": null,
43+
"tool_calls": [
44+
{
45+
"id": "call_LWUWpxL4zvZ6MlyuxN5xD529",
46+
"type": "function",
47+
"function": {
48+
"name": "extract_student_info",
49+
"arguments": "{\"name\":\"David Nguyen\",\"major\":\"computer science\",\"school\":\"Stanford University\",\"grades\":3.8,\"clubs\":[\"Chess Club\",\"South Asian Student Association\"]}"
50+
}
51+
}
52+
],
53+
"refusal": null,
54+
"annotations": []
55+
},
56+
"logprobs": null,
57+
"finish_reason": "tool_calls"
58+
}
59+
],
60+
"usage": {
61+
"prompt_tokens": 152,
62+
"completion_tokens": 44,
63+
"total_tokens": 196,
64+
"prompt_tokens_details": {
65+
"cached_tokens": 0,
66+
"audio_tokens": 0
67+
},
68+
"completion_tokens_details": {
69+
"reasoning_tokens": 0,
70+
"audio_tokens": 0,
71+
"accepted_prediction_tokens": 0,
72+
"rejected_prediction_tokens": 0
73+
}
74+
},
75+
"service_tier": "default",
76+
"system_fingerprint": "fp_560af6e559"
77+
}
78+
79+
-- end response body --

0 commit comments

Comments
 (0)