This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch fix/CAMEL-23960
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 3acfb23c4dfbbd3ff7dfe8584af0574589ae36a8
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Jul 18 15:04:28 2026 +0200

    CAMEL-23960: camel-openai - Fail fast when outputClass cannot be resolved
    
    Previously, if the outputClass option (or CamelOpenAIOutputClass header)
    contained an unresolvable class name, it was silently ignored and no
    structured output format was applied. Now uses resolveMandatoryClass
    which throws ClassNotFoundException for unknown classes.
    
    Endpoint-level outputClass is resolved once at startup for efficiency.
    Header-level overrides are still resolved dynamically per exchange.
    
    Also fixes the header metadata javaType from Class to String since the
    header value is a FQCN string, not a Class object.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../apache/camel/catalog/components/openai.json    |  2 +-
 .../org/apache/camel/component/openai/openai.json  |  2 +-
 .../camel/component/openai/OpenAIConstants.java    |  2 +-
 .../camel/component/openai/OpenAIProducer.java     | 18 ++++-
 .../component/openai/OpenAIOutputClassTest.java    | 85 ++++++++++++++++++++++
 .../endpoint/dsl/OpenAIEndpointBuilderFactory.java |  4 +-
 6 files changed, 105 insertions(+), 8 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/openai.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/openai.json
index 11942d539af7..ae4a63a23c81 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/openai.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/openai.json
@@ -42,7 +42,7 @@
     "CamelOpenAITopP": { "index": 5, "kind": "header", "displayName": "", 
"group": "producer", "label": "", "required": false, "javaType": "Double", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "An alternative to temperature for controlling 
randomness. Uses nucleus sampling where the model considers tokens with top_p 
probability mass", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#TOP_P" },
     "CamelOpenAIMaxTokens": { "index": 6, "kind": "header", "displayName": "", 
"group": "producer", "label": "", "required": false, "javaType": "Integer", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The maximum number of tokens to generate in the 
completion", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#MAX_TOKENS" },
     "CamelOpenAIStreaming": { "index": 7, "kind": "header", "displayName": "", 
"group": "producer", "label": "", "required": false, "javaType": "Boolean", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "Whether to stream the response back incrementally", 
"constantName": "org.apache.camel.component.openai.OpenAIConstants#STREAMING" },
-    "CamelOpenAIOutputClass": { "index": 8, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "Class", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The Java class to use for structured output parsing", 
"constantName": 
"org.apache.camel.component.openai.OpenAIConstants#OUTPUT_CLASS" },
+    "CamelOpenAIOutputClass": { "index": 8, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The Java class name (FQCN) to use for structured output 
parsing", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#OUTPUT_CLASS" },
     "CamelOpenAIJsonSchema": { "index": 9, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The JSON schema to use for structured output 
validation", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#JSON_SCHEMA" },
     "CamelOpenAIStripThinking": { "index": 10, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "Boolean", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "Whether to strip ... blocks from the 
response body", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#STRIP_THINKING" },
     "CamelOpenAIMediaType": { "index": 11, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The MIME type of the message body when sending a file or 
binary content (File, WrappedFile, byte or InputStream) to the model. Takes 
precedence over component content-type headers and automatic MIME type 
detection", "constantName": "org.apa [...]
diff --git 
a/components/camel-ai/camel-openai/src/generated/resources/META-INF/org/apache/camel/component/openai/openai.json
 
b/components/camel-ai/camel-openai/src/generated/resources/META-INF/org/apache/camel/component/openai/openai.json
index 11942d539af7..ae4a63a23c81 100644
--- 
a/components/camel-ai/camel-openai/src/generated/resources/META-INF/org/apache/camel/component/openai/openai.json
+++ 
b/components/camel-ai/camel-openai/src/generated/resources/META-INF/org/apache/camel/component/openai/openai.json
@@ -42,7 +42,7 @@
     "CamelOpenAITopP": { "index": 5, "kind": "header", "displayName": "", 
"group": "producer", "label": "", "required": false, "javaType": "Double", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "An alternative to temperature for controlling 
randomness. Uses nucleus sampling where the model considers tokens with top_p 
probability mass", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#TOP_P" },
     "CamelOpenAIMaxTokens": { "index": 6, "kind": "header", "displayName": "", 
"group": "producer", "label": "", "required": false, "javaType": "Integer", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The maximum number of tokens to generate in the 
completion", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#MAX_TOKENS" },
     "CamelOpenAIStreaming": { "index": 7, "kind": "header", "displayName": "", 
"group": "producer", "label": "", "required": false, "javaType": "Boolean", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "Whether to stream the response back incrementally", 
"constantName": "org.apache.camel.component.openai.OpenAIConstants#STREAMING" },
-    "CamelOpenAIOutputClass": { "index": 8, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "Class", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The Java class to use for structured output parsing", 
"constantName": 
"org.apache.camel.component.openai.OpenAIConstants#OUTPUT_CLASS" },
+    "CamelOpenAIOutputClass": { "index": 8, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The Java class name (FQCN) to use for structured output 
parsing", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#OUTPUT_CLASS" },
     "CamelOpenAIJsonSchema": { "index": 9, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The JSON schema to use for structured output 
validation", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#JSON_SCHEMA" },
     "CamelOpenAIStripThinking": { "index": 10, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "Boolean", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "Whether to strip ... blocks from the 
response body", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#STRIP_THINKING" },
     "CamelOpenAIMediaType": { "index": 11, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The MIME type of the message body when sending a file or 
binary content (File, WrappedFile, byte or InputStream) to the model. Takes 
precedence over component content-type headers and automatic MIME type 
detection", "constantName": "org.apa [...]
diff --git 
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConstants.java
 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConstants.java
index 09472bf975f7..a6bac9df580b 100644
--- 
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConstants.java
+++ 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConstants.java
@@ -42,7 +42,7 @@ public final class OpenAIConstants {
     public static final String MAX_TOKENS = "CamelOpenAIMaxTokens";
     @Metadata(description = "Whether to stream the response back 
incrementally", javaType = "Boolean")
     public static final String STREAMING = "CamelOpenAIStreaming";
-    @Metadata(description = "The Java class to use for structured output 
parsing", javaType = "Class")
+    @Metadata(description = "The Java class name (FQCN) to use for structured 
output parsing", javaType = "String")
     public static final String OUTPUT_CLASS = "CamelOpenAIOutputClass";
     @Metadata(description = "The JSON schema to use for structured output 
validation", javaType = "String")
     public static final String JSON_SCHEMA = "CamelOpenAIJsonSchema";
diff --git 
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java
 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java
index 07393c4adc05..37a79794a6ab 100644
--- 
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java
+++ 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java
@@ -76,6 +76,8 @@ public class OpenAIProducer extends DefaultAsyncProducer {
     private static final Pattern THINK_PATTERN = 
Pattern.compile("^\\s*<think>(.*?)</think>\\s*", Pattern.DOTALL);
     private static final String PENDING_USER_MESSAGE = 
"CamelOpenAIPendingUserMessage";
 
+    private Class<?> outputClassResolved;
+
     public OpenAIProducer(OpenAIEndpoint endpoint) {
         super(endpoint);
     }
@@ -84,6 +86,11 @@ public class OpenAIProducer extends DefaultAsyncProducer {
     protected void doStart() throws Exception {
         OpenAIConfiguration config = getEndpoint().getConfiguration();
 
+        if (ObjectHelper.isNotEmpty(config.getOutputClass())) {
+            outputClassResolved = 
getEndpoint().getCamelContext().getClassResolver()
+                    .resolveMandatoryClass(config.getOutputClass());
+        }
+
         if (ObjectHelper.isNotEmpty(config.getJsonSchema())) {
             String resolved = 
getEndpoint().getCamelContext().resolvePropertyPlaceholders(config.getJsonSchema());
             String content = resolveResourceContent(resolved);
@@ -160,10 +167,15 @@ public class OpenAIProducer extends DefaultAsyncProducer {
 
         // Structured output handling
         if (ObjectHelper.isNotEmpty(outputClass)) {
-            Class<?> responseClass = 
getEndpoint().getCamelContext().getClassResolver().resolveClass(outputClass);
-            if (responseClass != null) {
-                paramsBuilder.responseFormat(responseClass);
+            Class<?> responseClass;
+            String headerOutputClass = 
in.getHeader(OpenAIConstants.OUTPUT_CLASS, String.class);
+            if (ObjectHelper.isNotEmpty(headerOutputClass)) {
+                responseClass = 
getEndpoint().getCamelContext().getClassResolver()
+                        .resolveMandatoryClass(headerOutputClass);
+            } else {
+                responseClass = outputClassResolved;
             }
+            paramsBuilder.responseFormat(responseClass);
         } else if (ObjectHelper.isNotEmpty(jsonSchema)) {
             // Build OpenAI JSON schema response format from provided schema 
string
             try {
diff --git 
a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIOutputClassTest.java
 
b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIOutputClassTest.java
new file mode 100644
index 000000000000..670df8105def
--- /dev/null
+++ 
b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIOutputClassTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.component.openai;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.FailedToStartRouteException;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.infra.openai.mock.OpenAIMock;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class OpenAIOutputClassTest extends CamelTestSupport {
+
+    @RegisterExtension
+    public OpenAIMock openAIMock = new OpenAIMock().builder()
+            .when("hello")
+            .replyWith("Hi from mock")
+            .end()
+            .build();
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:chat")
+                        
.to("openai:chat-completion?model=gpt-5&apiKey=dummy&baseUrl="
+                            + openAIMock.getBaseUrl() + "/v1");
+            }
+        };
+    }
+
+    @Test
+    void unknownOutputClassViaHeaderShouldFail() {
+        Exchange result = template.request("direct:chat", e -> {
+            e.getIn().setBody("hello");
+            e.getIn().setHeader(OpenAIConstants.OUTPUT_CLASS, 
"com.does.not.Exist");
+        });
+
+        assertThat(result.getException())
+                .isInstanceOf(ClassNotFoundException.class)
+                .hasMessageContaining("com.does.not.Exist");
+    }
+
+    @Test
+    void unknownOutputClassOnEndpointShouldFailOnStartup() throws Exception {
+        CamelContext ctx = createCamelContext();
+        ctx.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:bad")
+                        
.to("openai:chat-completion?model=gpt-5&apiKey=dummy&outputClass=com.does.not.Exist&baseUrl="
+                            + openAIMock.getBaseUrl() + "/v1");
+            }
+        });
+
+        Exception thrown = assertThrows(FailedToStartRouteException.class, 
ctx::start);
+
+        assertThat(thrown)
+                .rootCause()
+                .isInstanceOf(ClassNotFoundException.class)
+                .hasMessageContaining("com.does.not.Exist");
+
+        ctx.stop();
+    }
+}
diff --git 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpenAIEndpointBuilderFactory.java
 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpenAIEndpointBuilderFactory.java
index 013cfd3e5ea7..5d353f4ddde8 100644
--- 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpenAIEndpointBuilderFactory.java
+++ 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/OpenAIEndpointBuilderFactory.java
@@ -1294,9 +1294,9 @@ public interface OpenAIEndpointBuilderFactory {
             return "CamelOpenAIStreaming";
         }
         /**
-         * The Java class to use for structured output parsing.
+         * The Java class name (FQCN) to use for structured output parsing.
          * 
-         * The option is a: {@code Class} type.
+         * The option is a: {@code String} type.
          * 
          * Group: producer
          * 

Reply via email to