From 474bf98d631bf1a3dc17b4b673b2a12ba650e373 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 3 Aug 2026 08:19:36 +0200 Subject: [PATCH 1/2] CAMEL-24322: Add tool-calling support via AiToolRegistry Extend the camel-openai component to discover and execute Camel route tools registered via the shared AiToolRegistry, alongside existing MCP tools. This implements Step 6 of the unified AI tool abstraction design (CAMEL-23382). Changes: - Add camel-ai-tool compile dependency to camel-openai - Add 'tags' configuration parameter to OpenAIConfiguration for filtering tools by tag from the shared AiToolRegistry - Create AiToolSpecToOpenAI converter that transforms AiToolSpec into OpenAI ChatCompletionFunctionTool using parametersJsonSchema - Extend OpenAIProducer to discover Camel route tools and dispatch them via AiToolExecutor in the agentic loop, with exchange isolation - Extend OpenAIToolExecutionProducer similarly for manual tool loops - Add AiToolSpecToOpenAITest with 8 test cases covering full spec, no params, no description, default type, required arrays, empty schema, invalid schema, and additionalProperties:false - Update error messages to reference generic "tool source" instead of MCP-specific wording Co-Authored-By: Claude Opus 4.6 --- components/camel-ai/camel-openai/pom.xml | 5 + .../openai/OpenAIEndpointConfigurer.java | 3 + .../openai/OpenAIEndpointUriFactory.java | 3 +- .../apache/camel/component/openai/openai.json | 37 ++-- .../component/openai/AiToolSpecToOpenAI.java | 82 ++++++++ .../component/openai/OpenAIConfiguration.java | 15 ++ .../component/openai/OpenAIProducer.java | 147 ++++++++++++-- .../openai/OpenAIToolExecutionProducer.java | 184 +++++++++++++----- .../openai/AiToolSpecToOpenAITest.java | 137 +++++++++++++ .../openai/OpenAIToolErrorStrategyTest.java | 2 +- 10 files changed, 537 insertions(+), 78 deletions(-) create mode 100644 components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/AiToolSpecToOpenAI.java create mode 100644 components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/AiToolSpecToOpenAITest.java diff --git a/components/camel-ai/camel-openai/pom.xml b/components/camel-ai/camel-openai/pom.xml index 9bafddb02bd22..251a9ed5e2758 100644 --- a/components/camel-ai/camel-openai/pom.xml +++ b/components/camel-ai/camel-openai/pom.xml @@ -43,6 +43,11 @@ camel-support + + org.apache.camel + camel-ai-tool + + com.openai openai-java diff --git a/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointConfigurer.java b/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointConfigurer.java index 825570b9a22be..8448d69b9bcde 100644 --- a/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointConfigurer.java +++ b/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointConfigurer.java @@ -140,6 +140,7 @@ public boolean configure(CamelContext camelContext, Object obj, String name, Obj case "stripThinking": target.getConfiguration().setStripThinking(property(camelContext, boolean.class, value)); return true; case "systemmessage": case "systemMessage": target.getConfiguration().setSystemMessage(property(camelContext, java.lang.String.class, value)); return true; + case "tags": target.getConfiguration().setTags(property(camelContext, java.lang.String.class, value)); return true; case "temperature": target.getConfiguration().setTemperature(property(camelContext, java.lang.Double.class, value)); return true; case "toolexecutionerrorstrategy": case "toolExecutionErrorStrategy": target.getConfiguration().setToolExecutionErrorStrategy(property(camelContext, org.apache.camel.component.openai.ToolExecutionErrorStrategy.class, value)); return true; @@ -271,6 +272,7 @@ public Class getOptionType(String name, boolean ignoreCase) { case "stripThinking": return boolean.class; case "systemmessage": case "systemMessage": return java.lang.String.class; + case "tags": return java.lang.String.class; case "temperature": return java.lang.Double.class; case "toolexecutionerrorstrategy": case "toolExecutionErrorStrategy": return org.apache.camel.component.openai.ToolExecutionErrorStrategy.class; @@ -403,6 +405,7 @@ public Object getOptionValue(Object obj, String name, boolean ignoreCase) { case "stripThinking": return target.getConfiguration().isStripThinking(); case "systemmessage": case "systemMessage": return target.getConfiguration().getSystemMessage(); + case "tags": return target.getConfiguration().getTags(); case "temperature": return target.getConfiguration().getTemperature(); case "toolexecutionerrorstrategy": case "toolExecutionErrorStrategy": return target.getConfiguration().getToolExecutionErrorStrategy(); diff --git a/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointUriFactory.java b/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointUriFactory.java index 60bf0d2c653de..b0ecf1364fa60 100644 --- a/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointUriFactory.java +++ b/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointUriFactory.java @@ -24,7 +24,7 @@ public class OpenAIEndpointUriFactory extends org.apache.camel.support.component private static final Set ENDPOINT_IDENTITY_PROPERTY_NAMES; private static final Map MULTI_VALUE_PREFIXES; static { - Set props = new HashSet<>(65); + Set props = new HashSet<>(66); props.add("additionalBodyProperty"); props.add("additionalHeader"); props.add("additionalResponseHeader"); @@ -86,6 +86,7 @@ public class OpenAIEndpointUriFactory extends org.apache.camel.support.component props.add("streaming"); props.add("stripThinking"); props.add("systemMessage"); + props.add("tags"); props.add("temperature"); props.add("toolExecutionErrorStrategy"); props.add("topP"); 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 75596ffafff6b..7bddb08018fd9 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 @@ -133,23 +133,24 @@ "streaming": { "index": 44, "kind": "parameter", "displayName": "Streaming", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Enable streaming responses" }, "stripThinking": { "index": 45, "kind": "parameter", "displayName": "Strip Thinking", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Strip ... blocks from model responses (used by reasoning models like Qwen3, DeepSeek-R1). The thinking content is stored in the CamelOpenAIThinkingContent header." }, "systemMessage": { "index": 46, "kind": "parameter", "displayName": "System Message", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "System message to prepend. When set and conversationMemory is enabled, the conversation history is reset." }, - "temperature": { "index": 47, "kind": "parameter", "displayName": "Temperature", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Temperature for response generation (0.0 to 2.0)" }, - "toolExecutionErrorStrategy": { "index": 48, "kind": "parameter", "displayName": "Tool Execution Error Strategy", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "org.apache.camel.component.openai.ToolExecutionErrorStrategy", "enum": [ "failExchange", "repromptModel" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "failExchange", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Strategy for handling exceptions thrown during MCP tool execution. 'failExchange' (default) propagates the exception to the Camel exchange so that standard Camel error handling (onException, dead-letter channel) can process it. This is the safer default because 'repromptModel' sends raw exception messages (which may contain connection strings, hostnames, or internal paths) to a third-party LLM provider. 'repromptModel' catches the error and sends it back to the model as a tool result so the model can attempt to recover." }, - "topP": { "index": 49, "kind": "parameter", "displayName": "Top P", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Top P for response generation (0.0 to 1.0)" }, - "userMessage": { "index": 50, "kind": "parameter", "displayName": "User Message", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Default user message text to use when no prompt is provided" }, - "lazyStartProducer": { "index": 51, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel's routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing." }, - "oauthProfile": { "index": 52, "kind": "parameter", "displayName": "Oauth Profile", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "OAuth profile name for obtaining an access token via the OAuth 2.0 Client Credentials grant. When set, the token is acquired from the configured identity provider and used instead of apiKey. Requires camel-oauth on the classpath. The profile properties are resolved from camel.oauth..client-id, camel.oauth..client-secret, and camel.oauth..token-endpoint." }, - "sslContextParameters": { "index": 53, "kind": "parameter", "displayName": "Ssl Context Parameters", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.apache.camel.support.jsse.SSLContextParameters", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "SSLContextParameters to use for configuring SSL\/TLS. When set, takes precedence over the individual sslTruststore, sslKeystore, and sslProtocol options." }, - "sslEndpointAlgorithm": { "index": 54, "kind": "parameter", "displayName": "Ssl Endpoint Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "security": "insecure:ssl", "insecureValue": "none", "defaultValue": "https", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The endpoint identification algorithm to validate the server hostname using the server certificate. Set to an empty string or 'none' to disable hostname verification" }, - "sslKeymanagerAlgorithm": { "index": 55, "kind": "parameter", "displayName": "Ssl Keymanager Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "SunX509", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The algorithm used by the key manager factory for SSL connections" }, - "sslKeyPassword": { "index": 56, "kind": "parameter", "displayName": "Ssl Key Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The password of the private key in the key store file" }, - "sslKeystoreLocation": { "index": 57, "kind": "parameter", "displayName": "Ssl Keystore Location", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The location of the key store file. This is optional and can be used for two-way authentication for the OpenAI API" }, - "sslKeystorePassword": { "index": 58, "kind": "parameter", "displayName": "Ssl Keystore Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The store password for the key store file" }, - "sslKeystoreType": { "index": 59, "kind": "parameter", "displayName": "Ssl Keystore Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "JKS", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The file format of the key store file" }, - "sslProtocol": { "index": 60, "kind": "parameter", "displayName": "Ssl Protocol", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "TLSv1.3", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The SSL protocol used to generate the SSLContext" }, - "sslTrustmanagerAlgorithm": { "index": 61, "kind": "parameter", "displayName": "Ssl Trustmanager Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "PKIX", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The algorithm used by the trust manager factory for SSL connections" }, - "sslTruststoreLocation": { "index": 62, "kind": "parameter", "displayName": "Ssl Truststore Location", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The location of the trust store file, used to validate the server's certificate" }, - "sslTruststorePassword": { "index": 63, "kind": "parameter", "displayName": "Ssl Truststore Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The password for the trust store file. If a password is not set, the configured trust store can still be used, but integrity checking is disabled" }, - "sslTruststoreType": { "index": 64, "kind": "parameter", "displayName": "Ssl Truststore Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "JKS", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The file format of the trust store file" } + "tags": { "index": 47, "kind": "parameter", "displayName": "Tags", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Comma-separated tags to filter tools from the shared AiToolRegistry. Tools registered via ai-tool: consumer endpoints with matching tags are discovered and exposed as OpenAI function-calling tools alongside any MCP tools. Tools with no tags (default pool) are always included." }, + "temperature": { "index": 48, "kind": "parameter", "displayName": "Temperature", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Temperature for response generation (0.0 to 2.0)" }, + "toolExecutionErrorStrategy": { "index": 49, "kind": "parameter", "displayName": "Tool Execution Error Strategy", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "org.apache.camel.component.openai.ToolExecutionErrorStrategy", "enum": [ "failExchange", "repromptModel" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "failExchange", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Strategy for handling exceptions thrown during MCP tool execution. 'failExchange' (default) propagates the exception to the Camel exchange so that standard Camel error handling (onException, dead-letter channel) can process it. This is the safer default because 'repromptModel' sends raw exception messages (which may contain connection strings, hostnames, or internal paths) to a third-party LLM provider. 'repromptModel' catches the error and sends it back to the model as a tool result so the model can attempt to recover." }, + "topP": { "index": 50, "kind": "parameter", "displayName": "Top P", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Top P for response generation (0.0 to 1.0)" }, + "userMessage": { "index": 51, "kind": "parameter", "displayName": "User Message", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Default user message text to use when no prompt is provided" }, + "lazyStartProducer": { "index": 52, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel's routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing." }, + "oauthProfile": { "index": 53, "kind": "parameter", "displayName": "Oauth Profile", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "OAuth profile name for obtaining an access token via the OAuth 2.0 Client Credentials grant. When set, the token is acquired from the configured identity provider and used instead of apiKey. Requires camel-oauth on the classpath. The profile properties are resolved from camel.oauth..client-id, camel.oauth..client-secret, and camel.oauth..token-endpoint." }, + "sslContextParameters": { "index": 54, "kind": "parameter", "displayName": "Ssl Context Parameters", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.apache.camel.support.jsse.SSLContextParameters", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "SSLContextParameters to use for configuring SSL\/TLS. When set, takes precedence over the individual sslTruststore, sslKeystore, and sslProtocol options." }, + "sslEndpointAlgorithm": { "index": 55, "kind": "parameter", "displayName": "Ssl Endpoint Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "security": "insecure:ssl", "insecureValue": "none", "defaultValue": "https", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The endpoint identification algorithm to validate the server hostname using the server certificate. Set to an empty string or 'none' to disable hostname verification" }, + "sslKeymanagerAlgorithm": { "index": 56, "kind": "parameter", "displayName": "Ssl Keymanager Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "SunX509", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The algorithm used by the key manager factory for SSL connections" }, + "sslKeyPassword": { "index": 57, "kind": "parameter", "displayName": "Ssl Key Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The password of the private key in the key store file" }, + "sslKeystoreLocation": { "index": 58, "kind": "parameter", "displayName": "Ssl Keystore Location", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The location of the key store file. This is optional and can be used for two-way authentication for the OpenAI API" }, + "sslKeystorePassword": { "index": 59, "kind": "parameter", "displayName": "Ssl Keystore Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The store password for the key store file" }, + "sslKeystoreType": { "index": 60, "kind": "parameter", "displayName": "Ssl Keystore Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "JKS", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The file format of the key store file" }, + "sslProtocol": { "index": 61, "kind": "parameter", "displayName": "Ssl Protocol", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "TLSv1.3", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The SSL protocol used to generate the SSLContext" }, + "sslTrustmanagerAlgorithm": { "index": 62, "kind": "parameter", "displayName": "Ssl Trustmanager Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "PKIX", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The algorithm used by the trust manager factory for SSL connections" }, + "sslTruststoreLocation": { "index": 63, "kind": "parameter", "displayName": "Ssl Truststore Location", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The location of the trust store file, used to validate the server's certificate" }, + "sslTruststorePassword": { "index": 64, "kind": "parameter", "displayName": "Ssl Truststore Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The password for the trust store file. If a password is not set, the configured trust store can still be used, but integrity checking is disabled" }, + "sslTruststoreType": { "index": 65, "kind": "parameter", "displayName": "Ssl Truststore Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "JKS", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The file format of the trust store file" } } } diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/AiToolSpecToOpenAI.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/AiToolSpecToOpenAI.java new file mode 100644 index 0000000000000..bb29b9c2e0edb --- /dev/null +++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/AiToolSpecToOpenAI.java @@ -0,0 +1,82 @@ +/* + * 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 java.util.Map; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.openai.core.JsonValue; +import com.openai.models.FunctionDefinition; +import com.openai.models.FunctionParameters; +import com.openai.models.chat.completions.ChatCompletionFunctionTool; +import org.apache.camel.component.ai.tool.AiToolSpec; + +/** + * Converts {@link AiToolSpec} instances to OpenAI {@link ChatCompletionFunctionTool} objects. + *

+ * Uses the pre-built JSON Schema string from {@link AiToolSpec#getParametersJsonSchema()} and parses it into the OpenAI + * SDK's {@link FunctionParameters} format via {@link JsonValue#from(Object)}. + */ +final class AiToolSpecToOpenAI { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final TypeReference> MAP_TYPE = new TypeReference<>() { + }; + + private AiToolSpecToOpenAI() { + } + + /** + * Converts an {@link AiToolSpec} to an OpenAI {@link ChatCompletionFunctionTool}. + * + * @param spec the tool specification to convert + * @return the OpenAI function tool definition + */ + static ChatCompletionFunctionTool toFunctionTool(AiToolSpec spec) { + FunctionDefinition.Builder funcBuilder = FunctionDefinition.builder() + .name(spec.getName()); + + if (spec.getDescription() != null) { + funcBuilder.description(spec.getDescription()); + } + + String jsonSchema = spec.getParametersJsonSchema(); + if (jsonSchema != null && !jsonSchema.isEmpty()) { + try { + Map schemaMap = OBJECT_MAPPER.readValue(jsonSchema, MAP_TYPE); + FunctionParameters.Builder paramsBuilder = FunctionParameters.builder(); + + if (!schemaMap.containsKey("type")) { + paramsBuilder.putAdditionalProperty("type", JsonValue.from("object")); + } + for (Map.Entry entry : schemaMap.entrySet()) { + paramsBuilder.putAdditionalProperty(entry.getKey(), JsonValue.from(entry.getValue())); + } + + funcBuilder.parameters(paramsBuilder.build()); + } catch (Exception e) { + throw new IllegalArgumentException( + "Failed to parse JSON Schema for tool '" + spec.getName() + "': " + e.getMessage(), e); + } + } + + return ChatCompletionFunctionTool.builder() + .function(funcBuilder.build()) + .build(); + } +} diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConfiguration.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConfiguration.java index 0c866ce17662d..f5080e90aa90f 100644 --- a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConfiguration.java +++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConfiguration.java @@ -167,6 +167,13 @@ public class OpenAIConfiguration implements Cloneable { + "(e.g. additionalResponseHeader.reasoning_content=CamelMyReasoningHeader)") private Map additionalResponseHeader; + @UriParam + @Metadata(description = "Comma-separated tags to filter tools from the shared AiToolRegistry. " + + "Tools registered via ai-tool: consumer endpoints with matching tags " + + "are discovered and exposed as OpenAI function-calling tools alongside " + + "any MCP tools. Tools with no tags (default pool) are always included.") + private String tags; + @UriParam(prefix = "mcpServer.", multiValue = true) @Metadata(description = "MCP (Model Context Protocol) server configurations. " + "Define servers using prefix notation: mcpServer..transportType=stdio|sse|streamableHttp, (Note that sse is deprecated) " @@ -682,6 +689,14 @@ public void setSpeechInstructions(String speechInstructions) { this.speechInstructions = speechInstructions; } + public String getTags() { + return tags; + } + + public void setTags(String tags) { + this.tags = tags; + } + public Map getMcpServer() { return mcpServer; } 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 d36c7d6e48e2b..9240a3bf9418c 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 @@ -24,8 +24,10 @@ import java.util.ArrayList; import java.util.Base64; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -59,8 +61,14 @@ import org.apache.camel.Exchange; import org.apache.camel.Message; import org.apache.camel.WrappedFile; +import org.apache.camel.component.ai.tool.AiToolExecutor; +import org.apache.camel.component.ai.tool.AiToolParameterHelper; +import org.apache.camel.component.ai.tool.AiToolRegistry; +import org.apache.camel.component.ai.tool.AiToolResult; +import org.apache.camel.component.ai.tool.AiToolSpec; import org.apache.camel.spi.Synchronization; import org.apache.camel.support.DefaultAsyncProducer; +import org.apache.camel.support.ExchangeHelper; import org.apache.camel.support.ResourceHelper; import org.apache.camel.util.ObjectHelper; import org.slf4j.Logger; @@ -204,15 +212,26 @@ private void processInternal(Exchange exchange) throws Exception { } } + // Discover Camel route tools from AiToolRegistry by tags + Map camelRouteTools = discoverCamelRouteTools(config); + boolean hasCamelRouteTools = !camelRouteTools.isEmpty(); + if (hasCamelRouteTools) { + for (AiToolSpec spec : camelRouteTools.values()) { + paramsBuilder.addTool(AiToolSpecToOpenAI.toFunctionTool(spec)); + } + } + + boolean hasAnyTools = hasMcpTools || hasCamelRouteTools; + ChatCompletionCreateParams params = paramsBuilder.build(); - if (Boolean.TRUE.equals(streaming) && hasMcpTools && config.isAutoToolExecution()) { - LOG.info("Streaming with MCP tools is not supported; falling back to non-streaming for the agentic loop"); - processNonStreaming(exchange, params, config); + if (Boolean.TRUE.equals(streaming) && hasAnyTools && config.isAutoToolExecution()) { + LOG.info("Streaming with tools is not supported; falling back to non-streaming for the agentic loop"); + processNonStreaming(exchange, params, config, camelRouteTools); } else if (Boolean.TRUE.equals(streaming)) { processStreaming(exchange, params); } else { - processNonStreaming(exchange, params, config); + processNonStreaming(exchange, params, config, camelRouteTools); } } @@ -437,17 +456,20 @@ private ChatCompletionContentPart createTextContentPart(String text) { .build()); } - private void processNonStreaming(Exchange exchange, ChatCompletionCreateParams params, OpenAIConfiguration config) + private void processNonStreaming( + Exchange exchange, ChatCompletionCreateParams params, OpenAIConfiguration config, + Map camelRouteTools) throws Exception { List mcpTools = getEndpoint().getMcpToolState().tools(); boolean hasMcpTools = mcpTools != null && !mcpTools.isEmpty(); + boolean hasAnyTools = hasMcpTools || !camelRouteTools.isEmpty(); - if (!hasMcpTools || !config.isAutoToolExecution()) { - // Path A: No MCP tools or auto-execution disabled -- existing behavior + if (!hasAnyTools || !config.isAutoToolExecution()) { + // Path A: No tools or auto-execution disabled -- existing behavior processNonStreamingSimple(exchange, params, config); } else { - // Path B: MCP tools with agentic loop - processNonStreamingAgentic(exchange, params, config); + // Path B: Tools with agentic loop (MCP and/or Camel route tools) + processNonStreamingAgentic(exchange, params, config, camelRouteTools); } } @@ -474,12 +496,17 @@ private void processNonStreamingSimple( } private void processNonStreamingAgentic( - Exchange exchange, ChatCompletionCreateParams params, OpenAIConfiguration config) + Exchange exchange, ChatCompletionCreateParams params, OpenAIConfiguration config, + Map camelRouteTools) throws Exception { int maxIterations = config.getMaxToolIterations(); + + Set availableToolNames = new java.util.LinkedHashSet<>(); + availableToolNames.addAll(getEndpoint().getMcpToolState().toolClientMap().keySet()); + availableToolNames.addAll(camelRouteTools.keySet()); LOG.debug("Starting agentic loop with maxToolIterations={}, available tools: {}", maxIterations, - getEndpoint().getMcpToolState().toolClientMap().keySet()); + availableToolNames); // Rebuild the builder from the immutable params so we can accumulate messages ChatCompletionCreateParams.Builder paramsBuilder = params.toBuilder(); @@ -542,15 +569,26 @@ private void processNonStreamingAgentic( String toolCallId = toolCall.asFunction().id(); toolCallsLog.add(toolName); + // Check if the tool is a Camel route tool first + AiToolSpec camelSpec = camelRouteTools.get(toolName); + if (camelSpec != null) { + String resultContent = executeCamelRouteTool(camelSpec, argsJson, exchange, config); + LOG.debug("Camel route tool '{}' result: {}", toolName, resultContent); + batchResults.add(new ToolResultEntry(toolCallId, resultContent)); + allReturnDirect = false; // Camel route tools do not support returnDirect + continue; + } + + // Fall back to MCP tool dispatch McpToolState mcpToolState = getEndpoint().getMcpToolState(); McpSyncClient mcpClient = mcpToolState.toolClientMap().get(toolName); if (mcpClient == null) { if (config.getHallucinatedToolNameStrategy() == HallucinatedToolNameStrategy.FAIL_EXCHANGE) { throw new IllegalStateException( - "Tool '" + toolName + "' not found in any configured MCP server"); + "Tool '" + toolName + "' not found in any configured tool source"); } // repromptModel: send a corrective tool result listing available tools - String available = String.join(", ", mcpToolState.toolClientMap().keySet()); + String available = String.join(", ", availableToolNames); String errorMsg = "Error: tool '" + toolName + "' does not exist. Available tools: " + available; LOG.warn("Hallucinated tool name '{}', sending corrective result to model", toolName); @@ -633,6 +671,89 @@ private void processNonStreamingAgentic( "Max tool iterations (%d) exceeded. Tools called: %s".formatted(maxIterations, toolCallsLog)); } + /** + * Executes a Camel route tool via {@link AiToolExecutor}, handling the exchange lifecycle and error strategies. + */ + private String executeCamelRouteTool( + AiToolSpec spec, String argsJson, Exchange exchange, OpenAIConfiguration config) + throws Exception { + LOG.debug("Executing Camel route tool '{}' with args: {}", spec.getName(), argsJson); + + Map argsMap; + try { + if (argsJson == null || argsJson.trim().isEmpty()) { + argsMap = Map.of(); + } else { + argsMap = OBJECT_MAPPER.readValue(argsJson, Map.class); + } + } catch (JsonProcessingException e) { + if (config.getToolExecutionErrorStrategy() == ToolExecutionErrorStrategy.FAIL_EXCHANGE) { + throw e; + } + LOG.warn("Invalid tool arguments for Camel route tool '{}': {}", spec.getName(), argsJson, e); + return "Error: invalid tool arguments: " + e.getMessage(); + } + + // Isolate tool execution in its own exchange copy + Exchange toolExchange = ExchangeHelper.createCopy(exchange, true); + try { + AiToolResult result = AiToolExecutor.execute(spec, argsMap, toolExchange); + return handleCamelToolResult(spec.getName(), result, config); + } catch (Exception e) { + if (config.getToolExecutionErrorStrategy() == ToolExecutionErrorStrategy.FAIL_EXCHANGE) { + throw e; + } + LOG.warn("Camel route tool '{}' execution failed: {}", spec.getName(), e.getMessage(), e); + return "Error: Tool execution failed: " + e.getMessage(); + } + } + + /** + * Converts an {@link AiToolResult} to a string for the LLM, respecting the configured error strategy. + */ + private String handleCamelToolResult(String toolName, AiToolResult result, OpenAIConfiguration config) + throws Exception { + if (result instanceof AiToolResult.Success success) { + return success.value(); + } else if (result instanceof AiToolResult.ArgumentError error) { + if (config.getToolExecutionErrorStrategy() == ToolExecutionErrorStrategy.FAIL_EXCHANGE) { + throw error.cause(); + } + LOG.warn("Camel route tool '{}' argument error: {}", toolName, error.message(), error.cause()); + return "Error: invalid tool arguments: " + error.message(); + } else if (result instanceof AiToolResult.ExecutionError error) { + if (config.getToolExecutionErrorStrategy() == ToolExecutionErrorStrategy.FAIL_EXCHANGE) { + throw error.cause(); + } + LOG.warn("Camel route tool '{}' execution error: {}", toolName, error.message(), error.cause()); + return "Error: Tool execution failed"; + } + return "Tool execution failed"; + } + + /** + * Discovers Camel route tools from the shared {@link AiToolRegistry} based on the configured tags. + */ + private Map discoverCamelRouteTools(OpenAIConfiguration config) { + String tags = config.getTags(); + if (ObjectHelper.isEmpty(tags)) { + return Map.of(); + } + + AiToolRegistry registry = AiToolRegistry.getOrCreate(getEndpoint().getCamelContext()); + String[] tagArray = AiToolParameterHelper.splitTags(tags); + + Map toolsByName = new LinkedHashMap<>(); + for (String tag : tagArray) { + for (AiToolSpec spec : registry.getToolsByTag(tag.trim())) { + toolsByName.putIfAbsent(spec.getName(), spec); + } + } + + LOG.debug("Discovered {} Camel route tools for tags: {}", toolsByName.size(), tags); + return toolsByName; + } + private void setAgenticTokenHeaders(Message message, OpenAIAgenticTokenTracker tokenTracker) { message.setHeader(OpenAIConstants.AGENTIC_PROMPT_TOKENS, tokenTracker.getPromptTokens()); message.setHeader(OpenAIConstants.AGENTIC_COMPLETION_TOKENS, tokenTracker.getCompletionTokens()); diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIToolExecutionProducer.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIToolExecutionProducer.java index a5313f0d70894..ca305c5c66cad 100644 --- a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIToolExecutionProducer.java +++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIToolExecutionProducer.java @@ -17,8 +17,11 @@ package org.apache.camel.component.openai; import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import com.fasterxml.jackson.core.JsonProcessingException; @@ -32,7 +35,14 @@ import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.spec.McpSchema; import org.apache.camel.Exchange; +import org.apache.camel.component.ai.tool.AiToolExecutor; +import org.apache.camel.component.ai.tool.AiToolParameterHelper; +import org.apache.camel.component.ai.tool.AiToolRegistry; +import org.apache.camel.component.ai.tool.AiToolResult; +import org.apache.camel.component.ai.tool.AiToolSpec; import org.apache.camel.support.DefaultProducer; +import org.apache.camel.support.ExchangeHelper; +import org.apache.camel.util.ObjectHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -124,64 +134,80 @@ public void process(Exchange exchange) throws Exception { .toolCalls(toolCalls) .build())); - // Execute each tool call via MCP and add tool result messages - if (getEndpoint().getMcpToolState().toolClientMap().isEmpty()) { + // Discover Camel route tools from AiToolRegistry + Map camelRouteTools = discoverCamelRouteTools(config); + boolean hasMcpTools = !getEndpoint().getMcpToolState().toolClientMap().isEmpty(); + boolean hasCamelRouteTools = !camelRouteTools.isEmpty(); + + if (!hasMcpTools && !hasCamelRouteTools) { throw new IllegalStateException( - "No MCP tool clients configured on the endpoint. Configure mcpServer.* parameters."); + "No tool sources configured on the endpoint. Configure mcpServer.* parameters or tags for Camel route tools."); } + // Build the available tool name set for hallucinated tool handling + Set availableToolNames = new LinkedHashSet<>(); + availableToolNames.addAll(getEndpoint().getMcpToolState().toolClientMap().keySet()); + availableToolNames.addAll(camelRouteTools.keySet()); + int executedCount = 0; for (ChatCompletionMessageToolCall toolCall : toolCalls) { String toolName = toolCall.asFunction().function().name(); String argsJson = toolCall.asFunction().function().arguments(); String toolCallId = toolCall.asFunction().id(); - McpToolState mcpToolState = getEndpoint().getMcpToolState(); - McpSyncClient mcpClient = mcpToolState.toolClientMap().get(toolName); - if (mcpClient == null) { - if (config.getHallucinatedToolNameStrategy() == HallucinatedToolNameStrategy.FAIL_EXCHANGE) { - throw new IllegalStateException( - "Tool '" + toolName + "' not found in any configured MCP server"); - } - // repromptModel: send a corrective tool result listing available tools - String available = String.join(", ", mcpToolState.toolClientMap().keySet()); - String errorMsg = "Error: tool '" + toolName - + "' does not exist. Available tools: " + available; - LOG.warn("Hallucinated tool name '{}', sending corrective result to model", toolName); - history.add(ChatCompletionMessageParam.ofTool( - ChatCompletionToolMessageParam.builder() - .toolCallId(toolCallId) - .content(errorMsg) - .build())); - executedCount++; - continue; - } - String resultContent; - try { - Map argsMap = OBJECT_MAPPER.readValue(argsJson, Map.class); - McpSchema.CallToolResult toolResult - = getEndpoint().callTool(mcpClient, toolName, argsMap); - - if (Boolean.TRUE.equals(toolResult.isError())) { - resultContent = "Error: " + extractTextContent(toolResult.content()); - LOG.warn("MCP tool '{}' returned error: {}", toolName, resultContent); - } else { - resultContent = extractTextContent(toolResult.content()); - } - } catch (JsonProcessingException e) { - if (config.getToolExecutionErrorStrategy() == ToolExecutionErrorStrategy.FAIL_EXCHANGE) { - throw e; + // Check if the tool is a Camel route tool first + AiToolSpec camelSpec = camelRouteTools.get(toolName); + if (camelSpec != null) { + resultContent = executeCamelRouteTool(camelSpec, argsJson, exchange, config); + } else { + // Fall back to MCP tool dispatch + McpToolState mcpToolState = getEndpoint().getMcpToolState(); + McpSyncClient mcpClient = mcpToolState.toolClientMap().get(toolName); + if (mcpClient == null) { + if (config.getHallucinatedToolNameStrategy() == HallucinatedToolNameStrategy.FAIL_EXCHANGE) { + throw new IllegalStateException( + "Tool '" + toolName + "' not found in any configured tool source"); + } + // repromptModel: send a corrective tool result listing available tools + String available = String.join(", ", availableToolNames); + String errorMsg = "Error: tool '" + toolName + + "' does not exist. Available tools: " + available; + LOG.warn("Hallucinated tool name '{}', sending corrective result to model", toolName); + history.add(ChatCompletionMessageParam.ofTool( + ChatCompletionToolMessageParam.builder() + .toolCallId(toolCallId) + .content(errorMsg) + .build())); + executedCount++; + continue; } - LOG.warn("Invalid tool arguments for '{}': {}", toolName, argsJson, e); - resultContent = "Error: invalid tool arguments: " + e.getMessage(); - } catch (Exception e) { - if (config.getToolExecutionErrorStrategy() == ToolExecutionErrorStrategy.FAIL_EXCHANGE) { - throw e; + + try { + Map argsMap = OBJECT_MAPPER.readValue(argsJson, Map.class); + McpSchema.CallToolResult toolResult + = getEndpoint().callTool(mcpClient, toolName, argsMap); + + if (Boolean.TRUE.equals(toolResult.isError())) { + resultContent = "Error: " + extractTextContent(toolResult.content()); + LOG.warn("MCP tool '{}' returned error: {}", toolName, resultContent); + } else { + resultContent = extractTextContent(toolResult.content()); + } + } catch (JsonProcessingException e) { + if (config.getToolExecutionErrorStrategy() == ToolExecutionErrorStrategy.FAIL_EXCHANGE) { + throw e; + } + LOG.warn("Invalid tool arguments for '{}': {}", toolName, argsJson, e); + resultContent = "Error: invalid tool arguments: " + e.getMessage(); + } catch (Exception e) { + if (config.getToolExecutionErrorStrategy() == ToolExecutionErrorStrategy.FAIL_EXCHANGE) { + throw e; + } + LOG.warn("MCP tool '{}' execution failed: {}", toolName, e.getMessage(), e); + resultContent = "Error: Tool execution failed: " + e.getMessage(); } - LOG.warn("MCP tool '{}' execution failed: {}", toolName, e.getMessage(), e); - resultContent = "Error: Tool execution failed: " + e.getMessage(); } history.add(ChatCompletionMessageParam.ofTool( @@ -198,6 +224,74 @@ public void process(Exchange exchange) throws Exception { exchange.getMessage().setHeader(OpenAIConstants.TOOL_ITERATIONS, executedCount); } + private Map discoverCamelRouteTools(OpenAIConfiguration config) { + String tags = config.getTags(); + if (ObjectHelper.isEmpty(tags)) { + return Map.of(); + } + + AiToolRegistry registry = AiToolRegistry.getOrCreate(getEndpoint().getCamelContext()); + String[] tagArray = AiToolParameterHelper.splitTags(tags); + + Map toolsByName = new LinkedHashMap<>(); + for (String tag : tagArray) { + for (AiToolSpec spec : registry.getToolsByTag(tag.trim())) { + toolsByName.putIfAbsent(spec.getName(), spec); + } + } + + LOG.debug("Discovered {} Camel route tools for tags: {}", toolsByName.size(), tags); + return toolsByName; + } + + private String executeCamelRouteTool( + AiToolSpec spec, String argsJson, Exchange exchange, OpenAIConfiguration config) + throws Exception { + LOG.debug("Executing Camel route tool '{}' with args: {}", spec.getName(), argsJson); + + Map argsMap; + try { + if (argsJson == null || argsJson.trim().isEmpty()) { + argsMap = Map.of(); + } else { + argsMap = OBJECT_MAPPER.readValue(argsJson, Map.class); + } + } catch (JsonProcessingException e) { + if (config.getToolExecutionErrorStrategy() == ToolExecutionErrorStrategy.FAIL_EXCHANGE) { + throw e; + } + LOG.warn("Invalid tool arguments for Camel route tool '{}': {}", spec.getName(), argsJson, e); + return "Error: invalid tool arguments: " + e.getMessage(); + } + + Exchange toolExchange = ExchangeHelper.createCopy(exchange, true); + try { + AiToolResult result = AiToolExecutor.execute(spec, argsMap, toolExchange); + if (result instanceof AiToolResult.Success success) { + return success.value(); + } else if (result instanceof AiToolResult.ArgumentError error) { + if (config.getToolExecutionErrorStrategy() == ToolExecutionErrorStrategy.FAIL_EXCHANGE) { + throw error.cause(); + } + LOG.warn("Camel route tool '{}' argument error: {}", spec.getName(), error.message(), error.cause()); + return "Error: invalid tool arguments: " + error.message(); + } else if (result instanceof AiToolResult.ExecutionError error) { + if (config.getToolExecutionErrorStrategy() == ToolExecutionErrorStrategy.FAIL_EXCHANGE) { + throw error.cause(); + } + LOG.warn("Camel route tool '{}' execution error: {}", spec.getName(), error.message(), error.cause()); + return "Error: Tool execution failed"; + } + return "Tool execution failed"; + } catch (Exception e) { + if (config.getToolExecutionErrorStrategy() == ToolExecutionErrorStrategy.FAIL_EXCHANGE) { + throw e; + } + LOG.warn("Camel route tool '{}' execution failed: {}", spec.getName(), e.getMessage(), e); + return "Error: Tool execution failed: " + e.getMessage(); + } + } + private String extractTextContent(List contents) { if (contents == null || contents.isEmpty()) { return ""; diff --git a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/AiToolSpecToOpenAITest.java b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/AiToolSpecToOpenAITest.java new file mode 100644 index 0000000000000..1afdaa1a788a9 --- /dev/null +++ b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/AiToolSpecToOpenAITest.java @@ -0,0 +1,137 @@ +/* + * 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 java.util.Map; + +import com.openai.core.JsonValue; +import com.openai.models.FunctionParameters; +import com.openai.models.chat.completions.ChatCompletionFunctionTool; +import org.apache.camel.component.ai.tool.AiToolParameterHelper; +import org.apache.camel.component.ai.tool.AiToolSpec; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class AiToolSpecToOpenAITest { + + @Test + void convertFullSpec() { + Map params = Map.of( + "city", "string", + "city.description", "The city name", + "city.required", "true", + "unit", "string", + "unit.enum", "celsius,fahrenheit", + "unit.description", "Temperature unit"); + + Map defs = AiToolParameterHelper.parseParameterMetadata(params); + String jsonSchema = AiToolParameterHelper.buildJsonSchema(params); + + AiToolSpec spec = new AiToolSpec("getWeather", "Get current weather", defs, jsonSchema, null); + + ChatCompletionFunctionTool result = AiToolSpecToOpenAI.toFunctionTool(spec); + + assertThat(result.function().name()).isEqualTo("getWeather"); + assertThat(result.function().description()).hasValue("Get current weather"); + assertThat(result.function().parameters()).isPresent(); + + FunctionParameters parameters = result.function().parameters().get(); + Map props = parameters._additionalProperties(); + + assertThat(props.get("type").asString()).contains("object"); + assertThat(props).containsKey("properties"); + } + + @Test + void convertSpecWithoutParameters() { + AiToolSpec spec = new AiToolSpec("noParams", "A tool with no parameters", Map.of(), null, null); + + ChatCompletionFunctionTool result = AiToolSpecToOpenAI.toFunctionTool(spec); + + assertThat(result.function().name()).isEqualTo("noParams"); + assertThat(result.function().description()).hasValue("A tool with no parameters"); + assertThat(result.function().parameters()).isEmpty(); + } + + @Test + void convertSpecWithoutDescription() { + AiToolSpec spec = new AiToolSpec("bareTool", null, Map.of(), null, null); + + ChatCompletionFunctionTool result = AiToolSpecToOpenAI.toFunctionTool(spec); + + assertThat(result.function().name()).isEqualTo("bareTool"); + assertThat(result.function().description()).isEmpty(); + } + + @Test + void convertSpecDefaultsTypeToObject() { + // JSON Schema without "type" key should get "object" defaulted + String jsonSchema = "{\"properties\":{\"x\":{\"type\":\"string\"}}}"; + AiToolSpec spec = new AiToolSpec("testTool", "Test", Map.of(), jsonSchema, null); + + ChatCompletionFunctionTool result = AiToolSpecToOpenAI.toFunctionTool(spec); + + assertThat(result.function().parameters()).isPresent(); + FunctionParameters parameters = result.function().parameters().get(); + assertThat(parameters._additionalProperties().get("type").asString()).contains("object"); + } + + @Test + void convertSpecPreservesRequiredArray() { + String jsonSchema = "{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]}"; + AiToolSpec spec = new AiToolSpec("withRequired", "Has required", Map.of(), jsonSchema, null); + + ChatCompletionFunctionTool result = AiToolSpecToOpenAI.toFunctionTool(spec); + + FunctionParameters parameters = result.function().parameters().get(); + assertThat(parameters._additionalProperties()).containsKey("required"); + assertThat(parameters._additionalProperties().get("required").asArray()).isNotEmpty(); + } + + @Test + void convertSpecWithEmptyJsonSchema() { + AiToolSpec spec = new AiToolSpec("emptySchema", "Empty", Map.of(), "", null); + + ChatCompletionFunctionTool result = AiToolSpecToOpenAI.toFunctionTool(spec); + + assertThat(result.function().name()).isEqualTo("emptySchema"); + assertThat(result.function().parameters()).isEmpty(); + } + + @Test + void convertSpecWithInvalidJsonSchemaThrows() { + AiToolSpec spec = new AiToolSpec("badSchema", "Bad", Map.of(), "not valid json", null); + + assertThatThrownBy(() -> AiToolSpecToOpenAI.toFunctionTool(spec)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Failed to parse JSON Schema for tool 'badSchema'"); + } + + @Test + void convertSpecPreservesAdditionalPropertiesFalse() { + String jsonSchema = "{\"type\":\"object\",\"properties\":{\"q\":{\"type\":\"string\"}},\"additionalProperties\":false}"; + AiToolSpec spec = new AiToolSpec("strict", "Strict tool", Map.of(), jsonSchema, null); + + ChatCompletionFunctionTool result = AiToolSpecToOpenAI.toFunctionTool(spec); + + FunctionParameters parameters = result.function().parameters().get(); + assertThat(parameters._additionalProperties()).containsKey("additionalProperties"); + assertThat(parameters._additionalProperties().get("additionalProperties").asBoolean()).contains(false); + } +} diff --git a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIToolErrorStrategyTest.java b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIToolErrorStrategyTest.java index 9a1cbcad3df16..f09d18f691098 100644 --- a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIToolErrorStrategyTest.java +++ b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIToolErrorStrategyTest.java @@ -185,7 +185,7 @@ void hallucinatedToolNameDefaultFailExchangeThrowsException() { .isInstanceOf(CamelExecutionException.class) .hasCauseInstanceOf(IllegalStateException.class) .cause() - .hasMessageContaining("not found in any configured MCP server"); + .hasMessageContaining("not found in any configured tool source"); } @Test From 3a1782355c2900b097b887fb70dcaa17e948e547 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Tue, 4 Aug 2026 00:14:27 +0200 Subject: [PATCH 2/2] CAMEL-24322: Regenerate catalog and endpoint DSL for tags parameter Co-Authored-By: Claude Opus 4.6 --- .../camel/catalog/components/openai.json | 37 ++++++++++--------- .../dsl/OpenAIEndpointBuilderFactory.java | 17 +++++++++ 2 files changed, 36 insertions(+), 18 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 75596ffafff6b..7bddb08018fd9 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 @@ -133,23 +133,24 @@ "streaming": { "index": 44, "kind": "parameter", "displayName": "Streaming", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Enable streaming responses" }, "stripThinking": { "index": 45, "kind": "parameter", "displayName": "Strip Thinking", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Strip ... blocks from model responses (used by reasoning models like Qwen3, DeepSeek-R1). The thinking content is stored in the CamelOpenAIThinkingContent header." }, "systemMessage": { "index": 46, "kind": "parameter", "displayName": "System Message", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "System message to prepend. When set and conversationMemory is enabled, the conversation history is reset." }, - "temperature": { "index": 47, "kind": "parameter", "displayName": "Temperature", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Temperature for response generation (0.0 to 2.0)" }, - "toolExecutionErrorStrategy": { "index": 48, "kind": "parameter", "displayName": "Tool Execution Error Strategy", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "org.apache.camel.component.openai.ToolExecutionErrorStrategy", "enum": [ "failExchange", "repromptModel" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "failExchange", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Strategy for handling exceptions thrown during MCP tool execution. 'failExchange' (default) propagates the exception to the Camel exchange so that standard Camel error handling (onException, dead-letter channel) can process it. This is the safer default because 'repromptModel' sends raw exception messages (which may contain connection strings, hostnames, or internal paths) to a third-party LLM provider. 'repromptModel' catches the error and sends it back to the model as a tool result so the model can attempt to recover." }, - "topP": { "index": 49, "kind": "parameter", "displayName": "Top P", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Top P for response generation (0.0 to 1.0)" }, - "userMessage": { "index": 50, "kind": "parameter", "displayName": "User Message", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Default user message text to use when no prompt is provided" }, - "lazyStartProducer": { "index": 51, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel's routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing." }, - "oauthProfile": { "index": 52, "kind": "parameter", "displayName": "Oauth Profile", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "OAuth profile name for obtaining an access token via the OAuth 2.0 Client Credentials grant. When set, the token is acquired from the configured identity provider and used instead of apiKey. Requires camel-oauth on the classpath. The profile properties are resolved from camel.oauth..client-id, camel.oauth..client-secret, and camel.oauth..token-endpoint." }, - "sslContextParameters": { "index": 53, "kind": "parameter", "displayName": "Ssl Context Parameters", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.apache.camel.support.jsse.SSLContextParameters", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "SSLContextParameters to use for configuring SSL\/TLS. When set, takes precedence over the individual sslTruststore, sslKeystore, and sslProtocol options." }, - "sslEndpointAlgorithm": { "index": 54, "kind": "parameter", "displayName": "Ssl Endpoint Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "security": "insecure:ssl", "insecureValue": "none", "defaultValue": "https", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The endpoint identification algorithm to validate the server hostname using the server certificate. Set to an empty string or 'none' to disable hostname verification" }, - "sslKeymanagerAlgorithm": { "index": 55, "kind": "parameter", "displayName": "Ssl Keymanager Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "SunX509", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The algorithm used by the key manager factory for SSL connections" }, - "sslKeyPassword": { "index": 56, "kind": "parameter", "displayName": "Ssl Key Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The password of the private key in the key store file" }, - "sslKeystoreLocation": { "index": 57, "kind": "parameter", "displayName": "Ssl Keystore Location", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The location of the key store file. This is optional and can be used for two-way authentication for the OpenAI API" }, - "sslKeystorePassword": { "index": 58, "kind": "parameter", "displayName": "Ssl Keystore Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The store password for the key store file" }, - "sslKeystoreType": { "index": 59, "kind": "parameter", "displayName": "Ssl Keystore Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "JKS", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The file format of the key store file" }, - "sslProtocol": { "index": 60, "kind": "parameter", "displayName": "Ssl Protocol", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "TLSv1.3", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The SSL protocol used to generate the SSLContext" }, - "sslTrustmanagerAlgorithm": { "index": 61, "kind": "parameter", "displayName": "Ssl Trustmanager Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "PKIX", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The algorithm used by the trust manager factory for SSL connections" }, - "sslTruststoreLocation": { "index": 62, "kind": "parameter", "displayName": "Ssl Truststore Location", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The location of the trust store file, used to validate the server's certificate" }, - "sslTruststorePassword": { "index": 63, "kind": "parameter", "displayName": "Ssl Truststore Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The password for the trust store file. If a password is not set, the configured trust store can still be used, but integrity checking is disabled" }, - "sslTruststoreType": { "index": 64, "kind": "parameter", "displayName": "Ssl Truststore Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "JKS", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The file format of the trust store file" } + "tags": { "index": 47, "kind": "parameter", "displayName": "Tags", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Comma-separated tags to filter tools from the shared AiToolRegistry. Tools registered via ai-tool: consumer endpoints with matching tags are discovered and exposed as OpenAI function-calling tools alongside any MCP tools. Tools with no tags (default pool) are always included." }, + "temperature": { "index": 48, "kind": "parameter", "displayName": "Temperature", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Temperature for response generation (0.0 to 2.0)" }, + "toolExecutionErrorStrategy": { "index": 49, "kind": "parameter", "displayName": "Tool Execution Error Strategy", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "org.apache.camel.component.openai.ToolExecutionErrorStrategy", "enum": [ "failExchange", "repromptModel" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "failExchange", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Strategy for handling exceptions thrown during MCP tool execution. 'failExchange' (default) propagates the exception to the Camel exchange so that standard Camel error handling (onException, dead-letter channel) can process it. This is the safer default because 'repromptModel' sends raw exception messages (which may contain connection strings, hostnames, or internal paths) to a third-party LLM provider. 'repromptModel' catches the error and sends it back to the model as a tool result so the model can attempt to recover." }, + "topP": { "index": 50, "kind": "parameter", "displayName": "Top P", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Top P for response generation (0.0 to 1.0)" }, + "userMessage": { "index": 51, "kind": "parameter", "displayName": "User Message", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Default user message text to use when no prompt is provided" }, + "lazyStartProducer": { "index": 52, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel's routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing." }, + "oauthProfile": { "index": 53, "kind": "parameter", "displayName": "Oauth Profile", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "OAuth profile name for obtaining an access token via the OAuth 2.0 Client Credentials grant. When set, the token is acquired from the configured identity provider and used instead of apiKey. Requires camel-oauth on the classpath. The profile properties are resolved from camel.oauth..client-id, camel.oauth..client-secret, and camel.oauth..token-endpoint." }, + "sslContextParameters": { "index": 54, "kind": "parameter", "displayName": "Ssl Context Parameters", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.apache.camel.support.jsse.SSLContextParameters", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "SSLContextParameters to use for configuring SSL\/TLS. When set, takes precedence over the individual sslTruststore, sslKeystore, and sslProtocol options." }, + "sslEndpointAlgorithm": { "index": 55, "kind": "parameter", "displayName": "Ssl Endpoint Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "security": "insecure:ssl", "insecureValue": "none", "defaultValue": "https", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The endpoint identification algorithm to validate the server hostname using the server certificate. Set to an empty string or 'none' to disable hostname verification" }, + "sslKeymanagerAlgorithm": { "index": 56, "kind": "parameter", "displayName": "Ssl Keymanager Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "SunX509", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The algorithm used by the key manager factory for SSL connections" }, + "sslKeyPassword": { "index": 57, "kind": "parameter", "displayName": "Ssl Key Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The password of the private key in the key store file" }, + "sslKeystoreLocation": { "index": 58, "kind": "parameter", "displayName": "Ssl Keystore Location", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The location of the key store file. This is optional and can be used for two-way authentication for the OpenAI API" }, + "sslKeystorePassword": { "index": 59, "kind": "parameter", "displayName": "Ssl Keystore Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The store password for the key store file" }, + "sslKeystoreType": { "index": 60, "kind": "parameter", "displayName": "Ssl Keystore Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "JKS", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The file format of the key store file" }, + "sslProtocol": { "index": 61, "kind": "parameter", "displayName": "Ssl Protocol", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "TLSv1.3", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The SSL protocol used to generate the SSLContext" }, + "sslTrustmanagerAlgorithm": { "index": 62, "kind": "parameter", "displayName": "Ssl Trustmanager Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "PKIX", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The algorithm used by the trust manager factory for SSL connections" }, + "sslTruststoreLocation": { "index": 63, "kind": "parameter", "displayName": "Ssl Truststore Location", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The location of the trust store file, used to validate the server's certificate" }, + "sslTruststorePassword": { "index": 64, "kind": "parameter", "displayName": "Ssl Truststore Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The password for the trust store file. If a password is not set, the configured trust store can still be used, but integrity checking is disabled" }, + "sslTruststoreType": { "index": 65, "kind": "parameter", "displayName": "Ssl Truststore Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "JKS", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The file format of the trust store file" } } } 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 ce0c3e15890af..fa09f34ffe26e 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 @@ -1200,6 +1200,23 @@ default OpenAIEndpointBuilder systemMessage(String systemMessage) { doSetProperty("systemMessage", systemMessage); return this; } + /** + * Comma-separated tags to filter tools from the shared AiToolRegistry. + * Tools registered via ai-tool: consumer endpoints with matching tags + * are discovered and exposed as OpenAI function-calling tools alongside + * any MCP tools. Tools with no tags (default pool) are always included. + * + * The option is a: java.lang.String type. + * + * Group: producer + * + * @param tags the value to set + * @return the dsl builder + */ + default OpenAIEndpointBuilder tags(String tags) { + doSetProperty("tags", tags); + return this; + } /** * Temperature for response generation (0.0 to 2.0). *