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 1e70867055 Fix OpenAI integration tests against real and OpenAI 
compatible services
1e70867055 is described below

commit 1e708670552f0a328370c7d1949486134f1d4510
Author: James Netherton <[email protected]>
AuthorDate: Thu Oct 15 16:10:24 2026 +0100

    Fix OpenAI integration tests against real and OpenAI compatible services
---
 integration-tests/openai/README.adoc               |   7 +++
 .../component/openai/it/OpenaiResource.java        |  24 ++++++++--
 .../quarkus/component/openai/it/OpenaiRoutes.java  |  50 +++++++++++--------
 .../src/main/resources/application.properties      |   3 ++
 .../quarkus/component/openai/it/OpenaiAudioIT.java |   2 +
 .../component/openai/it/OpenaiAudioTest.java       |   2 +
 .../component/openai/it/OpenaiChatTest.java        |  53 ++++++++-------------
 .../openai/src/test/resources/audio/test-audio.wav | Bin 44 -> 71964 bytes
 ...step1-c3d4e5f6-0000-0000-0000-000000000001.json |   2 +-
 ...step2-c3d4e5f6-0000-0000-0000-000000000002.json |   2 +-
 ...calls-a1b2c3d4-0000-0000-0000-000000000001.json |   4 +-
 11 files changed, 85 insertions(+), 64 deletions(-)

diff --git a/integration-tests/openai/README.adoc 
b/integration-tests/openai/README.adoc
index f619546f41..4a6d9a71fe 100644
--- a/integration-tests/openai/README.adoc
+++ b/integration-tests/openai/README.adoc
@@ -31,6 +31,13 @@ export OPENAI_CHAT_MODEL=your-chat-model-name
 export OPENAI_EMBEDDING_MODEL=your-embedding-model-name
 ----
 
+If the provider does not support audio transcription, skip the audio tests 
with:
+
+[source,shell]
+----
+export OPENAI_AUDIO_ENABLED=false
+----
+
 If the WireMock stub recordings need updating, then remove the existing files 
from `src/test/resources/mappings` and run tests with either:
 
 System property `-Dwiremock.record=true`
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 4cbe16b732..bb01bac5fb 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
@@ -71,8 +71,18 @@ public class OpenaiResource {
     @Path("/chat/streaming")
     @POST
     @Consumes(MediaType.TEXT_PLAIN)
-    public void chat(String chatMessageContent) {
-        producerTemplate.sendBody("direct:chatStreaming", chatMessageContent);
+    @Produces(MediaType.APPLICATION_JSON)
+    public Map<String, Object> chatStreaming(String chatMessageContent) {
+        Exchange exchange = producerTemplate.request("direct:chatStreaming",
+                e -> e.getMessage().setBody(chatMessageContent));
+        if (exchange.getException() != null) {
+            throw new RuntimeException(exchange.getException());
+        }
+
+        Message message = exchange.getMessage();
+        return Map.of(
+                "content", message.getBody(String.class),
+                "chunks", 
message.getHeader(OpenaiRoutes.STREAMING_CHUNK_COUNT, Integer.class));
     }
 
     @Path("/chat/image")
@@ -118,9 +128,9 @@ public class OpenaiResource {
     @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);
+    @Produces(MediaType.APPLICATION_JSON)
+    public Map<String, Object> chatAgentic(String chatMessageContent) {
+        return requestWithToolCalls("direct:chatAgentic", chatMessageContent);
     }
 
     @Path("/chat/routeTools")
@@ -129,6 +139,10 @@ public class OpenaiResource {
     @Produces(MediaType.APPLICATION_JSON)
     public Map<String, Object> chatRouteTools(@QueryParam("returnDirect") 
boolean returnDirect, String chatMessageContent) {
         String endpointUri = returnDirect ? 
"direct:chatRouteToolsReturnDirect" : "direct:chatRouteTools";
+        return requestWithToolCalls(endpointUri, chatMessageContent);
+    }
+
+    private Map<String, Object> requestWithToolCalls(String endpointUri, 
String chatMessageContent) {
         Exchange exchange = producerTemplate.request(endpointUri,
                 e -> e.getMessage().setBody(chatMessageContent));
         if (exchange.getException() != null) {
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 dab8f1e0f4..08ffc720fe 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
@@ -16,19 +16,15 @@
  */
 package org.apache.camel.quarkus.component.openai.it;
 
-import java.util.List;
-
 import com.openai.models.chat.completions.ChatCompletionChunk;
-import com.openai.models.chat.completions.ChatCompletionChunk.Choice;
 import org.apache.camel.Exchange;
 import org.apache.camel.Message;
 import org.apache.camel.Processor;
 import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.quarkus.component.openai.it.model.Product;
-import org.apache.camel.util.ObjectHelper;
-import org.apache.camel.util.StringHelper;
 
 public class OpenaiRoutes extends RouteBuilder {
+    public static final String STREAMING_CHUNK_COUNT = "streamingChunkCount";
 
     @Override
     public void configure() throws Exception {
@@ -38,29 +34,37 @@ public class OpenaiRoutes extends RouteBuilder {
 
         from("direct:chatStreaming")
                 .to("openai:chat-completion?streaming=true")
-                .split(body())
+                // Providers split tokens into chunks differently, so join the 
streamed content rather than inspecting individual chunks
+                .split(body(), (oldExchange, newExchange) -> {
+                    String content = 
newExchange.getMessage().getBody(String.class);
+                    if (oldExchange == null) {
+                        
newExchange.getMessage().setHeader(STREAMING_CHUNK_COUNT, content.isEmpty() ? 0 
: 1);
+                        return newExchange;
+                    }
+
+                    if (!content.isEmpty()) {
+                        Message message = oldExchange.getMessage();
+                        message.setBody(message.getBody(String.class) + 
content);
+                        message.setHeader(STREAMING_CHUNK_COUNT, 
message.getHeader(STREAMING_CHUNK_COUNT, Integer.class) + 1);
+                    }
+                    return oldExchange;
+                })
                 .streaming()
                 .process(new Processor() {
                     @Override
                     public void process(Exchange exchange) throws Exception {
                         Message message = exchange.getMessage();
                         ChatCompletionChunk chunk = 
message.getBody(ChatCompletionChunk.class);
-                        message.setBody(null);
-
-                        List<Choice> choices = chunk.choices();
-                        if (!choices.isEmpty()) {
-                            Choice choice = choices.get(0);
-                            choice.delta().content().ifPresent(content -> {
-                                if (ObjectHelper.isNotEmpty(content) && 
StringHelper.isDigit(content)) {
-                                    message.setBody(content);
-                                }
-                            });
-                        }
+                        String content = chunk.choices()
+                                .stream()
+                                .findFirst()
+                                .flatMap(choice -> choice.delta().content())
+                                .orElse("");
+                        message.setBody(content);
                     }
                 })
-                .filter().simple("${body} != null")
-                .log("Streaming chat chunk: ${body}")
-                .wireTap("seda:chatStreamingResult");
+                .end()
+                .log("Streaming chat response received in ${header." + 
STREAMING_CHUNK_COUNT + "} chunks: ${body}");
 
         from("direct:chatStructuredOutputWithSchema")
                 
.to("openai:chat-completion?jsonSchema=resource:classpath:schema/product.json")
@@ -80,13 +84,17 @@ public class OpenaiRoutes extends RouteBuilder {
                 .setHeader("CamelOpenAIMaxTokens", constant(10))
                 .to("openai:chat-completion?conversationMemory=true")
                 .log("Chat response 1: ${body}")
+                // reasoning models may spend the whole token budget before 
producing any content
+                .removeHeader("CamelOpenAIMaxTokens")
                 // here we will limit it via instruction, so the model can 
"reorganize" the word "John" to fit in the shorter answer
                 .setBody(constant("What is my name? Respond maximum 5 words."))
                 .to("openai:chat-completion?conversationMemory=true")
                 .log("Chat response 2: ${body}");
 
         from("direct:chatToolCalls")
-                .to("openai:chat-completion")
+                .to("openai:chat-completion?autoToolExecution=false"
+                        + "&mcpServer.tools.transportType=streamableHttp"
+                        + 
"&mcpServer.tools.url=http://localhost:{{quarkus.http.test-port:8081}}";)
                 .log("Tool calls response: ${body}");
 
         from("direct:chatAgentic")
diff --git a/integration-tests/openai/src/main/resources/application.properties 
b/integration-tests/openai/src/main/resources/application.properties
index f376a66421..ce006b27b3 100644
--- a/integration-tests/openai/src/main/resources/application.properties
+++ b/integration-tests/openai/src/main/resources/application.properties
@@ -19,3 +19,6 @@
 quarkus.http.test-timeout=120S
 
 quarkus.native.resources.includes=schema/product.json
+
+# Set to false to skip OpenaiAudioTest when the target endpoint has no audio 
transcription support
+openai.audio.enabled=true
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
index 3c9547a0be..990b88b631 100644
--- 
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
@@ -17,7 +17,9 @@
 package org.apache.camel.quarkus.component.openai.it;
 
 import io.quarkus.test.junit.QuarkusIntegrationTest;
+import io.quarkus.test.junit.condition.EnabledIfConfig;
 
+@EnabledIfConfig(named = "openai.audio.enabled", matches = "true")
 @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
index e5b22e71c6..3f9781fbd4 100644
--- 
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
@@ -26,6 +26,7 @@ import java.nio.file.Paths;
 
 import io.quarkus.test.common.QuarkusTestResource;
 import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.condition.EnabledIfConfig;
 import io.restassured.RestAssured;
 import io.restassured.http.ContentType;
 import org.apache.camel.util.FileUtil;
@@ -36,6 +37,7 @@ import org.junit.jupiter.api.TestInfo;
 
 import static org.hamcrest.Matchers.containsStringIgnoringCase;
 
+@EnabledIfConfig(named = "openai.audio.enabled", matches = "true")
 @QuarkusTestResource(OpenaiTestResource.class)
 @QuarkusTest
 class OpenaiAudioTest {
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 ab7905036c..da56f95238 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
@@ -24,8 +24,9 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.time.Duration;
-import java.util.HashSet;
 import java.util.Set;
+import java.util.regex.MatchResult;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 
@@ -33,7 +34,6 @@ import io.quarkus.test.common.QuarkusTestResource;
 import io.quarkus.test.junit.QuarkusTest;
 import io.restassured.RestAssured;
 import io.restassured.http.ContentType;
-import io.restassured.response.Response;
 import org.apache.camel.util.FileUtil;
 import org.apache.commons.lang3.StringUtils;
 import org.awaitility.Awaitility;
@@ -47,6 +47,7 @@ import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.containsStringIgnoringCase;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.hasItem;
 import static org.hamcrest.Matchers.notNullValue;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
@@ -101,10 +102,7 @@ class OpenaiChatTest {
                     .post("/openai/chat/image")
                     .then()
                     .statusCode(200)
-                    .body(
-                            containsStringIgnoringCase("camel"),
-                            containsStringIgnoringCase("silhouette"),
-                            containsStringIgnoringCase("icon"));
+                    .body(containsStringIgnoringCase("camel"));
         } finally {
             if (FileUtil.isWindows()) {
                 // File may be locked by the Quarkus process, so clean up on 
VM exit
@@ -173,32 +171,21 @@ class OpenaiChatTest {
 
     @Test
     void streamingChat() {
-        // Send the streaming request
-        RestAssured.given()
+        String content = RestAssured.given()
                 .contentType(ContentType.TEXT)
                 .body("Stream the numbers 1 to 9 on a new line each time and 
nothing else.")
                 .post("/openai/chat/streaming")
                 .then()
-                .statusCode(204);
-
-        // Assert the streamed results
-        Set<String> receivedNumbers = new HashSet<>();
-        Awaitility.await()
-                .atMost(Duration.ofMinutes(1))
-                .pollInterval(Duration.ofMillis(200))
-                .until(() -> {
-                    Response response = RestAssured.given()
-                            .queryParam("endpointUri", 
"seda:chatStreamingResult")
-                            .get("/openai/chat/results");
+                .statusCode(200)
+                .body("chunks", greaterThan(1))
+                .extract()
+                .path("content");
 
-                    if (response.getStatusCode() == 200) {
-                        String result = response.getBody().asString();
-                        if (result != null) {
-                            receivedNumbers.add(result.trim());
-                        }
-                    }
-                    return receivedNumbers.size() == 9;
-                });
+        Set<String> receivedNumbers = Pattern.compile("\\d+")
+                .matcher(content)
+                .results()
+                .map(MatchResult::group)
+                .collect(Collectors.toSet());
 
         Set<String> expectedNumbers = IntStream.rangeClosed(1, 9)
                 .mapToObj(String::valueOf)
@@ -237,26 +224,24 @@ class OpenaiChatTest {
     void chatToolCalls() {
         RestAssured.given()
                 .contentType(ContentType.TEXT)
-                .body("What is 2 + 3?")
+                .body("What is 2 + 3? Use the add_numbers tool.")
                 .post("/openai/chat/toolCalls")
                 .then()
                 .statusCode(200)
-                .body(
-                        containsStringIgnoringCase("add"),
-                        containsStringIgnoringCase("call_abc123"));
+                .body(containsStringIgnoringCase("add_numbers"));
     }
 
     @Test
     void chatAgentic() {
         RestAssured.given()
                 .contentType(ContentType.TEXT)
-                .body("What is 17 + 25?")
+                .body("What is 17 + 25? Use the add_numbers tool.")
                 .post("/openai/chat/agentic")
                 .then()
                 .statusCode(200)
                 .body(
-                        containsStringIgnoringCase("42"),
-                        containsStringIgnoringCase("sum"));
+                        "body", containsStringIgnoringCase("42"),
+                        "toolCalls", hasItem("add_numbers"));
     }
 
     @Test
diff --git a/integration-tests/openai/src/test/resources/audio/test-audio.wav 
b/integration-tests/openai/src/test/resources/audio/test-audio.wav
index d26b87bdbb..a7769d28bc 100644
Binary files a/integration-tests/openai/src/test/resources/audio/test-audio.wav 
and b/integration-tests/openai/src/test/resources/audio/test-audio.wav differ
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
index f568cbbf62..2697fa07a8 100644
--- 
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
@@ -8,7 +8,7 @@
     "url" : "/chat/completions",
     "method" : "POST",
     "bodyPatterns" : [ {
-      "equalToJson" : "{\"messages\":[{\"content\":\"What is 17 + 
25?\",\"role\":\"user\"}],\"model\":\"gpt-5\"}",
+      "equalToJson" : "{\"messages\":[{\"content\":\"What is 17 + 25? Use the 
add_numbers tool.\",\"role\":\"user\"}],\"model\":\"gpt-5\"}",
       "ignoreArrayOrder" : true,
       "ignoreExtraElements" : true
     } ]
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
index af64a62927..10c1c2908e 100644
--- 
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
@@ -8,7 +8,7 @@
     "url" : "/chat/completions",
     "method" : "POST",
     "bodyPatterns" : [ {
-      "equalToJson" : "{\"messages\":[{\"content\":\"What is 17 + 
25?\",\"role\":\"user\"}],\"model\":\"gpt-5\"}",
+      "equalToJson" : "{\"messages\":[{\"content\":\"What is 17 + 25? Use the 
add_numbers tool.\",\"role\":\"user\"}],\"model\":\"gpt-5\"}",
       "ignoreArrayOrder" : true,
       "ignoreExtraElements" : true
     }, {
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
index 235b0237f4..72f8e92e54 100644
--- 
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
@@ -5,14 +5,14 @@
     "url" : "/chat/completions",
     "method" : "POST",
     "bodyPatterns" : [ {
-      "equalToJson" : "{\"messages\":[{\"content\":\"What is 2 + 
3?\",\"role\":\"user\"}],\"model\":\"gpt-5\"}",
+      "equalToJson" : "{\"messages\":[{\"content\":\"What is 2 + 3? Use the 
add_numbers tool.\",\"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\": \ [...]
+    "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_numbers\",\n              \"argume [...]
     "headers" : {
       "Content-Type" : "application/json"
     }

Reply via email to