This is an automated email from the ASF dual-hosted git repository.
jamesnetherton pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git
The following commit(s) were added to refs/heads/main by this push:
new 1bd64ee87f Fixes #8858. Add test coverage for openai tool calls,
agentic loop, and audio transcription
1bd64ee87f is described below
commit 1bd64ee87f567f60e960a806e7a6a6c21f7af70e
Author: Jomin Mathew <[email protected]>
AuthorDate: Wed Aug 19 07:06:33 2026 +0100
Fixes #8858. Add test coverage for openai tool calls, agentic loop, and
audio transcription
Add integration tests for recent camel-openai enhancements:
- Tool call detection: verify component surfaces tool call objects
when finish_reason is tool_calls
- Agentic loop with MCP: full round-trip using quarkus-mcp-server
to host an add_numbers tool, WireMock scenarios for the two-step
model interaction
- Audio transcription: send a WAV file to audio-transcription endpoint
and verify the transcribed text
All tests include native mode IT classes.
Co-authored-by: jomin mathew <>
---
integration-tests/openai/pom.xml | 17 +++++
.../component/openai/it/OpenaiResource.java | 24 +++++++
.../quarkus/component/openai/it/OpenaiRoutes.java | 15 ++++
.../quarkus/component/openai/it/OpenaiTools.java | 30 ++++++++
.../quarkus/component/openai/it/OpenaiAudioIT.java | 24 +++++++
.../component/openai/it/OpenaiAudioTest.java | 79 +++++++++++++++++++++
.../component/openai/it/OpenaiChatTest.java | 26 +++++++
.../openai/src/test/resources/audio/test-audio.wav | Bin 0 -> 44 bytes
...tions-b2c3d4e5-0000-0000-0000-000000000001.json | 18 +++++
...step1-c3d4e5f6-0000-0000-0000-000000000001.json | 26 +++++++
...step2-c3d4e5f6-0000-0000-0000-000000000002.json | 28 ++++++++
...calls-a1b2c3d4-0000-0000-0000-000000000001.json | 23 ++++++
pom.xml | 1 +
13 files changed, 311 insertions(+)
diff --git a/integration-tests/openai/pom.xml b/integration-tests/openai/pom.xml
index 2f809d5c00..1f6543125c 100644
--- a/integration-tests/openai/pom.xml
+++ b/integration-tests/openai/pom.xml
@@ -39,6 +39,10 @@
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-file</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-mcp-server</artifactId>
+ </dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-openai</artifactId>
@@ -142,6 +146,19 @@
</exclusion>
</exclusions>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+
<artifactId>camel-quarkus-mcp-server-deployment</artifactId>
+ <version>${project.version}</version>
+ <type>pom</type>
+ <scope>test</scope>
+ <exclusions>
+ <exclusion>
+ <groupId>*</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-openai-deployment</artifactId>
diff --git
a/integration-tests/openai/src/main/java/org/apache/camel/quarkus/component/openai/it/OpenaiResource.java
b/integration-tests/openai/src/main/java/org/apache/camel/quarkus/component/openai/it/OpenaiResource.java
index adcb368036..f1a7409323 100644
---
a/integration-tests/openai/src/main/java/org/apache/camel/quarkus/component/openai/it/OpenaiResource.java
+++
b/integration-tests/openai/src/main/java/org/apache/camel/quarkus/component/openai/it/OpenaiResource.java
@@ -107,6 +107,30 @@ public class OpenaiResource {
return
producerTemplate.requestBody("direct:chatStructuredOutputWithClass",
chatMessageContent, String.class);
}
+ @Path("/chat/toolCalls")
+ @POST
+ @Consumes(MediaType.TEXT_PLAIN)
+ @Produces(MediaType.TEXT_PLAIN)
+ public String chatToolCalls(String chatMessageContent) {
+ return producerTemplate.requestBody("direct:chatToolCalls",
chatMessageContent, String.class);
+ }
+
+ @Path("/chat/agentic")
+ @POST
+ @Consumes(MediaType.TEXT_PLAIN)
+ @Produces(MediaType.TEXT_PLAIN)
+ public String chatAgentic(String chatMessageContent) {
+ return producerTemplate.requestBody("direct:chatAgentic",
chatMessageContent, String.class);
+ }
+
+ @Path("/audio/transcription")
+ @POST
+ @Consumes(MediaType.TEXT_PLAIN)
+ @Produces(MediaType.TEXT_PLAIN)
+ public String audioTranscription(String audioFilePath) {
+ return producerTemplate.requestBody("direct:audioTranscription", new
File(audioFilePath), String.class);
+ }
+
@Path("/chat/results")
@GET
@Consumes(MediaType.TEXT_PLAIN)
diff --git
a/integration-tests/openai/src/main/java/org/apache/camel/quarkus/component/openai/it/OpenaiRoutes.java
b/integration-tests/openai/src/main/java/org/apache/camel/quarkus/component/openai/it/OpenaiRoutes.java
index e510fec295..26672ddce0 100644
---
a/integration-tests/openai/src/main/java/org/apache/camel/quarkus/component/openai/it/OpenaiRoutes.java
+++
b/integration-tests/openai/src/main/java/org/apache/camel/quarkus/component/openai/it/OpenaiRoutes.java
@@ -85,6 +85,21 @@ public class OpenaiRoutes extends RouteBuilder {
.to("openai:chat-completion?conversationMemory=true")
.log("Chat response 2: ${body}");
+ from("direct:chatToolCalls")
+ .to("openai:chat-completion")
+ .log("Tool calls response: ${body}");
+
+ from("direct:chatAgentic")
+ .to("openai:chat-completion?autoToolExecution=true"
+ + "&mcpServer.tools.transportType=streamableHttp"
+ +
"&mcpServer.tools.url=http://localhost:{{quarkus.http.test-port:8081}}"
+ + "&maxToolIterations=3")
+ .log("Agentic response: ${body}");
+
+ from("direct:audioTranscription")
+ .to("openai:audio-transcription?audioModel=whisper-1")
+ .log("Audio transcription response: ${body}");
+
from("direct:embed")
.to("openai-embeddings:embeddings");
diff --git
a/integration-tests/openai/src/main/java/org/apache/camel/quarkus/component/openai/it/OpenaiTools.java
b/integration-tests/openai/src/main/java/org/apache/camel/quarkus/component/openai/it/OpenaiTools.java
new file mode 100644
index 0000000000..dc699ba8d2
--- /dev/null
+++
b/integration-tests/openai/src/main/java/org/apache/camel/quarkus/component/openai/it/OpenaiTools.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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 org.apache.camel.quarkus.component.openai.it;
+
+import io.quarkiverse.mcp.server.Tool;
+import io.quarkiverse.mcp.server.ToolArg;
+
+public class OpenaiTools {
+
+ @Tool(name = "add_numbers", description = "Add two numbers")
+ String add(
+ @ToolArg(description = "First addend") long a,
+ @ToolArg(description = "Second addend") long b) {
+ return String.valueOf(a + b);
+ }
+}
diff --git
a/integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiAudioIT.java
b/integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiAudioIT.java
new file mode 100644
index 0000000000..3c9547a0be
--- /dev/null
+++
b/integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiAudioIT.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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 org.apache.camel.quarkus.component.openai.it;
+
+import io.quarkus.test.junit.QuarkusIntegrationTest;
+
+@QuarkusIntegrationTest
+class OpenaiAudioIT extends OpenaiAudioTest {
+
+}
diff --git
a/integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiAudioTest.java
b/integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiAudioTest.java
new file mode 100644
index 0000000000..e5b22e71c6
--- /dev/null
+++
b/integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiAudioTest.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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 org.apache.camel.quarkus.component.openai.it;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import io.quarkus.test.common.QuarkusTestResource;
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.RestAssured;
+import io.restassured.http.ContentType;
+import org.apache.camel.util.FileUtil;
+import org.jboss.logging.Logger;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+
+import static org.hamcrest.Matchers.containsStringIgnoringCase;
+
+@QuarkusTestResource(OpenaiTestResource.class)
+@QuarkusTest
+class OpenaiAudioTest {
+
+ private static final Logger LOG = Logger.getLogger(OpenaiAudioTest.class);
+
+ @BeforeEach
+ void logTestName(TestInfo testInfo) {
+ LOG.info(String.format("Running OpenaiAudioTest test %s",
testInfo.getDisplayName()));
+ }
+
+ @Test
+ void audioTranscription() throws IOException {
+ Path audioFile = Paths.get("target/test-audio.wav");
+
+ try (InputStream stream =
OpenaiAudioTest.class.getResourceAsStream("/audio/test-audio.wav")) {
+ if (stream == null) {
+ throw new IllegalStateException("Failed loading
test-audio.wav");
+ }
+
+ try (OutputStream out = new FileOutputStream(audioFile.toFile())) {
+ stream.transferTo(out);
+ }
+
+ RestAssured.given()
+ .contentType(ContentType.TEXT)
+ .body(audioFile.toAbsolutePath().toString())
+ .post("/openai/audio/transcription")
+ .then()
+ .statusCode(200)
+ .body(containsStringIgnoringCase("hello"));
+ } finally {
+ if (FileUtil.isWindows()) {
+ audioFile.toFile().deleteOnExit();
+ } else {
+ Files.deleteIfExists(audioFile);
+ }
+ }
+ }
+
+}
diff --git
a/integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiChatTest.java
b/integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiChatTest.java
index 06eda3262d..22eb6b71a5 100644
---
a/integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiChatTest.java
+++
b/integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiChatTest.java
@@ -231,4 +231,30 @@ class OpenaiChatTest {
"price", greaterThan(0.0F));
}
+ @Test
+ void chatToolCalls() {
+ RestAssured.given()
+ .contentType(ContentType.TEXT)
+ .body("What is 2 + 3?")
+ .post("/openai/chat/toolCalls")
+ .then()
+ .statusCode(200)
+ .body(
+ containsStringIgnoringCase("add"),
+ containsStringIgnoringCase("call_abc123"));
+ }
+
+ @Test
+ void chatAgentic() {
+ RestAssured.given()
+ .contentType(ContentType.TEXT)
+ .body("What is 17 + 25?")
+ .post("/openai/chat/agentic")
+ .then()
+ .statusCode(200)
+ .body(
+ containsStringIgnoringCase("42"),
+ containsStringIgnoringCase("sum"));
+ }
+
}
diff --git a/integration-tests/openai/src/test/resources/audio/test-audio.wav
b/integration-tests/openai/src/test/resources/audio/test-audio.wav
new file mode 100644
index 0000000000..d26b87bdbb
Binary files /dev/null and
b/integration-tests/openai/src/test/resources/audio/test-audio.wav differ
diff --git
a/integration-tests/openai/src/test/resources/mappings/audio_transcriptions-b2c3d4e5-0000-0000-0000-000000000001.json
b/integration-tests/openai/src/test/resources/mappings/audio_transcriptions-b2c3d4e5-0000-0000-0000-000000000001.json
new file mode 100644
index 0000000000..41b034cf9b
--- /dev/null
+++
b/integration-tests/openai/src/test/resources/mappings/audio_transcriptions-b2c3d4e5-0000-0000-0000-000000000001.json
@@ -0,0 +1,18 @@
+{
+ "id" : "b2c3d4e5-0000-0000-0000-000000000001",
+ "name" : "audio_transcriptions",
+ "request" : {
+ "url" : "/audio/transcriptions",
+ "method" : "POST"
+ },
+ "response" : {
+ "status" : 200,
+ "body" : "{\n \"text\": \"Hello, this is a test audio
transcription.\"\n}\n",
+ "headers" : {
+ "Content-Type" : "application/json"
+ }
+ },
+ "uuid" : "b2c3d4e5-0000-0000-0000-000000000001",
+ "persistent" : true,
+ "insertionIndex" : 200
+}
diff --git
a/integration-tests/openai/src/test/resources/mappings/chat_completions-agentic-step1-c3d4e5f6-0000-0000-0000-000000000001.json
b/integration-tests/openai/src/test/resources/mappings/chat_completions-agentic-step1-c3d4e5f6-0000-0000-0000-000000000001.json
new file mode 100644
index 0000000000..f568cbbf62
--- /dev/null
+++
b/integration-tests/openai/src/test/resources/mappings/chat_completions-agentic-step1-c3d4e5f6-0000-0000-0000-000000000001.json
@@ -0,0 +1,26 @@
+{
+ "id" : "c3d4e5f6-0000-0000-0000-000000000001",
+ "name" : "chat_completions_agentic_step1",
+ "scenarioName" : "agentic-add-numbers",
+ "requiredScenarioState" : "Started",
+ "newScenarioState" : "tool-executed",
+ "request" : {
+ "url" : "/chat/completions",
+ "method" : "POST",
+ "bodyPatterns" : [ {
+ "equalToJson" : "{\"messages\":[{\"content\":\"What is 17 +
25?\",\"role\":\"user\"}],\"model\":\"gpt-5\"}",
+ "ignoreArrayOrder" : true,
+ "ignoreExtraElements" : true
+ } ]
+ },
+ "response" : {
+ "status" : 200,
+ "body" : "{\n \"id\": \"chatcmpl-agentic-1\",\n \"object\":
\"chat.completion\",\n \"created\": 1770382622,\n \"model\": \"gpt-5\",\n
\"choices\": [\n {\n \"index\": 0,\n \"message\": {\n
\"role\": \"assistant\",\n \"content\": null,\n \"tool_calls\":
[\n {\n \"id\": \"call_agentic_1\",\n \"type\":
\"function\",\n \"function\": {\n \"name\":
\"add_numbers\",\n \"arguments\": \"{ [...]
+ "headers" : {
+ "Content-Type" : "application/json"
+ }
+ },
+ "uuid" : "c3d4e5f6-0000-0000-0000-000000000001",
+ "persistent" : true,
+ "insertionIndex" : 200
+}
diff --git
a/integration-tests/openai/src/test/resources/mappings/chat_completions-agentic-step2-c3d4e5f6-0000-0000-0000-000000000002.json
b/integration-tests/openai/src/test/resources/mappings/chat_completions-agentic-step2-c3d4e5f6-0000-0000-0000-000000000002.json
new file mode 100644
index 0000000000..af64a62927
--- /dev/null
+++
b/integration-tests/openai/src/test/resources/mappings/chat_completions-agentic-step2-c3d4e5f6-0000-0000-0000-000000000002.json
@@ -0,0 +1,28 @@
+{
+ "id" : "c3d4e5f6-0000-0000-0000-000000000002",
+ "name" : "chat_completions_agentic_step2",
+ "scenarioName" : "agentic-add-numbers",
+ "requiredScenarioState" : "tool-executed",
+ "newScenarioState" : "Started",
+ "request" : {
+ "url" : "/chat/completions",
+ "method" : "POST",
+ "bodyPatterns" : [ {
+ "equalToJson" : "{\"messages\":[{\"content\":\"What is 17 +
25?\",\"role\":\"user\"}],\"model\":\"gpt-5\"}",
+ "ignoreArrayOrder" : true,
+ "ignoreExtraElements" : true
+ }, {
+ "matchesJsonPath" : "$.messages[?(@.content == '42')]"
+ } ]
+ },
+ "response" : {
+ "status" : 200,
+ "body" : "{\n \"id\": \"chatcmpl-agentic-2\",\n \"object\":
\"chat.completion\",\n \"created\": 1770382623,\n \"model\": \"gpt-5\",\n
\"choices\": [\n {\n \"index\": 0,\n \"message\": {\n
\"role\": \"assistant\",\n \"content\": \"The sum of 17 and 25 is
42.\",\n \"refusal\": null\n },\n \"finish_reason\":
\"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 80,\n
\"completion_tokens\": 15,\n \"total_tokens\": 95\n }\n}\n",
+ "headers" : {
+ "Content-Type" : "application/json"
+ }
+ },
+ "uuid" : "c3d4e5f6-0000-0000-0000-000000000002",
+ "persistent" : true,
+ "insertionIndex" : 201
+}
diff --git
a/integration-tests/openai/src/test/resources/mappings/chat_completions-tool-calls-a1b2c3d4-0000-0000-0000-000000000001.json
b/integration-tests/openai/src/test/resources/mappings/chat_completions-tool-calls-a1b2c3d4-0000-0000-0000-000000000001.json
new file mode 100644
index 0000000000..235b0237f4
--- /dev/null
+++
b/integration-tests/openai/src/test/resources/mappings/chat_completions-tool-calls-a1b2c3d4-0000-0000-0000-000000000001.json
@@ -0,0 +1,23 @@
+{
+ "id" : "a1b2c3d4-0000-0000-0000-000000000001",
+ "name" : "chat_completions_tool_calls",
+ "request" : {
+ "url" : "/chat/completions",
+ "method" : "POST",
+ "bodyPatterns" : [ {
+ "equalToJson" : "{\"messages\":[{\"content\":\"What is 2 +
3?\",\"role\":\"user\"}],\"model\":\"gpt-5\"}",
+ "ignoreArrayOrder" : true,
+ "ignoreExtraElements" : true
+ } ]
+ },
+ "response" : {
+ "status" : 200,
+ "body" : "{\n \"id\": \"chatcmpl-tool-calls\",\n \"object\":
\"chat.completion\",\n \"created\": 1770382622,\n \"model\":
\"gpt-4o-2025-08-07\",\n \"choices\": [\n {\n \"index\": 0,\n
\"message\": {\n \"role\": \"assistant\",\n \"content\": null,\n
\"tool_calls\": [\n {\n \"id\": \"call_abc123\",\n
\"type\": \"function\",\n \"function\": {\n
\"name\": \"add\",\n \"arguments\": \ [...]
+ "headers" : {
+ "Content-Type" : "application/json"
+ }
+ },
+ "uuid" : "a1b2c3d4-0000-0000-0000-000000000001",
+ "persistent" : true,
+ "insertionIndex" : 100
+}
diff --git a/pom.xml b/pom.xml
index d1e8959087..c534006d40 100644
--- a/pom.xml
+++ b/pom.xml
@@ -599,6 +599,7 @@
<exclude>**/*.proto</exclude>
<exclude>**/*.spec</exclude>
<exclude>**/*.txt</exclude>
+ <exclude>**/*.wav</exclude>
<exclude>**/*.xquery</exclude>
<exclude>**/.factorypath</exclude>
<exclude>**/.pnp.js</exclude>