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
commit 3fb6ec5f72c7b53eb72414f436aedf0f83f6dc5e Author: James Netherton <[email protected]> AuthorDate: Fri Jan 30 07:27:12 2026 +0000 Add OpenAI extension Fixes #8203 --- catalog/pom.xml | 17 ++ docs/modules/ROOT/examples/components/openai.yml | 13 ++ docs/modules/ROOT/nav.adoc | 1 + .../ROOT/pages/reference/extensions/openai.adoc | 94 +++++++++ extensions/openai/deployment/pom.xml | 71 +++++++ .../openai/deployment/OpenaiProcessor.java | 121 ++++++++++++ extensions/openai/pom.xml | 39 ++++ extensions/openai/runtime/pom.xml | 109 +++++++++++ extensions/openai/runtime/src/main/doc/usage.adoc | 50 +++++ .../main/resources/META-INF/quarkus-extension.yaml | 33 ++++ extensions/pom.xml | 1 + integration-tests/openai/README.adoc | 36 ++++ integration-tests/openai/pom.xml | 173 +++++++++++++++++ .../component/openai/it/OpenaiResource.java | 132 +++++++++++++ .../quarkus/component/openai/it/OpenaiRoutes.java | 86 +++++++++ .../quarkus/component/openai/it/model/Product.java | 49 +++++ .../src/main/resources/application.properties | 21 ++ .../openai/src/main/resources/schema/product.json | 11 ++ .../quarkus/component/openai/it/OpenaiIT.java | 24 +++ .../quarkus/component/openai/it/OpenaiTest.java | 211 +++++++++++++++++++++ .../component/openai/it/OpenaiTestResource.java | 94 +++++++++ .../openai/src/test/resources/img/camel-logo.png | Bin 0 -> 13008 bytes ...tions-371c2c6c-ace6-42bd-ac5d-6214ba9cdceb.json | 46 +++++ ...tions-6424a591-8168-4708-9e85-18266a335435.json | 43 +++++ ...tions-74689f85-f209-4129-b2c8-5e4d38c8eb54.json | 43 +++++ ...tions-8454f54f-99d3-46a6-b443-720151e4d73a.json | 46 +++++ ...tions-9368b026-c5a6-4dc2-ad3a-d34eaa1c149b.json | 43 +++++ ...tions-e61cd29d-a5ff-42ad-a4de-9975902e0f87.json | 45 +++++ ...tions-f0e11826-78d1-4bf6-988f-f6d31ae30e0b.json | 43 +++++ ...tions-fb0a5dd7-0e7c-4aa5-9274-203c032bfac5.json | 43 +++++ .../test/resources/prompts/whatis-camel-prompt.txt | 1 + integration-tests/pom.xml | 1 + poms/bom/pom.xml | 21 ++ poms/bom/src/main/generated/flattened-full-pom.xml | 21 ++ .../src/main/generated/flattened-reduced-pom.xml | 21 ++ .../generated/flattened-reduced-verbose-pom.xml | 21 ++ tooling/scripts/test-categories.yaml | 1 + 37 files changed, 1825 insertions(+) diff --git a/catalog/pom.xml b/catalog/pom.xml index c08330bf67..e2090b3078 100644 --- a/catalog/pom.xml +++ b/catalog/pom.xml @@ -3113,6 +3113,19 @@ </exclusion> </exclusions> </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-openai</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-openapi-java</artifactId> @@ -4862,6 +4875,10 @@ <skipArtifactIdBase>support-.*</skipArtifactIdBase> <skipArtifactIdBase>integration-tests?-support-.*</skipArtifactIdBase> </skipArtifactIdBases> + <!-- TODO: Remove this with Camel >= 4.18.0 --> + <additionalExtensions> + <additionalExtension>openai</additionalExtension> + </additionalExtensions> </configuration> </execution> </executions> diff --git a/docs/modules/ROOT/examples/components/openai.yml b/docs/modules/ROOT/examples/components/openai.yml new file mode 100644 index 0000000000..6657f35bc2 --- /dev/null +++ b/docs/modules/ROOT/examples/components/openai.yml @@ -0,0 +1,13 @@ +# Do not edit directly! +# This file was generated by camel-quarkus-maven-plugin:update-extension-doc-page +cqArtifactId: camel-quarkus-openai +cqArtifactIdBase: openai +cqNativeSupported: true +cqStatus: Stable +cqDeprecated: false +cqJvmSince: 3.32.0 +cqNativeSince: 3.32.0 +cqCamelPartName: openai +cqCamelPartTitle: OpenAI +cqCamelPartDescription: OpenAI endpoint for chat completion. +cqExtensionPageTitle: OpenAI diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 9dfdc87ff8..f5259c0892 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -251,6 +251,7 @@ *** xref:reference/extensions/observability-services.adoc[Observability Services] *** xref:reference/extensions/olingo4.adoc[Olingo4] *** xref:reference/extensions/once.adoc[Once] +*** xref:reference/extensions/openai.adoc[OpenAI] *** xref:reference/extensions/openapi-java.adoc[OpenAPI Java] *** xref:reference/extensions/opensearch.adoc[OpenSearch] *** xref:reference/extensions/openstack.adoc[OpenStack] diff --git a/docs/modules/ROOT/pages/reference/extensions/openai.adoc b/docs/modules/ROOT/pages/reference/extensions/openai.adoc new file mode 100644 index 0000000000..66a29dbfa3 --- /dev/null +++ b/docs/modules/ROOT/pages/reference/extensions/openai.adoc @@ -0,0 +1,94 @@ +// Do not edit directly! +// This file was generated by camel-quarkus-maven-plugin:update-extension-doc-page +[id="extensions-openai"] += OpenAI +:linkattrs: +:cq-artifact-id: camel-quarkus-openai +:cq-native-supported: true +:cq-status: Stable +:cq-status-deprecation: Stable +:cq-description: OpenAI endpoint for chat completion. +:cq-deprecated: false +:cq-jvm-since: 3.32.0 +:cq-native-since: 3.32.0 + +ifeval::[{doc-show-badges} == true] +[.badges] +[.badge-key]##JVM since##[.badge-supported]##3.32.0## [.badge-key]##Native since##[.badge-supported]##3.32.0## +endif::[] + +OpenAI endpoint for chat completion. + +[id="extensions-openai-maven-coordinates"] +== Maven coordinates + +https://{link-quarkus-code-generator}/?extension-search=camel-quarkus-openai[Create a new project with this extension on {link-quarkus-code-generator}, window="_blank"] + +Or add the coordinates to your existing project: + +[source,xml] +---- +<dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-openai</artifactId> +</dependency> +---- +ifeval::[{doc-show-user-guide-link} == true] +Check the xref:user-guide/index.adoc[User guide] for more information about writing Camel Quarkus applications. +endif::[] + +[id="extensions-openai-usage"] +== Usage +[id="extensions-openai-usage-structured-output-with-output-class-in-native-mode"] +=== Structured output with output class in native mode + +When using structured output with the `outputClass` option in native mode, you must ensure that the target class is registered for reflection. + +This can be done with the `@RegisterForReflection` annotation or configuration property `quarkus.camel.native.reflection.include-patterns`. For example: + +[source,java] +---- +@RegisterForReflection +public class MyStructuredOutputClass { + ... +} +---- + +[source,java] +---- +public class Routes extends RouteBuilder { + @Override + public void configure() { + from("direct:chat") + .to("openai:chat-completion?outputClass=" + MyStructuredOutputClass.class.getName()); + } +} +---- + +[id="extensions-openai-usage-structured-output-with-json-schema-from-classpath-resource-in-native-mode"] +=== Structured output with JSON schema from classpath resource in native mode + +When loading JSON schema classpath resources in native mode, you must ensure the resource is included in the native application. + +For example, given a route like the following. + +[source,java] +---- +public class Routes extends RouteBuilder { + @Override + public void configure() { + from("direct:chat") + .to("openai:chat-completion?jsonSchema=resource:classpath:schemas/mySchema.json"); + } +} +---- + +Add the following to `application.properties`. + +[source,properties] +---- +quarkus.native.resources.includes=schemas/mySchema.json +---- + +Refer to the xref:user-guide/native-mode.adoc#reflection[Native mode] user guide for more information. + diff --git a/extensions/openai/deployment/pom.xml b/extensions/openai/deployment/pom.xml new file mode 100644 index 0000000000..ddcbe4c1e2 --- /dev/null +++ b/extensions/openai/deployment/pom.xml @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + 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. + +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-openai-parent</artifactId> + <version>3.32.0-SNAPSHOT</version> + <relativePath>../pom.xml</relativePath> + </parent> + + <artifactId>camel-quarkus-openai-deployment</artifactId> + <name>Camel Quarkus :: OpenAI :: Deployment</name> + + <dependencies> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-core-deployment</artifactId> + </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-support-swagger-deployment</artifactId> + </dependency> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-jackson-deployment</artifactId> + </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-openai</artifactId> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <configuration> + <annotationProcessorPaths> + <path> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-extension-processor</artifactId> + <version>${quarkus.version}</version> + </path> + </annotationProcessorPaths> + </configuration> + </plugin> + </plugins> + </build> + +</project> diff --git a/extensions/openai/deployment/src/main/java/org/apache/camel/quarkus/component/openai/deployment/OpenaiProcessor.java b/extensions/openai/deployment/src/main/java/org/apache/camel/quarkus/component/openai/deployment/OpenaiProcessor.java new file mode 100644 index 0000000000..61cc735a7e --- /dev/null +++ b/extensions/openai/deployment/src/main/java/org/apache/camel/quarkus/component/openai/deployment/OpenaiProcessor.java @@ -0,0 +1,121 @@ +/* + * 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.deployment; + +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.openai.core.JsonField; +import com.openai.core.JsonValue; +import com.openai.models.chat.completions.ChatCompletion; +import com.openai.models.chat.completions.ChatCompletionChunk; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.Capability; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.IndexDependencyBuildItem; +import io.quarkus.deployment.builditem.RemovedResourceBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageResourcePatternsBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyBuildItem; +import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; +import io.quarkus.jackson.deployment.IgnoreJsonDeserializeClassBuildItem; +import io.quarkus.maven.dependency.ArtifactKey; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.Type; + +class OpenaiProcessor { + private static final String FEATURE = "camel-openai"; + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FEATURE); + } + + @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class) + IndexDependencyBuildItem indexDependencies() { + return new IndexDependencyBuildItem("com.openai", "openai-java-core"); + } + + @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class) + void registerForReflection( + Capabilities capabilities, + CombinedIndexBuildItem combinedIndex, + BuildProducer<IgnoreJsonDeserializeClassBuildItem> ignoredJsonDeserializeClass, + BuildProducer<ReflectiveClassBuildItem> reflectiveClass, + BuildProducer<ReflectiveHierarchyBuildItem> reflectiveHierarchy, + BuildProducer<NativeImageResourcePatternsBuildItem> nativeResourcePatterns) { + + reflectiveHierarchy.produce(ReflectiveHierarchyBuildItem + .builder(Type.create(ChatCompletion.class)).ignoreNested(false) + .build()); + + reflectiveHierarchy.produce(ReflectiveHierarchyBuildItem + .builder(Type.create(ChatCompletionChunk.class)).ignoreNested(false) + .build()); + + // Make quarkus-kotlin optional since not everything it provides is required + if (capabilities.isMissing(Capability.KOTLIN)) { + Stream.of(JsonField.class.getName(), JsonValue.class.getName()) + .map(DotName::createSimple) + .forEach(className -> { + // Suppress quarkus-jackson adding its own reflective config for JsonDeserialize so we can add our own + ignoredJsonDeserializeClass.produce(new IgnoreJsonDeserializeClassBuildItem(className)); + reflectiveHierarchy.produce(ReflectiveHierarchyBuildItem + .builder(Type.create(className, Type.Kind.CLASS)).ignoreNested(false) + .build()); + }); + + Set<String> openAIModelClassNames = combinedIndex.getIndex() + .getKnownClasses() + .stream() + .map(ClassInfo::name) + .map(DotName::toString) + .filter(className -> className.startsWith("com.openai.models")) + .collect(Collectors.toSet()); + + reflectiveClass.produce(ReflectiveClassBuildItem.builder(openAIModelClassNames.toArray(new String[0])) + .fields() + .methods() + .build()); + + nativeResourcePatterns.produce(NativeImageResourcePatternsBuildItem.builder() + .includeGlobs("META-INF/**/*.kotlin_module", + "META-INF/services/kotlin.reflect.*", + "**/*.kotlin_builtins") + .build()); + } + } + + @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class) + RemovedResourceBuildItem excludeNativeImageDirectives() { + // Remove all native-image directives from openai-java-core as it's mostly redundant & inaccurate for Quarkus + return new RemovedResourceBuildItem( + ArtifactKey.fromString("com.openai:openai-java-core"), + Set.of( + "META-INF/native-image/jni-config.json", + "META-INF/native-image/predefined-classes-config.json", + "META-INF/native-image/proxy-config.json", + "META-INF/native-image/reflect-config.json", + "META-INF/native-image/resource-config.json", + "META-INF/native-image/serialization-config.json")); + } +} diff --git a/extensions/openai/pom.xml b/extensions/openai/pom.xml new file mode 100644 index 0000000000..5db4d58503 --- /dev/null +++ b/extensions/openai/pom.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + 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. + +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-extensions</artifactId> + <version>3.32.0-SNAPSHOT</version> + <relativePath>../pom.xml</relativePath> + </parent> + + <artifactId>camel-quarkus-openai-parent</artifactId> + <name>Camel Quarkus :: OpenAI</name> + <packaging>pom</packaging> + + <modules> + <module>deployment</module> + <module>runtime</module> + </modules> +</project> diff --git a/extensions/openai/runtime/pom.xml b/extensions/openai/runtime/pom.xml new file mode 100644 index 0000000000..1f27c1edde --- /dev/null +++ b/extensions/openai/runtime/pom.xml @@ -0,0 +1,109 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + 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. + +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-openai-parent</artifactId> + <version>3.32.0-SNAPSHOT</version> + <relativePath>../pom.xml</relativePath> + </parent> + + <artifactId>camel-quarkus-openai</artifactId> + <name>Camel Quarkus :: OpenAI :: Runtime</name> + <description>OpenAI endpoint for chat completion.</description> + + <properties> + <camel.quarkus.jvmSince>3.32.0</camel.quarkus.jvmSince> + <camel.quarkus.nativeSince>3.32.0</camel.quarkus.nativeSince> + </properties> + + <dependencies> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-core</artifactId> + </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-support-swagger</artifactId> + </dependency> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-jackson</artifactId> + </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-openai</artifactId> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-extension-maven-plugin</artifactId> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <configuration> + <annotationProcessorPaths> + <path> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-extension-processor</artifactId> + <version>${quarkus.version}</version> + </path> + </annotationProcessorPaths> + </configuration> + </plugin> + </plugins> + </build> + + + <profiles> + <profile> + <id>full</id> + <activation> + <property> + <name>!quickly</name> + </property> + </activation> + <build> + <plugins> + <plugin> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-maven-plugin</artifactId> + <executions> + <execution> + <id>update-extension-doc-page</id> + <goals> + <goal>update-extension-doc-page</goal> + </goals> + <phase>process-classes</phase> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> + </profiles> +</project> diff --git a/extensions/openai/runtime/src/main/doc/usage.adoc b/extensions/openai/runtime/src/main/doc/usage.adoc new file mode 100644 index 0000000000..6312e4bbc5 --- /dev/null +++ b/extensions/openai/runtime/src/main/doc/usage.adoc @@ -0,0 +1,50 @@ +=== Structured output with output class in native mode + +When using structured output with the `outputClass` option in native mode, you must ensure that the target class is registered for reflection. + +This can be done with the `@RegisterForReflection` annotation or configuration property `quarkus.camel.native.reflection.include-patterns`. For example: + +[source,java] +---- +@RegisterForReflection +public class MyStructuredOutputClass { + ... +} +---- + +[source,java] +---- +public class Routes extends RouteBuilder { + @Override + public void configure() { + from("direct:chat") + .to("openai:chat-completion?outputClass=" + MyStructuredOutputClass.class.getName()); + } +} +---- + +=== Structured output with JSON schema from classpath resource in native mode + +When loading JSON schema classpath resources in native mode, you must ensure the resource is included in the native application. + +For example, given a route like the following. + +[source,java] +---- +public class Routes extends RouteBuilder { + @Override + public void configure() { + from("direct:chat") + .to("openai:chat-completion?jsonSchema=resource:classpath:schemas/mySchema.json"); + } +} +---- + +Add the following to `application.properties`. + +[source,properties] +---- +quarkus.native.resources.includes=schemas/mySchema.json +---- + +Refer to the xref:user-guide/native-mode.adoc#reflection[Native mode] user guide for more information. diff --git a/extensions/openai/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/openai/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000..96712fbf2a --- /dev/null +++ b/extensions/openai/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,33 @@ +# +# 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. +# + +# This is a generated file. Do not edit directly! +# To re-generate, run the following command from the top level directory: +# +# mvn -N cq:update-quarkus-metadata +# +--- +name: "Camel OpenAI" +description: "OpenAI endpoint for chat completion" +metadata: + icon-url: "https://raw.githubusercontent.com/apache/camel-website/main/antora-ui-camel/src/img/logo-d.svg" + sponsor: "Apache Software Foundation" + guide: "https://camel.apache.org/camel-quarkus/latest/reference/extensions/openai.html" + categories: + - "integration" + status: + - "stable" diff --git a/extensions/pom.xml b/extensions/pom.xml index 633ea293de..7816c06180 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -219,6 +219,7 @@ <module>ognl</module> <module>olingo4</module> <module>once</module> + <module>openai</module> <module>openapi-java</module> <module>openstack</module> <module>opentelemetry</module> diff --git a/integration-tests/openai/README.adoc b/integration-tests/openai/README.adoc new file mode 100644 index 0000000000..97bc3d5edf --- /dev/null +++ b/integration-tests/openai/README.adoc @@ -0,0 +1,36 @@ +== Camel Quarkus OpenAI Integration Tests + +By default, the OpenAI integration tests use WireMock to stub the API interactions. + +=== Testing with OpenAI + +To run the `camel-quarkus-openai` integration tests against the real OpenAI APIs, you must first create a OpenAI developer account https://developers.openai.com/. +Or if you are already registered for ChatGPT, then you can use your account credentials to log in to the developer area. + +With access to the developer area you can create a new project together with an API key for use in the required environment variables as shown below. + +[source,shell] +---- +export OPENAI_API_KEY=your-openai-api-key +---- + +This will test with model `gpt-5` against the default API endpoint of https://api.openai.com/v1. + +=== Testing with alternate OpenAI compatible providers + +Alternatively, you can test against any OpenAI compatible API by configuring the endpoint URL. + +[source,shell] +---- +export OPENAI_API_KEY=fake-key +export OPENAI_BASE_URL=https://you-api-endpoint/v1 +export OPENAI_MODEL=your-model-name +---- + +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` + +Or + +Set environment variable `WIREMOCK_RECORD=true` diff --git a/integration-tests/openai/pom.xml b/integration-tests/openai/pom.xml new file mode 100644 index 0000000000..aa5252c102 --- /dev/null +++ b/integration-tests/openai/pom.xml @@ -0,0 +1,173 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + 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. + +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-build-parent-it</artifactId> + <version>3.32.0-SNAPSHOT</version> + <relativePath>../../poms/build-parent-it/pom.xml</relativePath> + </parent> + + <artifactId>camel-quarkus-integration-test-openai</artifactId> + <name>Camel Quarkus :: Integration Tests :: OpenAI</name> + <description>Integration tests for Camel Quarkus OpenAI extension</description> + + <dependencies> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-direct</artifactId> + </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-file</artifactId> + </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-openai</artifactId> + </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-seda</artifactId> + </dependency> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-resteasy</artifactId> + </dependency> + + <!-- test dependencies --> + <dependency> + <groupId>io.quarkus</groupId> + <artifactId>quarkus-junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>io.rest-assured</groupId> + <artifactId>rest-assured</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.awaitility</groupId> + <artifactId>awaitility</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-integration-wiremock-support</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + <profiles> + <profile> + <id>native</id> + <activation> + <property> + <name>native</name> + </property> + </activation> + <properties> + <quarkus.native.enabled>true</quarkus.native.enabled> + </properties> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-failsafe-plugin</artifactId> + <executions> + <execution> + <goals> + <goal>integration-test</goal> + <goal>verify</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> + <profile> + <id>virtualDependencies</id> + <activation> + <property> + <name>!noVirtualDependencies</name> + </property> + </activation> + <dependencies> + <!-- The following dependencies guarantee that this module is built after them. You can update them by running `mvn process-resources -Pformat -N` from the source tree root directory --> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-direct-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-file-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> + <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-seda-deployment</artifactId> + <version>${project.version}</version> + <type>pom</type> + <scope>test</scope> + <exclusions> + <exclusion> + <groupId>*</groupId> + <artifactId>*</artifactId> + </exclusion> + </exclusions> + </dependency> + </dependencies> + </profile> + </profiles> + +</project> 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 new file mode 100644 index 0000000000..4b69253890 --- /dev/null +++ b/integration-tests/openai/src/main/java/org/apache/camel/quarkus/component/openai/it/OpenaiResource.java @@ -0,0 +1,132 @@ +/* + * 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.File; +import java.util.HashMap; +import java.util.Map; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.MediaType; +import org.apache.camel.CamelContext; +import org.apache.camel.ConsumerTemplate; +import org.apache.camel.ProducerTemplate; +import org.apache.camel.component.openai.OpenAIConstants; +import org.apache.camel.util.ObjectHelper; + +@Path("/openai") +@ApplicationScoped +public class OpenaiResource { + @Inject + ProducerTemplate producerTemplate; + + @Inject + ConsumerTemplate consumerTemplate; + + @Inject + CamelContext context; + + @Path("/chat") + @POST + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public String chat( + @QueryParam("userMessage") String userMessage, + String chatMessageContent) { + return doChat(userMessage, chatMessageContent); + } + + @Path("/chat/streaming") + @POST + @Consumes(MediaType.TEXT_PLAIN) + public void chat(String chatMessageContent) { + producerTemplate.sendBody("direct:chatStreaming", chatMessageContent); + } + + @Path("/chat/image") + @POST + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public String chatWithImage(@QueryParam("userMessage") String userMessage, String imagePath) { + return doChat(userMessage, new File(imagePath)); + } + + @Path("/chat/memory") + @POST + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public String chatWithMemory(String chatMessageContent) { + return producerTemplate.requestBody("direct:chatWithMemory", chatMessageContent, String.class); + } + + @Path("/chat/structured/schema") + @POST + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.APPLICATION_JSON) + public String chatWithStructuredOutputWithSchema(String chatMessageContent) { + return producerTemplate.requestBody("direct:chatStructuredOutputWithSchema", chatMessageContent, String.class); + } + + @Path("/chat/structured/class") + @POST + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.APPLICATION_JSON) + public String chatWithStructuredOutputWithClass(String chatMessageContent) { + return producerTemplate.requestBody("direct:chatStructuredOutputWithClass", chatMessageContent, String.class); + } + + @Path("/chat/results") + @GET + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + public String chatResults(@QueryParam("endpointUri") String endpointUri) { + return consumerTemplate.receiveBody(endpointUri, 10000, String.class); + } + + @Path("/routes/{routeId}/{operation}") + @POST + public void routeOperations( + @PathParam("routeId") String routeId, + @PathParam("operation") String operation) throws Exception { + + if (operation.equals("start")) { + context.getRouteController().startRoute(routeId); + } else if (operation.equals("stop")) { + context.getRouteController().stopRoute(routeId); + } else { + throw new IllegalArgumentException("Unknown operation: " + operation); + } + } + + private String doChat(String userMessage, Object chatMessageContent) { + Map<String, Object> headers = new HashMap<>(); + + if (ObjectHelper.isNotEmpty(userMessage)) { + headers.put(OpenAIConstants.USER_MESSAGE, userMessage); + } + + return producerTemplate.requestBodyAndHeaders("direct:chat", chatMessageContent, headers, String.class); + } +} 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 new file mode 100644 index 0000000000..a8eb914d21 --- /dev/null +++ b/integration-tests/openai/src/main/java/org/apache/camel/quarkus/component/openai/it/OpenaiRoutes.java @@ -0,0 +1,86 @@ +/* + * 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.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; + +public class OpenaiRoutes extends RouteBuilder { + + @Override + public void configure() throws Exception { + from("direct:chat") + .to("openai:chat-completion") + .log("Chat response: ${body}"); + + from("direct:chatStreaming") + .to("openai:chat-completion?streaming=true") + .split(body()) + .streaming() + .process(new Processor() { + @Override + public void process(Exchange exchange) throws Exception { + Message message = exchange.getMessage(); + ChatCompletionChunk chunk = message.getBody(ChatCompletionChunk.class); + List<Choice> choices = chunk.choices(); + if (!choices.isEmpty()) { + Choice choice = choices.get(0); + choice.delta().content().ifPresent(content -> { + if (ObjectHelper.isNotEmpty(content) && !content.equals("\n")) { + message.setBody(content); + } else { + message.setBody(null); + } + }); + } else { + message.setBody(null); + } + } + }) + .filter().simple("${body} != null") + .log("Streaming chat chunk: ${body}") + .wireTap("seda:chatStreamingResult"); + + from("direct:chatStructuredOutputWithSchema") + .to("openai:chat-completion?jsonSchema=resource:classpath:schema/product.json") + .log("Structured output with schema chat response: ${body}"); + + from("direct:chatStructuredOutputWithClass") + .to("openai:chat-completion?outputClass=" + Product.class.getName()) + .log("Structured output with class chat response: ${body}"); + + from("file:target/prompts?noop=true").routeId("file-prompts").autoStartup(false) + .to("openai:chat-completion") + .log("File prompt chat response: ${body}") + .to("seda:filePromptResults"); + + from("direct:chatWithMemory") + .to("openai:chat-completion?conversationMemory=true") + .log("Chat response 1: ${body}") + .setBody(constant("What is my Camel species?")) + .to("openai:chat-completion?conversationMemory=true") + .log("Chat response 2: ${body}"); + } +} diff --git a/integration-tests/openai/src/main/java/org/apache/camel/quarkus/component/openai/it/model/Product.java b/integration-tests/openai/src/main/java/org/apache/camel/quarkus/component/openai/it/model/Product.java new file mode 100644 index 0000000000..be10f8848f --- /dev/null +++ b/integration-tests/openai/src/main/java/org/apache/camel/quarkus/component/openai/it/model/Product.java @@ -0,0 +1,49 @@ +/* + * 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.model; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +@RegisterForReflection +public class Product { + private String name; + private Double price; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Double getPrice() { + return price; + } + + public void setPrice(Double price) { + this.price = price; + } + + @Override + public String toString() { + return "{" + + "\"name\":\"" + name + "\"," + + "\"price\":" + price + + "}"; + } +} diff --git a/integration-tests/openai/src/main/resources/application.properties b/integration-tests/openai/src/main/resources/application.properties new file mode 100644 index 0000000000..f376a66421 --- /dev/null +++ b/integration-tests/openai/src/main/resources/application.properties @@ -0,0 +1,21 @@ +## --------------------------------------------------------------------------- +## 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. +## --------------------------------------------------------------------------- + +# The LLM may time a long time to respond +quarkus.http.test-timeout=120S + +quarkus.native.resources.includes=schema/product.json diff --git a/integration-tests/openai/src/main/resources/schema/product.json b/integration-tests/openai/src/main/resources/schema/product.json new file mode 100644 index 0000000000..3ec778a8eb --- /dev/null +++ b/integration-tests/openai/src/main/resources/schema/product.json @@ -0,0 +1,11 @@ +{ + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "price": { + "type": "number" + } + } +} diff --git a/integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiIT.java b/integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiIT.java new file mode 100644 index 0000000000..b30598ff66 --- /dev/null +++ b/integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiIT.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 OpenaiIT extends OpenaiTest { + +} diff --git a/integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiTest.java b/integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiTest.java new file mode 100644 index 0000000000..464c22ede1 --- /dev/null +++ b/integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiTest.java @@ -0,0 +1,211 @@ +/* + * 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 java.time.Duration; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +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.awaitility.Awaitility; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.Matchers.containsStringIgnoringCase; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@QuarkusTestResource(OpenaiTestResource.class) +@QuarkusTest +class OpenaiTest { + @Test + void simpleChat() { + RestAssured.given() + .contentType(ContentType.TEXT) + .body("In one sentence, what is Apache Camel?") + .post("/openai/chat") + .then() + .statusCode(200) + .body(containsStringIgnoringCase("integration framework")); + } + + @Test + void chatWithImage() throws IOException { + Path path = Paths.get("target/camel-log.png"); + + try (InputStream stream = OpenaiTest.class.getResourceAsStream("/img/camel-logo.png")) { + if (stream == null) { + throw new IllegalStateException("Failed loading camel-logo.png"); + } + + try (OutputStream out = new FileOutputStream(path.toFile())) { + stream.transferTo(out); + } + + RestAssured.given() + .queryParam("userMessage", "Describe what you see in this image") + .body("target/camel-log.png") + .post("/openai/chat/image") + .then() + .statusCode(200) + .body( + containsStringIgnoringCase("camel"), + containsStringIgnoringCase("silhouette"), + containsStringIgnoringCase("logo")); + } finally { + if (FileUtil.isWindows()) { + // File may be locked by the Quarkus process, so clean up on VM exit + path.toFile().deleteOnExit(); + } else { + Files.deleteIfExists(path); + } + } + } + + @Test + void chatInitiatedFromFileConsumer() throws IOException { + Path prompts = Paths.get("target/prompts"); + Path prompt = prompts.resolve("whatis-camel-prompt.txt"); + Files.createDirectories(prompts); + + try (InputStream stream = OpenaiTest.class.getResourceAsStream("/prompts/whatis-camel-prompt.txt")) { + if (stream == null) { + throw new IllegalStateException("Failed loading whatis-camel-prompt.txt"); + } + + try (OutputStream out = new FileOutputStream(prompt.toFile())) { + stream.transferTo(out); + } + + // Start the file-prompts route + RestAssured.given() + .post("/openai/routes/file-prompts/start") + .then() + .statusCode(204); + + Awaitility.await().pollDelay(Duration.ofSeconds(1)).atMost(Duration.ofMinutes(1)).untilAsserted(() -> { + RestAssured.given() + .queryParam("endpointUri", "seda:filePromptResults") + .get("/openai/chat/results") + .then() + .statusCode(200) + .body(containsStringIgnoringCase("integration framework")); + }); + } finally { + // Stop the file-prompts route + RestAssured.given() + .post("/openai/routes/file-prompts/stop") + .then() + .statusCode(204); + + if (FileUtil.isWindows()) { + // File may be locked by the Quarkus process, so clean up on VM exit + prompt.toFile().deleteOnExit(); + } else { + Files.deleteIfExists(prompt); + } + } + } + + @Test + void chatWithMemory() { + RestAssured.given() + .contentType(ContentType.TEXT) + .body("I am a Camel and my species is Camelus Dromedarius.") + .post("/openai/chat/memory") + .then() + .statusCode(200) + .body(containsStringIgnoringCase("Camelus Dromedarius")); + } + + @Test + void streamingChat() { + // Send the streaming request + RestAssured.given() + .contentType(ContentType.TEXT) + .body("Stream the numbers 1 to 10 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"); + + if (response.getStatusCode() == 200) { + String result = response.getBody().asString(); + if (result != null && !result.isBlank()) { + receivedNumbers.add(result.trim()); + } + } + return receivedNumbers.size() >= 10; + }); + + Set<String> expectedNumbers = IntStream.rangeClosed(1, 10) + .mapToObj(String::valueOf) + .collect(Collectors.toSet()); + + assertEquals(expectedNumbers, receivedNumbers); + } + + @Test + void structuredOutputWithSchemaResource() { + RestAssured.given() + .contentType(ContentType.TEXT) + .body("Create an example product description.") + .post("/openai/chat/structured/schema") + .then() + .statusCode(200) + .body( + "name", notNullValue(), + "price", greaterThan(0.0F)); + } + + @Test + void structuredOutputWithOutputClass() { + RestAssured.given() + .contentType(ContentType.TEXT) + .body("Create an example product for a product named 'Bluetooth Headphones'.") + .post("/openai/chat/structured/class") + .then() + .statusCode(200) + .body( + "name", is("Bluetooth Headphones"), + "price", greaterThan(0.0F)); + } +} diff --git a/integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiTestResource.java b/integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiTestResource.java new file mode 100644 index 0000000000..550e3fb992 --- /dev/null +++ b/integration-tests/openai/src/test/java/org/apache/camel/quarkus/component/openai/it/OpenaiTestResource.java @@ -0,0 +1,94 @@ +/* + * 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.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; +import com.github.tomakehurst.wiremock.common.FileSource; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import com.github.tomakehurst.wiremock.extension.Parameters; +import com.github.tomakehurst.wiremock.extension.StubMappingTransformer; +import com.github.tomakehurst.wiremock.http.HttpHeader; +import com.github.tomakehurst.wiremock.http.HttpHeaders; +import com.github.tomakehurst.wiremock.http.ResponseDefinition; +import com.github.tomakehurst.wiremock.stubbing.StubMapping; +import com.openai.core.ClientOptions; +import org.apache.camel.quarkus.test.wiremock.WireMockTestResourceLifecycleManager; +import org.apache.camel.util.ObjectHelper; + +public class OpenaiTestResource extends WireMockTestResourceLifecycleManager { + private static final String OPENAI_API_URL = ClientOptions.PRODUCTION_URL; + private static final String OPENAI_ENV_API_KEY = "OPENAI_API_KEY"; + private static final String OPENAI_ENV_BASE_URL = "OPENAI_BASE_URL"; + private static final String OPENAI_ENV_MODEL = "OPENAI_MODEL"; + + @Override + public Map<String, String> start() { + Map<String, String> configuration = super.start(); + String wiremockUrl = configuration.get("wiremock.url"); + if (ObjectHelper.isNotEmpty(wiremockUrl)) { + configuration.put("camel.component.openai.baseUrl", wiremockUrl); + } else { + configuration.put("camel.component.openai.baseUrl", OPENAI_API_URL); + } + + configuration.put("camel.component.openai.model", envOrDefault(OPENAI_ENV_MODEL, "gpt-5")); + configuration.put("camel.component.openai.apiKey", envOrDefault(OPENAI_ENV_API_KEY, "test-key")); + return configuration; + } + + @Override + protected String getRecordTargetBaseUrl() { + return envOrDefault(OPENAI_ENV_BASE_URL, OPENAI_API_URL); + } + + @Override + protected boolean isMockingEnabled() { + return !envVarsPresent(OPENAI_ENV_API_KEY); + } + + @Override + protected void customizeWiremockConfiguration(WireMockConfiguration config) { + // Removes openai-project header from saved mappings + config.extensions(new StubMappingTransformer() { + @Override + public String getName() { + return "camel-quarkus-openai-transformer"; + } + + @Override + public StubMapping transform(StubMapping stubMapping, FileSource fileSource, Parameters parameters) { + ResponseDefinition original = stubMapping.getResponse(); + HttpHeaders originalHeaders = original.getHeaders(); + List<HttpHeader> filteredHeaders = originalHeaders.all().stream() + .filter(h -> !h.keyEquals("openai-project")) + .collect(Collectors.toList()); + + HttpHeaders newHeaders = new HttpHeaders(filteredHeaders); + ResponseDefinition newResponse = ResponseDefinitionBuilder.like(original) + .withHeaders(newHeaders) + .build(); + + stubMapping.setResponse(newResponse); + return stubMapping; + } + }); + } +} diff --git a/integration-tests/openai/src/test/resources/img/camel-logo.png b/integration-tests/openai/src/test/resources/img/camel-logo.png new file mode 100644 index 0000000000..e5d56708dc Binary files /dev/null and b/integration-tests/openai/src/test/resources/img/camel-logo.png differ diff --git a/integration-tests/openai/src/test/resources/mappings/chat_completions-371c2c6c-ace6-42bd-ac5d-6214ba9cdceb.json b/integration-tests/openai/src/test/resources/mappings/chat_completions-371c2c6c-ace6-42bd-ac5d-6214ba9cdceb.json new file mode 100644 index 0000000000..3c9b3bb9fb --- /dev/null +++ b/integration-tests/openai/src/test/resources/mappings/chat_completions-371c2c6c-ace6-42bd-ac5d-6214ba9cdceb.json @@ -0,0 +1,46 @@ +{ + "id" : "371c2c6c-ace6-42bd-ac5d-6214ba9cdceb", + "name" : "chat_completions", + "request" : { + "url" : "/chat/completions", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{\"messages\":[{\"content\":\"In one sentence, what is Apache Camel?\",\"role\":\"user\"}],\"model\":\"gpt-5\",\"temperature\":1.0}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : true + } ] + }, + "response" : { + "status" : 200, + "body" : "{\n \"id\": \"chatcmpl-D2add3YmiXdwEboHVOpiUIdoDfjbC\",\n \"object\": \"chat.completion\",\n \"created\": 1769509865,\n \"model\": \"gpt-5-2025-08-07\",\n \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": \"assistant\",\n \"content\": \"Apache Camel is an open-source integration framework that implements Enterprise Integration Patterns to route, transform, and mediate messages between systems using a wide range of connectors and [...] + "headers" : { + "x-request-id" : "req_9815d7db45994ed687d0e50c1d2c280e", + "x-ratelimit-limit-tokens" : "500000", + "openai-organization" : "user-nvrq0gduw4i0ooapnshoh6gw", + "CF-RAY" : "9c479411eb3333fe-LHR", + "Server" : "cloudflare", + "X-Content-Type-Options" : "nosniff", + "x-ratelimit-reset-requests" : "120ms", + "x-openai-proxy-wasm" : "v0.1", + "x-ratelimit-remaining-tokens" : "499988", + "cf-cache-status" : "DYNAMIC", + "x-ratelimit-remaining-requests" : "499", + "Date" : "Tue, 27 Jan 2026 10:31:08 GMT", + "x-ratelimit-reset-tokens" : "1ms", + "access-control-expose-headers" : "X-Request-ID", + "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", + "x-ratelimit-limit-requests" : "500", + "Set-Cookie" : [ "__cf_bm=T9eL.pfXGoicZCWWEh3JoIymqHyonP3NQT3ybqP5uJM-1769509868-1.0.1.1-g2IwAYQcOxKWsbEzb7JAeW.eqCmgd9b5OPcIIR2gpybdiMZ9hfcWbci81tG3Y9OKz95.XJYEwHI7EXofta4vzn3Dqb6HwvtJmtUWbtCKaCs; path=/; expires=Tue, 27-Jan-26 11:01:08 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None", "_cfuvid=jlzIJcuAOE7EBxWLG.y39y8a2dsoeSRKDgnVZOcJbQc-1769509868234-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" ], + "openai-version" : "2020-10-01", + "openai-processing-ms" : "2648", + "alt-svc" : "h3=\":443\"; ma=86400", + "Content-Type" : "application/json" + } + }, + "uuid" : "371c2c6c-ace6-42bd-ac5d-6214ba9cdceb", + "persistent" : true, + "scenarioName" : "scenario-1-chat-completions", + "requiredScenarioState" : "Started", + "newScenarioState" : "scenario-1-chat-completions-2", + "insertionIndex" : 6 +} \ No newline at end of file diff --git a/integration-tests/openai/src/test/resources/mappings/chat_completions-6424a591-8168-4708-9e85-18266a335435.json b/integration-tests/openai/src/test/resources/mappings/chat_completions-6424a591-8168-4708-9e85-18266a335435.json new file mode 100644 index 0000000000..1719fedf5a --- /dev/null +++ b/integration-tests/openai/src/test/resources/mappings/chat_completions-6424a591-8168-4708-9e85-18266a335435.json @@ -0,0 +1,43 @@ +{ + "id" : "6424a591-8168-4708-9e85-18266a335435", + "name" : "chat_completions", + "request" : { + "url" : "/chat/completions", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{\"messages\":[{\"content\":\"Stream the numbers 1 to 10 on a new line each time and nothing else.\",\"role\":\"user\"}],\"model\":\"gpt-5\",\"temperature\":1.0,\"stream\":true}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : true + } ] + }, + "response" : { + "status" : 200, + "body" : "data: {\"id\":\"chatcmpl-D2aeAjgif1gBVAPu9GH45QUEeM595\",\"object\":\"chat.completion.chunk\",\"created\":1769509898,\"model\":\"gpt-5-2025-08-07\",\"service_tier\":\"default\",\"system_fingerprint\":null,\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null},\"finish_reason\":null}],\"obfuscation\":\"OawGDTE\"}\n\ndata: {\"id\":\"chatcmpl-D2aeAjgif1gBVAPu9GH45QUEeM595\",\"object\":\"chat.completion.chunk\",\"created\":1769509898,\"mo [...] + "headers" : { + "x-request-id" : "req_999fb5204cd840dda154e7ce979d509f", + "x-ratelimit-limit-tokens" : "500000", + "openai-organization" : "user-nvrq0gduw4i0ooapnshoh6gw", + "CF-RAY" : "9c4794deaab51179-LHR", + "Server" : "cloudflare", + "X-Content-Type-Options" : "nosniff", + "x-ratelimit-reset-requests" : "120ms", + "x-openai-proxy-wasm" : "v0.1", + "x-ratelimit-remaining-tokens" : "499981", + "cf-cache-status" : "DYNAMIC", + "x-ratelimit-remaining-requests" : "499", + "Date" : "Tue, 27 Jan 2026 10:31:39 GMT", + "x-ratelimit-reset-tokens" : "2ms", + "access-control-expose-headers" : "X-Request-ID", + "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", + "x-ratelimit-limit-requests" : "500", + "Set-Cookie" : [ "__cf_bm=FmQ83ZDP77tCY4DWrYu6kHtDgQAtDbHaX9X39W3RA5I-1769509899-1.0.1.1-0gvxDpfDmIUPFkcyK2r6Vw0PR6ZThH5aQb0z__jSaAy4LYoesF7vy_Isp7uHr7y_tlwKjXgvfDAGGizzfdwx5Yjj2Tf05D6NVj.emhrSwfc; path=/; expires=Tue, 27-Jan-26 11:01:39 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None", "_cfuvid=vHD0D_Dm6mz2b1mEwlY0Ys8AbDU.LOG17CtwdT860Xc-1769509899926-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" ], + "openai-version" : "2020-10-01", + "openai-processing-ms" : "1749", + "alt-svc" : "h3=\":443\"; ma=86400", + "Content-Type" : "text/event-stream; charset=utf-8" + } + }, + "uuid" : "6424a591-8168-4708-9e85-18266a335435", + "persistent" : true, + "insertionIndex" : 2 +} \ No newline at end of file diff --git a/integration-tests/openai/src/test/resources/mappings/chat_completions-74689f85-f209-4129-b2c8-5e4d38c8eb54.json b/integration-tests/openai/src/test/resources/mappings/chat_completions-74689f85-f209-4129-b2c8-5e4d38c8eb54.json new file mode 100644 index 0000000000..7d93275d04 --- /dev/null +++ b/integration-tests/openai/src/test/resources/mappings/chat_completions-74689f85-f209-4129-b2c8-5e4d38c8eb54.json @@ -0,0 +1,43 @@ +{ + "id" : "74689f85-f209-4129-b2c8-5e4d38c8eb54", + "name" : "chat_completions", + "request" : { + "url" : "/chat/completions", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{\"messages\":[{\"content\":\"I am a Camel and my species is Camelus Dromedarius.\",\"role\":\"user\"}],\"model\":\"gpt-5\",\"temperature\":1.0}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : true + } ] + }, + "response" : { + "status" : 200, + "body" : "{\n \"id\": \"chatcmpl-D2acuKCizPKhRtRg7za2XobaULzIZ\",\n \"object\": \"chat.completion\",\n \"created\": 1769509820,\n \"model\": \"gpt-5-2025-08-07\",\n \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": \"assistant\",\n \"content\": \"Hello, noble dromedary (Camelus dromedarius)! The one‑humped desert specialist.\\n\\nQuick facts:\\n- Adapted to heat: can lose ~25% body water and rehydrate fast (up to ~100 liters).\\n- Hump st [...] + "headers" : { + "x-request-id" : "req_235a97c40a5946a2b4f47c7cac94fb73", + "x-ratelimit-limit-tokens" : "500000", + "openai-organization" : "user-nvrq0gduw4i0ooapnshoh6gw", + "CF-RAY" : "9c4792f3f89ec16b-LHR", + "Server" : "cloudflare", + "X-Content-Type-Options" : "nosniff", + "x-ratelimit-reset-requests" : "120ms", + "x-openai-proxy-wasm" : "v0.1", + "x-ratelimit-remaining-tokens" : "499985", + "cf-cache-status" : "DYNAMIC", + "x-ratelimit-remaining-requests" : "499", + "Date" : "Tue, 27 Jan 2026 10:30:40 GMT", + "x-ratelimit-reset-tokens" : "1ms", + "access-control-expose-headers" : "X-Request-ID", + "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", + "x-ratelimit-limit-requests" : "500", + "Set-Cookie" : [ "__cf_bm=33byza1OYC15qC511TZS_ytkkbXGjCsihf2Va75m_C8-1769509840-1.0.1.1-sWlxikX3h1CzbG8teJ.NIMKqaY9olMkhsGOwWTdUQ11VE9fnrHXw1pyHz7.VvD_de8N3UMVBNPkCtPBQR9wbiMwCJ.9XvESHkoDQf3RQli0; path=/; expires=Tue, 27-Jan-26 11:00:40 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None", "_cfuvid=kpNFQ_OqqCEUnWomGoWgeWYqAfsLVf9zPWIQ5NXN2S4-1769509840956-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" ], + "openai-version" : "2020-10-01", + "openai-processing-ms" : "20843", + "alt-svc" : "h3=\":443\"; ma=86400", + "Content-Type" : "application/json" + } + }, + "uuid" : "74689f85-f209-4129-b2c8-5e4d38c8eb54", + "persistent" : true, + "insertionIndex" : 8 +} \ No newline at end of file diff --git a/integration-tests/openai/src/test/resources/mappings/chat_completions-8454f54f-99d3-46a6-b443-720151e4d73a.json b/integration-tests/openai/src/test/resources/mappings/chat_completions-8454f54f-99d3-46a6-b443-720151e4d73a.json new file mode 100644 index 0000000000..cad37f5f3e --- /dev/null +++ b/integration-tests/openai/src/test/resources/mappings/chat_completions-8454f54f-99d3-46a6-b443-720151e4d73a.json @@ -0,0 +1,46 @@ +{ + "id" : "8454f54f-99d3-46a6-b443-720151e4d73a", + "name" : "chat_completions", + "request" : { + "url" : "/chat/completions", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{\"messages\":[{\"content\":[{\"text\":\"Describe what you see in this image\",\"type\":\"text\"},{\"image_url\":{\"url\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIgAAACACAYAAADQ6SE/AAAKomlDQ1BJQ0MgUHJvZmlsZQAASImVlwdQk9kWx+/3pYeElhA6hN6ktwBSQg+9NxshCRBKDAlBxa4sruBaEBEBRdBVEQXXAogdUWyLgGLXDbKIKOtiQVRU3gcMYXffvPfmnZkz9zf/nHvuuXfu/eYEALIiWyjMghUByBbkiqICvOkJiUl03CsAAQKQBwrAiM0RC5kRESEAsZnx7/bxHhKN2B3LyVz//vt/NSUuT8wBAIpAOIUr5mQjfBLxYY5QlAsAai+iGyzJFU5yO8JUEVI [...] + "ignoreArrayOrder" : true, + "ignoreExtraElements" : true + } ] + }, + "response" : { + "status" : 200, + "body" : "{\n \"id\": \"chatcmpl-D2ae3F39KqqvDYmRCB8ytK4uivhk8\",\n \"object\": \"chat.completion\",\n \"created\": 1769509891,\n \"model\": \"gpt-5-2025-08-07\",\n \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": \"assistant\",\n \"content\": \"A minimalist logo showing a white camel silhouette inside an orange circle. The background has orange and brown shapes resembling desert dunes.\",\n \"refusal\": null,\n \"annotatio [...] + "headers" : { + "Server" : "cloudflare", + "x-ratelimit-reset-input-images" : "1ms", + "x-ratelimit-reset-tokens" : "93ms", + "x-ratelimit-limit-input-images" : "50000", + "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", + "x-ratelimit-remaining-input-images" : "49999", + "Set-Cookie" : [ "__cf_bm=zPu326k25_oKkl.tdR0o6S90RsNw8nGm2ksOXYvBjfo-1769509897-1.0.1.1-cKkSZO6QVoyyYzU4QsH2XebZbyXWrXpGgFVAGzALvH1n5vZw0bCGw_W1DQOv8pkRGWIZs9u8SZO2zmihgfjxwJ_1wDsdfpNlrVHh5qC6ES0; path=/; expires=Tue, 27-Jan-26 11:01:37 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None", "_cfuvid=tVCs6wDcvX6z0mZeIFCM18FQ4sgbX.5Ie78JUwxAxMY-1769509897907-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" ], + "Content-Type" : "application/json", + "x-request-id" : "req_53c5adcfb8b049c68382a8ba1ae8f23c", + "x-ratelimit-limit-tokens" : "500000", + "openai-organization" : "user-nvrq0gduw4i0ooapnshoh6gw", + "CF-RAY" : "9c4794b6f86fbd74-LHR", + "X-Content-Type-Options" : "nosniff", + "x-ratelimit-reset-requests" : "120ms", + "x-openai-proxy-wasm" : "v0.1", + "x-ratelimit-remaining-tokens" : "499224", + "cf-cache-status" : "DYNAMIC", + "x-ratelimit-remaining-requests" : "499", + "Date" : "Tue, 27 Jan 2026 10:31:37 GMT", + "access-control-expose-headers" : "X-Request-ID", + "x-ratelimit-limit-requests" : "500", + "openai-version" : "2020-10-01", + "openai-processing-ms" : "6049", + "alt-svc" : "h3=\":443\"; ma=86400" + } + }, + "uuid" : "8454f54f-99d3-46a6-b443-720151e4d73a", + "persistent" : true, + "insertionIndex" : 3 +} \ No newline at end of file diff --git a/integration-tests/openai/src/test/resources/mappings/chat_completions-9368b026-c5a6-4dc2-ad3a-d34eaa1c149b.json b/integration-tests/openai/src/test/resources/mappings/chat_completions-9368b026-c5a6-4dc2-ad3a-d34eaa1c149b.json new file mode 100644 index 0000000000..31026d3387 --- /dev/null +++ b/integration-tests/openai/src/test/resources/mappings/chat_completions-9368b026-c5a6-4dc2-ad3a-d34eaa1c149b.json @@ -0,0 +1,43 @@ +{ + "id" : "9368b026-c5a6-4dc2-ad3a-d34eaa1c149b", + "name" : "chat_completions", + "request" : { + "url" : "/chat/completions", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{\"messages\":[{\"content\":\"Create an example product description.\",\"role\":\"user\"}],\"model\":\"gpt-5\",\"response_format\":{\"json_schema\":{\"name\":\"camel_schema\",\"schema\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"},\"price\":{\"type\":\"number\"}}}},\"type\":\"json_schema\"},\"temperature\":1.0}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : true + } ] + }, + "response" : { + "status" : 200, + "body" : "{\n \"id\": \"chatcmpl-D2adkRbYAuoCLZFmOSbydHCztd0Jx\",\n \"object\": \"chat.completion\",\n \"created\": 1769509872,\n \"model\": \"gpt-5-2025-08-07\",\n \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": \"assistant\",\n \"content\": \"{\\\"name\\\":\\\"Aurora Smart Mug (12 oz, Temperature-Controlled)\\\",\\\"price\\\":129.99}\",\n \"refusal\": null,\n \"annotations\": []\n },\n \"finish_reason\": \"stop [...] + "headers" : { + "x-request-id" : "req_c8cd80a34c514cf39a913625256dfb9c", + "x-ratelimit-limit-tokens" : "500000", + "openai-organization" : "user-nvrq0gduw4i0ooapnshoh6gw", + "CF-RAY" : "9c479440789735ca-LHR", + "Server" : "cloudflare", + "X-Content-Type-Options" : "nosniff", + "x-ratelimit-reset-requests" : "120ms", + "x-openai-proxy-wasm" : "v0.1", + "x-ratelimit-remaining-tokens" : "499988", + "cf-cache-status" : "DYNAMIC", + "x-ratelimit-remaining-requests" : "499", + "Date" : "Tue, 27 Jan 2026 10:31:31 GMT", + "x-ratelimit-reset-tokens" : "1ms", + "access-control-expose-headers" : "X-Request-ID", + "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", + "x-ratelimit-limit-requests" : "500", + "Set-Cookie" : [ "__cf_bm=d0AxkyftuYYziAnrqHz..8QdV9pLr63OWpq5cUnC.7k-1769509891-1.0.1.1-KGITVe8dMxwpFIZC5yByRG51C4L_Mvs81l7kheCwI5Q7oSbeGn8R5L3pHCwkISkMbWHsL4m115KnxUGo1h9kfgUvGUHrcbObZ3wpq0xza.M; path=/; expires=Tue, 27-Jan-26 11:01:31 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None", "_cfuvid=nMBpOA30JgxvHJZTVFpzf1SoQBJnneKeAZaEPqHIrTg-1769509891388-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" ], + "openai-version" : "2020-10-01", + "openai-processing-ms" : "18380", + "alt-svc" : "h3=\":443\"; ma=86400", + "Content-Type" : "application/json" + } + }, + "uuid" : "9368b026-c5a6-4dc2-ad3a-d34eaa1c149b", + "persistent" : true, + "insertionIndex" : 4 +} \ No newline at end of file diff --git a/integration-tests/openai/src/test/resources/mappings/chat_completions-e61cd29d-a5ff-42ad-a4de-9975902e0f87.json b/integration-tests/openai/src/test/resources/mappings/chat_completions-e61cd29d-a5ff-42ad-a4de-9975902e0f87.json new file mode 100644 index 0000000000..b4181c6e27 --- /dev/null +++ b/integration-tests/openai/src/test/resources/mappings/chat_completions-e61cd29d-a5ff-42ad-a4de-9975902e0f87.json @@ -0,0 +1,45 @@ +{ + "id" : "e61cd29d-a5ff-42ad-a4de-9975902e0f87", + "name" : "chat_completions", + "request" : { + "url" : "/chat/completions", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{\"messages\":[{\"content\":\"In one sentence, what is Apache Camel?\",\"role\":\"user\"}],\"model\":\"gpt-5\",\"temperature\":1.0}", + "ignoreArrayOrder" : true, + "ignoreExtraElements" : true + } ] + }, + "response" : { + "status" : 200, + "body" : "{\n \"id\": \"chatcmpl-D2adh1LgAqGNfZ25hExQ533tA6821\",\n \"object\": \"chat.completion\",\n \"created\": 1769509869,\n \"model\": \"gpt-5-2025-08-07\",\n \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": \"assistant\",\n \"content\": \"Apache Camel is an open-source integration framework that implements Enterprise Integration Patterns, providing a lightweight routing and mediation engine with a DSL and many components to connec [...] + "headers" : { + "x-request-id" : "req_fd57ef25ac39465dad43036765d84e03", + "x-ratelimit-limit-tokens" : "500000", + "openai-organization" : "user-nvrq0gduw4i0ooapnshoh6gw", + "CF-RAY" : "9c47942b8b1fc418-LHR", + "Server" : "cloudflare", + "X-Content-Type-Options" : "nosniff", + "x-ratelimit-reset-requests" : "120ms", + "x-openai-proxy-wasm" : "v0.1", + "x-ratelimit-remaining-tokens" : "499988", + "cf-cache-status" : "DYNAMIC", + "x-ratelimit-remaining-requests" : "499", + "Date" : "Tue, 27 Jan 2026 10:31:12 GMT", + "x-ratelimit-reset-tokens" : "1ms", + "access-control-expose-headers" : "X-Request-ID", + "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", + "x-ratelimit-limit-requests" : "500", + "Set-Cookie" : [ "__cf_bm=ymu_2KZQC_t.c3L3JOAbeixy9wtmC2xyPxxr7APetmw-1769509872-1.0.1.1-rN7ays_witJLb0asu6T5a747LRHyO5hFxHCe7upipi_vI.KVjQ26v8i7jnpXhmFN.j6Uh6mWCRV8Lw9mo58NAhs1mTkYS3KoDvMVFj2krfg; path=/; expires=Tue, 27-Jan-26 11:01:12 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None", "_cfuvid=MjSpddawvuLLIbFr1xePQ9B9VzLv1KFPhYOcvPeQYq4-1769509872549-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" ], + "openai-version" : "2020-10-01", + "openai-processing-ms" : "2914", + "alt-svc" : "h3=\":443\"; ma=86400", + "Content-Type" : "application/json" + } + }, + "uuid" : "e61cd29d-a5ff-42ad-a4de-9975902e0f87", + "persistent" : true, + "scenarioName" : "scenario-1-chat-completions", + "requiredScenarioState" : "scenario-1-chat-completions-2", + "insertionIndex" : 5 +} \ No newline at end of file diff --git a/integration-tests/openai/src/test/resources/mappings/chat_completions-f0e11826-78d1-4bf6-988f-f6d31ae30e0b.json b/integration-tests/openai/src/test/resources/mappings/chat_completions-f0e11826-78d1-4bf6-988f-f6d31ae30e0b.json new file mode 100644 index 0000000000..dba12c5c4d --- /dev/null +++ b/integration-tests/openai/src/test/resources/mappings/chat_completions-f0e11826-78d1-4bf6-988f-f6d31ae30e0b.json @@ -0,0 +1,43 @@ +{ + "id" : "f0e11826-78d1-4bf6-988f-f6d31ae30e0b", + "name" : "chat_completions", + "request" : { + "url" : "/chat/completions", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{\"messages\":[{\"role\":\"assistant\",\"content\":\"Hello, noble dromedary (Camelus dromedarius)! The one‑humped desert specialist.\\n\\nQuick facts:\\n- Adapted to heat: can lose ~25% body water and rehydrate fast (up to ~100 liters).\\n- Hump stores fat for energy; not water.\\n- Three-chambered foregut fermenter; eats tough, thorny plants.\\n- Long lashes and closable nostrils keep out sand.\\n- Top speed ~65 km/h; good endurance around 40 km/h.\\n- Gestation ~ [...] + "ignoreArrayOrder" : true, + "ignoreExtraElements" : true + } ] + }, + "response" : { + "status" : 200, + "body" : "{\n \"id\": \"chatcmpl-D2adFTDPvRuxRRcmU1pPQDGGiQxyL\",\n \"object\": \"chat.completion\",\n \"created\": 1769509841,\n \"model\": \"gpt-5-2025-08-07\",\n \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": \"assistant\",\n \"content\": \"I can’t tell without a photo or details, but here’s a quick way to identify it—answer these and I’ll confirm:\\n\\n1) How many humps?\\n- One hump → Dromedary (Camelus dromedarius)\\n- Two humps [...] + "headers" : { + "x-request-id" : "req_3edf3b20977140a2a68d78a29f6307dd", + "x-ratelimit-limit-tokens" : "500000", + "openai-organization" : "user-nvrq0gduw4i0ooapnshoh6gw", + "CF-RAY" : "9c47937afc62946b-LHR", + "Server" : "cloudflare", + "X-Content-Type-Options" : "nosniff", + "x-ratelimit-reset-requests" : "120ms", + "x-openai-proxy-wasm" : "v0.1", + "x-ratelimit-remaining-tokens" : "499841", + "cf-cache-status" : "DYNAMIC", + "x-ratelimit-remaining-requests" : "499", + "Date" : "Tue, 27 Jan 2026 10:31:05 GMT", + "x-ratelimit-reset-tokens" : "19ms", + "access-control-expose-headers" : "X-Request-ID", + "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", + "x-ratelimit-limit-requests" : "500", + "Set-Cookie" : [ "__cf_bm=nfCEMBx8kR84HbjzNLM4sadXOLDwgVdaOpvzXlaK.8A-1769509865-1.0.1.1-v7K1nUcOBZQ1IWz1eA0rxZEGtUcrt7pfmwHkvwPSCZGofgLjDeLQNAwyAnAt6O73_fz2EVYXMEhEWUc8UPlnWSJLrecbC.CEYtqXKlsXC6Q; path=/; expires=Tue, 27-Jan-26 11:01:05 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None", "_cfuvid=Hwb3.ovfE31QNem57HVJNi3IvP9FuaLFzSpTz1ziU2M-1769509865016-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" ], + "openai-version" : "2020-10-01", + "openai-processing-ms" : "23727", + "alt-svc" : "h3=\":443\"; ma=86400", + "Content-Type" : "application/json" + } + }, + "uuid" : "f0e11826-78d1-4bf6-988f-f6d31ae30e0b", + "persistent" : true, + "insertionIndex" : 7 +} \ No newline at end of file diff --git a/integration-tests/openai/src/test/resources/mappings/chat_completions-fb0a5dd7-0e7c-4aa5-9274-203c032bfac5.json b/integration-tests/openai/src/test/resources/mappings/chat_completions-fb0a5dd7-0e7c-4aa5-9274-203c032bfac5.json new file mode 100644 index 0000000000..0f80f03477 --- /dev/null +++ b/integration-tests/openai/src/test/resources/mappings/chat_completions-fb0a5dd7-0e7c-4aa5-9274-203c032bfac5.json @@ -0,0 +1,43 @@ +{ + "id" : "fb0a5dd7-0e7c-4aa5-9274-203c032bfac5", + "name" : "chat_completions", + "request" : { + "url" : "/chat/completions", + "method" : "POST", + "bodyPatterns" : [ { + "equalToJson" : "{\"messages\":[{\"content\":\"Create an example product for a product named 'Bluetooth Headphones'.\",\"role\":\"user\"}],\"model\":\"gpt-5\",\"response_format\":{\"json_schema\":{\"name\":\"json-schema-from-Product\",\"schema\":{\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"},\"price\":{\"type\":\"number\"}},\"required\":[\"name\",\"price\"],\"additionalProperties\":false},\"strict\":t [...] + "ignoreArrayOrder" : true, + "ignoreExtraElements" : true + } ] + }, + "response" : { + "status" : 200, + "body" : "{\n \"id\": \"chatcmpl-D2aeDT44NmI7quPk6EKQySXbjfQlS\",\n \"object\": \"chat.completion\",\n \"created\": 1769509901,\n \"model\": \"gpt-5-2025-08-07\",\n \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": \"assistant\",\n \"content\": \"{\\\"name\\\":\\\"Bluetooth Headphones\\\",\\\"price\\\":79.99}\",\n \"refusal\": null,\n \"annotations\": []\n },\n \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": [...] + "headers" : { + "x-request-id" : "req_2e8d1220dad145129e1a39c5c335393b", + "x-ratelimit-limit-tokens" : "500000", + "openai-organization" : "user-nvrq0gduw4i0ooapnshoh6gw", + "CF-RAY" : "9c4794f4aad30c3c-LHR", + "Server" : "cloudflare", + "X-Content-Type-Options" : "nosniff", + "x-ratelimit-reset-requests" : "120ms", + "x-openai-proxy-wasm" : "v0.1", + "x-ratelimit-remaining-tokens" : "499980", + "cf-cache-status" : "DYNAMIC", + "x-ratelimit-remaining-requests" : "499", + "Date" : "Tue, 27 Jan 2026 10:31:54 GMT", + "x-ratelimit-reset-tokens" : "2ms", + "access-control-expose-headers" : "X-Request-ID", + "Strict-Transport-Security" : "max-age=31536000; includeSubDomains; preload", + "x-ratelimit-limit-requests" : "500", + "Set-Cookie" : [ "__cf_bm=_CXULdvnKtnj9mqc9nBMXgAv1ZF0JvPIl2.yqy2x6Ng-1769509914-1.0.1.1-Z.jsxl9BYlVStT0t5pO.eqC96zSPgQ9oUz_urB.d4M9SVnMPyYC8F3A34RyVkw4SQAEg1ECSBfE15qCaAuLTLyh9mAp0Nl18Gx6CNk0lZIg; path=/; expires=Tue, 27-Jan-26 11:01:54 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None", "_cfuvid=2mwBW9xXw_VpeLnnP2tddDnAAab82LNgca4NBHc8Gl8-1769509914516-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None" ], + "openai-version" : "2020-10-01", + "openai-processing-ms" : "12820", + "alt-svc" : "h3=\":443\"; ma=86400", + "Content-Type" : "application/json" + } + }, + "uuid" : "fb0a5dd7-0e7c-4aa5-9274-203c032bfac5", + "persistent" : true, + "insertionIndex" : 1 +} \ No newline at end of file diff --git a/integration-tests/openai/src/test/resources/prompts/whatis-camel-prompt.txt b/integration-tests/openai/src/test/resources/prompts/whatis-camel-prompt.txt new file mode 100644 index 0000000000..af2a069b25 --- /dev/null +++ b/integration-tests/openai/src/test/resources/prompts/whatis-camel-prompt.txt @@ -0,0 +1 @@ +In one sentence, what is Apache Camel? \ No newline at end of file diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 3b69f8d01f..f69ff0e2ce 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -190,6 +190,7 @@ <module>ognl</module> <module>olingo4</module> <module>once</module> + <module>openai</module> <module>openapi-java</module> <module>openstack</module> <module>opentelemetry</module> diff --git a/poms/bom/pom.xml b/poms/bom/pom.xml index cd5a466b2a..1ad268527d 100644 --- a/poms/bom/pom.xml +++ b/poms/bom/pom.xml @@ -2400,6 +2400,17 @@ <artifactId>camel-once</artifactId> <version>${camel.version}</version> </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-openai</artifactId> + <version>${camel.version}</version> + <exclusions> + <exclusion> + <groupId>io.swagger.core.v3</groupId> + <artifactId>swagger-annotations</artifactId> + </exclusion> + </exclusions> + </dependency> <dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-openapi-java</artifactId> @@ -5720,6 +5731,16 @@ <artifactId>camel-quarkus-once-deployment</artifactId> <version>${camel-quarkus.version}</version> </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-openai</artifactId> + <version>${camel-quarkus.version}</version> + </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-openai-deployment</artifactId> + <version>${camel-quarkus.version}</version> + </dependency> <dependency> <groupId>org.apache.camel.quarkus</groupId> <artifactId>camel-quarkus-openapi-java</artifactId> diff --git a/poms/bom/src/main/generated/flattened-full-pom.xml b/poms/bom/src/main/generated/flattened-full-pom.xml index a6735d2948..6ba6f94950 100644 --- a/poms/bom/src/main/generated/flattened-full-pom.xml +++ b/poms/bom/src/main/generated/flattened-full-pom.xml @@ -2316,6 +2316,17 @@ <artifactId>camel-once</artifactId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> <version>4.17.0</version><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> </dependency> + <dependency> + <groupId>org.apache.camel</groupId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> + <artifactId>camel-openai</artifactId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> + <version>4.17.0</version><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> + <exclusions> + <exclusion> + <groupId>io.swagger.core.v3</groupId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> + <artifactId>swagger-annotations</artifactId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> + </exclusion> + </exclusions> + </dependency> <dependency> <groupId>org.apache.camel</groupId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> <artifactId>camel-openapi-java</artifactId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> @@ -5608,6 +5619,16 @@ <artifactId>camel-quarkus-once-deployment</artifactId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> <version>3.32.0-SNAPSHOT</version><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> + <artifactId>camel-quarkus-openai</artifactId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> + <version>3.32.0-SNAPSHOT</version><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> + </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> + <artifactId>camel-quarkus-openai-deployment</artifactId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> + <version>3.32.0-SNAPSHOT</version><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> + </dependency> <dependency> <groupId>org.apache.camel.quarkus</groupId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> <artifactId>camel-quarkus-openapi-java</artifactId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> diff --git a/poms/bom/src/main/generated/flattened-reduced-pom.xml b/poms/bom/src/main/generated/flattened-reduced-pom.xml index 30d11c038b..556e2330f2 100644 --- a/poms/bom/src/main/generated/flattened-reduced-pom.xml +++ b/poms/bom/src/main/generated/flattened-reduced-pom.xml @@ -2306,6 +2306,17 @@ <artifactId>camel-once</artifactId> <version>4.17.0</version> </dependency> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-openai</artifactId> + <version>4.17.0</version> + <exclusions> + <exclusion> + <groupId>io.swagger.core.v3</groupId> + <artifactId>swagger-annotations</artifactId> + </exclusion> + </exclusions> + </dependency> <dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-openapi-java</artifactId> @@ -5587,6 +5598,16 @@ <artifactId>camel-quarkus-once-deployment</artifactId> <version>3.32.0-SNAPSHOT</version> </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-openai</artifactId> + <version>3.32.0-SNAPSHOT</version> + </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-openai-deployment</artifactId> + <version>3.32.0-SNAPSHOT</version> + </dependency> <dependency> <groupId>org.apache.camel.quarkus</groupId> <artifactId>camel-quarkus-openapi-java</artifactId> diff --git a/poms/bom/src/main/generated/flattened-reduced-verbose-pom.xml b/poms/bom/src/main/generated/flattened-reduced-verbose-pom.xml index 7d46431840..0b7706ef5c 100644 --- a/poms/bom/src/main/generated/flattened-reduced-verbose-pom.xml +++ b/poms/bom/src/main/generated/flattened-reduced-verbose-pom.xml @@ -2306,6 +2306,17 @@ <artifactId>camel-once</artifactId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> <version>4.17.0</version><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> </dependency> + <dependency> + <groupId>org.apache.camel</groupId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> + <artifactId>camel-openai</artifactId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> + <version>4.17.0</version><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> + <exclusions> + <exclusion> + <groupId>io.swagger.core.v3</groupId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> + <artifactId>swagger-annotations</artifactId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> + </exclusion> + </exclusions> + </dependency> <dependency> <groupId>org.apache.camel</groupId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> <artifactId>camel-openapi-java</artifactId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> @@ -5587,6 +5598,16 @@ <artifactId>camel-quarkus-once-deployment</artifactId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> <version>3.32.0-SNAPSHOT</version><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> + <artifactId>camel-quarkus-openai</artifactId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> + <version>3.32.0-SNAPSHOT</version><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> + </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> + <artifactId>camel-quarkus-openai-deployment</artifactId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> + <version>3.32.0-SNAPSHOT</version><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> + </dependency> <dependency> <groupId>org.apache.camel.quarkus</groupId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> <artifactId>camel-quarkus-openapi-java</artifactId><!-- org.apache.camel.quarkus:camel-quarkus-bom:${project.version} --> diff --git a/tooling/scripts/test-categories.yaml b/tooling/scripts/test-categories.yaml index 57ea78b9ad..c3dcb50302 100644 --- a/tooling/scripts/test-categories.yaml +++ b/tooling/scripts/test-categories.yaml @@ -136,6 +136,7 @@ group-07: - lumberjack - ognl - olingo4 + - openai - optaplanner - pg-replication-slot - splunk-hec
