You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ld...@apache.org on 2026/10/15 08:02:41 UTC
(camel) 01/08: CAMEL-25669: Add native OpenAI Decisions support
This is an automated email from the ASF dual-hosted git repository.
luigidemasi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
commit 2cca8a20db65520cddaf4a668659d954c19a690a
Author: Luigi De Masi <ld...@redhat.com>
AuthorDate: Wed Oct 14 22:22:43 2026 +0200
CAMEL-25669: Add native OpenAI Decisions support
Use native Decisions request and response types with validated text/image input and dedicated refusal, response, and service exceptions. Upgrade the SDK and cover the integration with local HTTP tests and opt-in API smoke tests.
Co-authored-by: OpenAI Codex <no...@openai.com>
Signed-off-by: Luigi De Masi <ld...@redhat.com>
---
.../org/apache/camel/catalog/components/llm.json | 4 +-
.../apache/camel/catalog/components/openai.json | 4 +-
.../camel/catalog/docs/openai-component.adoc | 34 +++
.../org/apache/camel/component/openai/llm.json | 4 +-
.../org/apache/camel/component/openai/openai.json | 4 +-
.../src/main/docs/openai-component.adoc | 34 +++
.../component/openai/OpenAIConfiguration.java | 2 +-
.../openai/OpenAIDecisionRefusalException.java | 30 +++
.../openai/OpenAIDecisionResponseException.java | 30 +++
.../openai/OpenAIDecisionServiceException.java | 30 +++
.../component/openai/OpenAIDecisionsProducer.java | 36 +++
.../component/openai/OpenAIDecisionsSupport.java | 276 +++++++++++++++++++++
.../camel/component/openai/OpenAIEndpoint.java | 34 ++-
.../camel/component/openai/OpenAIOperations.java | 3 +-
.../component/openai/OpenAIDecisionsHttpTest.java | 139 +++++++++++
.../component/openai/OpenAIDecisionsTest.java | 138 +++++++++++
.../OpenAIDecisionsExternalServiceIT.java | 98 ++++++++
.../builder/endpoint/StaticEndpointBuilders.java | 24 +-
.../endpoint/dsl/OpenAIEndpointBuilderFactory.java | 54 ++--
parent/pom.xml | 2 +-
20 files changed, 926 insertions(+), 54 deletions(-)
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/llm.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/llm.json
index c052860fbcf8..c6e238e99c9e 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/llm.json
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/llm.json
@@ -147,7 +147,7 @@
"CamelOpenAIImageTotalTokens": { "index": 108, "kind": "header", "displayName": "", "group": "common", "label": "", "required": false, "javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The total number of tokens billed for the image request. Only reported by GPT image models", "constantName": "org.apache.camel.component.openai.OpenAIConstants#IMAGE_TOTAL_TOKENS" }
},
"properties": {
- "operation": { "index": 0, "kind": "path", "displayName": "Operation", "group": "common", "label": "", "required": true, "type": "enum", "javaType": "org.apache.camel.component.openai.OpenAIOperations", "enum": [ "chat-completion", "responses", "responses-retrieve", "responses-cancel", "batch", "batch-retrieve", "batch-cancel", "batch-results", "embeddings", "tool-execution", "audio-transcription", "audio-translation", "audio-speech", "moderation", "image-generation", "image-edit", " [...]
+ "operation": { "index": 0, "kind": "path", "displayName": "Operation", "group": "common", "label": "", "required": true, "type": "enum", "javaType": "org.apache.camel.component.openai.OpenAIOperations", "enum": [ "chat-completion", "decisions", "responses", "responses-retrieve", "responses-cancel", "batch", "batch-retrieve", "batch-cancel", "batch-results", "embeddings", "tool-execution", "audio-transcription", "audio-translation", "audio-speech", "moderation", "image-generation", "i [...]
"additionalBodyProperty": { "index": 1, "kind": "parameter", "displayName": "Additional Body Property", "group": "common", "label": "", "required": false, "type": "object", "javaType": "java.util.Map<java.lang.String, java.lang.Object>", "prefix": "additionalBodyProperty.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configurat [...]
"additionalHeader": { "index": 2, "kind": "parameter", "displayName": "Additional Header", "group": "common", "label": "", "required": false, "type": "object", "javaType": "java.util.Map<java.lang.String, java.lang.Object>", "prefix": "additionalHeader.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configur [...]
"additionalResponseHeader": { "index": 3, "kind": "parameter", "displayName": "Additional Response Header", "group": "common", "label": "", "required": false, "type": "object", "javaType": "java.util.Map<java.lang.String, java.lang.Object>", "prefix": "additionalResponseHeader.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "conf [...]
@@ -206,7 +206,7 @@
"mcpServer": { "index": 56, "kind": "parameter", "displayName": "Mcp Server", "group": "common", "label": "", "required": false, "type": "object", "javaType": "java.util.Map<java.lang.String, java.lang.Object>", "prefix": "mcpServer.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "MCP (Model Contex [...]
"mcpTimeout": { "index": 57, "kind": "parameter", "displayName": "Mcp Timeout", "group": "common", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 20, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Timeout in seconds for MCP tool call requests. Applies to all MCP operations includi [...]
"mcpToolRefresh": { "index": 58, "kind": "parameter", "displayName": "Mcp Tool Refresh", "group": "common", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": true, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Refresh the advertised tool list when an MCP server notifies that its [...]
- "model": { "index": 59, "kind": "parameter", "displayName": "Model", "group": "common", "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": "The model to use for chat completion" },
+ "model": { "index": 59, "kind": "parameter", "displayName": "Model", "group": "common", "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": "The model to use for the selected operation" },
"moderationModel": { "index": 60, "kind": "parameter", "displayName": "Moderation Model", "group": "common", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "omni-moderation-latest", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The model to use for moderation" },
"outputClass": { "index": 61, "kind": "parameter", "displayName": "Output Class", "group": "common", "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": "Fully qualified class name for structured output using response format" },
"parallelToolExecution": { "index": 62, "kind": "parameter", "displayName": "Parallel Tool Execution", "group": "common", "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": "Execute the tool calls returned by the model in a sing [...]
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 09982cd59edd..071be79b4cac 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
@@ -147,7 +147,7 @@
"CamelOpenAIImageTotalTokens": { "index": 108, "kind": "header", "displayName": "", "group": "common", "label": "", "required": false, "javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The total number of tokens billed for the image request. Only reported by GPT image models", "constantName": "org.apache.camel.component.openai.OpenAIConstants#IMAGE_TOTAL_TOKENS" }
},
"properties": {
- "operation": { "index": 0, "kind": "path", "displayName": "Operation", "group": "common", "label": "", "required": true, "type": "enum", "javaType": "org.apache.camel.component.openai.OpenAIOperations", "enum": [ "chat-completion", "responses", "responses-retrieve", "responses-cancel", "batch", "batch-retrieve", "batch-cancel", "batch-results", "embeddings", "tool-execution", "audio-transcription", "audio-translation", "audio-speech", "moderation", "image-generation", "image-edit", " [...]
+ "operation": { "index": 0, "kind": "path", "displayName": "Operation", "group": "common", "label": "", "required": true, "type": "enum", "javaType": "org.apache.camel.component.openai.OpenAIOperations", "enum": [ "chat-completion", "decisions", "responses", "responses-retrieve", "responses-cancel", "batch", "batch-retrieve", "batch-cancel", "batch-results", "embeddings", "tool-execution", "audio-transcription", "audio-translation", "audio-speech", "moderation", "image-generation", "i [...]
"additionalBodyProperty": { "index": 1, "kind": "parameter", "displayName": "Additional Body Property", "group": "common", "label": "", "required": false, "type": "object", "javaType": "java.util.Map<java.lang.String, java.lang.Object>", "prefix": "additionalBodyProperty.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configurat [...]
"additionalHeader": { "index": 2, "kind": "parameter", "displayName": "Additional Header", "group": "common", "label": "", "required": false, "type": "object", "javaType": "java.util.Map<java.lang.String, java.lang.Object>", "prefix": "additionalHeader.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configur [...]
"additionalResponseHeader": { "index": 3, "kind": "parameter", "displayName": "Additional Response Header", "group": "common", "label": "", "required": false, "type": "object", "javaType": "java.util.Map<java.lang.String, java.lang.Object>", "prefix": "additionalResponseHeader.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "conf [...]
@@ -206,7 +206,7 @@
"mcpServer": { "index": 56, "kind": "parameter", "displayName": "Mcp Server", "group": "common", "label": "", "required": false, "type": "object", "javaType": "java.util.Map<java.lang.String, java.lang.Object>", "prefix": "mcpServer.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "MCP (Model Contex [...]
"mcpTimeout": { "index": 57, "kind": "parameter", "displayName": "Mcp Timeout", "group": "common", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 20, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Timeout in seconds for MCP tool call requests. Applies to all MCP operations includi [...]
"mcpToolRefresh": { "index": 58, "kind": "parameter", "displayName": "Mcp Tool Refresh", "group": "common", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": true, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Refresh the advertised tool list when an MCP server notifies that its [...]
- "model": { "index": 59, "kind": "parameter", "displayName": "Model", "group": "common", "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": "The model to use for chat completion" },
+ "model": { "index": 59, "kind": "parameter", "displayName": "Model", "group": "common", "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": "The model to use for the selected operation" },
"moderationModel": { "index": 60, "kind": "parameter", "displayName": "Moderation Model", "group": "common", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "omni-moderation-latest", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The model to use for moderation" },
"outputClass": { "index": 61, "kind": "parameter", "displayName": "Output Class", "group": "common", "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": "Fully qualified class name for structured output using response format" },
"parallelToolExecution": { "index": 62, "kind": "parameter", "displayName": "Parallel Tool Execution", "group": "common", "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": "Execute the tool calls returned by the model in a sing [...]
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-component.adoc
index 002f1aebb2dd..24dcf60ca7c0 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-component.adoc
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-component.adoc
@@ -30,6 +30,39 @@ Maven users will need to add the following dependency to their `pom.xml` for thi
</dependency>
----
+== Decisions
+
+`openai:decisions` (also `llm:decisions`) accepts a native
+`com.openai.models.decisions.DecisionCreateParams` body and returns a
+`com.openai.models.decisions.Decision` body. It uses the endpoint's managed OpenAI client,
+including credentials, base URL, TLS, proxy, timeout and retry settings.
+Decisions does not initialize configured MCP servers or route tools.
+
+[source,java]
+----
+var request = DecisionCreateParams.builder()
+ .model("gpt-6-luna")
+ .input("The service is unavailable for every customer")
+ .addQuestion(DecisionCreateParams.Question.Predicate.builder()
+ .name("urgent").instructions("Does this require immediate attention?").build())
+ .build();
+var response = template.requestBody("openai:decisions", request, Decision.class);
+double probability = response.answers().get(0).asPredicate().probability();
+----
+
+Use a model and API endpoint that implement Decisions. Inputs are text or native ordered
+text/image messages; images use inline PNG, JPEG, GIF or WebP base64 data URLs. Other input
+modalities and hosted image URLs are rejected by this integration. Requests contain 1–200
+questions and at most 128 images. Responses are validated against the complete request before
+returning any result. OpenAI predicate probabilities have no built-in Boolean threshold.
+
+Refusal raises `OpenAIDecisionRefusalException`, invalid provider output raises
+`OpenAIDecisionResponseException`, and transport/API/client-start failures raise
+`OpenAIDecisionServiceException`. No failure is converted into a decision value.
+
+See the
+https://developers.openai.com/api/docs/guides/decisions[Decisions guide] for the provider contract.
+
== URI Format
[source]
@@ -44,6 +77,7 @@ TIP: Prefer `llm:` in new routes for discoverability. Examples below use `openai
Supported operations:
* `chat-completion` - Generate chat completions using language models
+* `decisions` - Native typed predicate, choice and rubric assessments of text and images
* `responses` - Call the OpenAI Responses API (hosted tools, server-side conversation state; non-streaming)
* `responses-retrieve` - Retrieve a stored Responses API response, such as one started in background mode
* `responses-cancel` - Cancel a Responses API response still running in background mode
diff --git a/components/camel-ai/camel-openai/src/generated/resources/META-INF/org/apache/camel/component/openai/llm.json b/components/camel-ai/camel-openai/src/generated/resources/META-INF/org/apache/camel/component/openai/llm.json
index c052860fbcf8..c6e238e99c9e 100644
--- a/components/camel-ai/camel-openai/src/generated/resources/META-INF/org/apache/camel/component/openai/llm.json
+++ b/components/camel-ai/camel-openai/src/generated/resources/META-INF/org/apache/camel/component/openai/llm.json
@@ -147,7 +147,7 @@
"CamelOpenAIImageTotalTokens": { "index": 108, "kind": "header", "displayName": "", "group": "common", "label": "", "required": false, "javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The total number of tokens billed for the image request. Only reported by GPT image models", "constantName": "org.apache.camel.component.openai.OpenAIConstants#IMAGE_TOTAL_TOKENS" }
},
"properties": {
- "operation": { "index": 0, "kind": "path", "displayName": "Operation", "group": "common", "label": "", "required": true, "type": "enum", "javaType": "org.apache.camel.component.openai.OpenAIOperations", "enum": [ "chat-completion", "responses", "responses-retrieve", "responses-cancel", "batch", "batch-retrieve", "batch-cancel", "batch-results", "embeddings", "tool-execution", "audio-transcription", "audio-translation", "audio-speech", "moderation", "image-generation", "image-edit", " [...]
+ "operation": { "index": 0, "kind": "path", "displayName": "Operation", "group": "common", "label": "", "required": true, "type": "enum", "javaType": "org.apache.camel.component.openai.OpenAIOperations", "enum": [ "chat-completion", "decisions", "responses", "responses-retrieve", "responses-cancel", "batch", "batch-retrieve", "batch-cancel", "batch-results", "embeddings", "tool-execution", "audio-transcription", "audio-translation", "audio-speech", "moderation", "image-generation", "i [...]
"additionalBodyProperty": { "index": 1, "kind": "parameter", "displayName": "Additional Body Property", "group": "common", "label": "", "required": false, "type": "object", "javaType": "java.util.Map<java.lang.String, java.lang.Object>", "prefix": "additionalBodyProperty.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configurat [...]
"additionalHeader": { "index": 2, "kind": "parameter", "displayName": "Additional Header", "group": "common", "label": "", "required": false, "type": "object", "javaType": "java.util.Map<java.lang.String, java.lang.Object>", "prefix": "additionalHeader.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configur [...]
"additionalResponseHeader": { "index": 3, "kind": "parameter", "displayName": "Additional Response Header", "group": "common", "label": "", "required": false, "type": "object", "javaType": "java.util.Map<java.lang.String, java.lang.Object>", "prefix": "additionalResponseHeader.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "conf [...]
@@ -206,7 +206,7 @@
"mcpServer": { "index": 56, "kind": "parameter", "displayName": "Mcp Server", "group": "common", "label": "", "required": false, "type": "object", "javaType": "java.util.Map<java.lang.String, java.lang.Object>", "prefix": "mcpServer.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "MCP (Model Contex [...]
"mcpTimeout": { "index": 57, "kind": "parameter", "displayName": "Mcp Timeout", "group": "common", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 20, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Timeout in seconds for MCP tool call requests. Applies to all MCP operations includi [...]
"mcpToolRefresh": { "index": 58, "kind": "parameter", "displayName": "Mcp Tool Refresh", "group": "common", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": true, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Refresh the advertised tool list when an MCP server notifies that its [...]
- "model": { "index": 59, "kind": "parameter", "displayName": "Model", "group": "common", "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": "The model to use for chat completion" },
+ "model": { "index": 59, "kind": "parameter", "displayName": "Model", "group": "common", "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": "The model to use for the selected operation" },
"moderationModel": { "index": 60, "kind": "parameter", "displayName": "Moderation Model", "group": "common", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "omni-moderation-latest", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The model to use for moderation" },
"outputClass": { "index": 61, "kind": "parameter", "displayName": "Output Class", "group": "common", "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": "Fully qualified class name for structured output using response format" },
"parallelToolExecution": { "index": 62, "kind": "parameter", "displayName": "Parallel Tool Execution", "group": "common", "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": "Execute the tool calls returned by the model in a sing [...]
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 09982cd59edd..071be79b4cac 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
@@ -147,7 +147,7 @@
"CamelOpenAIImageTotalTokens": { "index": 108, "kind": "header", "displayName": "", "group": "common", "label": "", "required": false, "javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The total number of tokens billed for the image request. Only reported by GPT image models", "constantName": "org.apache.camel.component.openai.OpenAIConstants#IMAGE_TOTAL_TOKENS" }
},
"properties": {
- "operation": { "index": 0, "kind": "path", "displayName": "Operation", "group": "common", "label": "", "required": true, "type": "enum", "javaType": "org.apache.camel.component.openai.OpenAIOperations", "enum": [ "chat-completion", "responses", "responses-retrieve", "responses-cancel", "batch", "batch-retrieve", "batch-cancel", "batch-results", "embeddings", "tool-execution", "audio-transcription", "audio-translation", "audio-speech", "moderation", "image-generation", "image-edit", " [...]
+ "operation": { "index": 0, "kind": "path", "displayName": "Operation", "group": "common", "label": "", "required": true, "type": "enum", "javaType": "org.apache.camel.component.openai.OpenAIOperations", "enum": [ "chat-completion", "decisions", "responses", "responses-retrieve", "responses-cancel", "batch", "batch-retrieve", "batch-cancel", "batch-results", "embeddings", "tool-execution", "audio-transcription", "audio-translation", "audio-speech", "moderation", "image-generation", "i [...]
"additionalBodyProperty": { "index": 1, "kind": "parameter", "displayName": "Additional Body Property", "group": "common", "label": "", "required": false, "type": "object", "javaType": "java.util.Map<java.lang.String, java.lang.Object>", "prefix": "additionalBodyProperty.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configurat [...]
"additionalHeader": { "index": 2, "kind": "parameter", "displayName": "Additional Header", "group": "common", "label": "", "required": false, "type": "object", "javaType": "java.util.Map<java.lang.String, java.lang.Object>", "prefix": "additionalHeader.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configur [...]
"additionalResponseHeader": { "index": 3, "kind": "parameter", "displayName": "Additional Response Header", "group": "common", "label": "", "required": false, "type": "object", "javaType": "java.util.Map<java.lang.String, java.lang.Object>", "prefix": "additionalResponseHeader.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "conf [...]
@@ -206,7 +206,7 @@
"mcpServer": { "index": 56, "kind": "parameter", "displayName": "Mcp Server", "group": "common", "label": "", "required": false, "type": "object", "javaType": "java.util.Map<java.lang.String, java.lang.Object>", "prefix": "mcpServer.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "MCP (Model Contex [...]
"mcpTimeout": { "index": 57, "kind": "parameter", "displayName": "Mcp Timeout", "group": "common", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 20, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Timeout in seconds for MCP tool call requests. Applies to all MCP operations includi [...]
"mcpToolRefresh": { "index": 58, "kind": "parameter", "displayName": "Mcp Tool Refresh", "group": "common", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": true, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Refresh the advertised tool list when an MCP server notifies that its [...]
- "model": { "index": 59, "kind": "parameter", "displayName": "Model", "group": "common", "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": "The model to use for chat completion" },
+ "model": { "index": 59, "kind": "parameter", "displayName": "Model", "group": "common", "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": "The model to use for the selected operation" },
"moderationModel": { "index": 60, "kind": "parameter", "displayName": "Moderation Model", "group": "common", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "omni-moderation-latest", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The model to use for moderation" },
"outputClass": { "index": 61, "kind": "parameter", "displayName": "Output Class", "group": "common", "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": "Fully qualified class name for structured output using response format" },
"parallelToolExecution": { "index": 62, "kind": "parameter", "displayName": "Parallel Tool Execution", "group": "common", "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": "Execute the tool calls returned by the model in a sing [...]
diff --git a/components/camel-ai/camel-openai/src/main/docs/openai-component.adoc b/components/camel-ai/camel-openai/src/main/docs/openai-component.adoc
index 002f1aebb2dd..24dcf60ca7c0 100644
--- a/components/camel-ai/camel-openai/src/main/docs/openai-component.adoc
+++ b/components/camel-ai/camel-openai/src/main/docs/openai-component.adoc
@@ -30,6 +30,39 @@ Maven users will need to add the following dependency to their `pom.xml` for thi
</dependency>
----
+== Decisions
+
+`openai:decisions` (also `llm:decisions`) accepts a native
+`com.openai.models.decisions.DecisionCreateParams` body and returns a
+`com.openai.models.decisions.Decision` body. It uses the endpoint's managed OpenAI client,
+including credentials, base URL, TLS, proxy, timeout and retry settings.
+Decisions does not initialize configured MCP servers or route tools.
+
+[source,java]
+----
+var request = DecisionCreateParams.builder()
+ .model("gpt-6-luna")
+ .input("The service is unavailable for every customer")
+ .addQuestion(DecisionCreateParams.Question.Predicate.builder()
+ .name("urgent").instructions("Does this require immediate attention?").build())
+ .build();
+var response = template.requestBody("openai:decisions", request, Decision.class);
+double probability = response.answers().get(0).asPredicate().probability();
+----
+
+Use a model and API endpoint that implement Decisions. Inputs are text or native ordered
+text/image messages; images use inline PNG, JPEG, GIF or WebP base64 data URLs. Other input
+modalities and hosted image URLs are rejected by this integration. Requests contain 1–200
+questions and at most 128 images. Responses are validated against the complete request before
+returning any result. OpenAI predicate probabilities have no built-in Boolean threshold.
+
+Refusal raises `OpenAIDecisionRefusalException`, invalid provider output raises
+`OpenAIDecisionResponseException`, and transport/API/client-start failures raise
+`OpenAIDecisionServiceException`. No failure is converted into a decision value.
+
+See the
+https://developers.openai.com/api/docs/guides/decisions[Decisions guide] for the provider contract.
+
== URI Format
[source]
@@ -44,6 +77,7 @@ TIP: Prefer `llm:` in new routes for discoverability. Examples below use `openai
Supported operations:
* `chat-completion` - Generate chat completions using language models
+* `decisions` - Native typed predicate, choice and rubric assessments of text and images
* `responses` - Call the OpenAI Responses API (hosted tools, server-side conversation state; non-streaming)
* `responses-retrieve` - Retrieve a stored Responses API response, such as one started in background mode
* `responses-cancel` - Cancel a Responses API response still running in background mode
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 b19ef617531d..bec600217483 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
@@ -104,7 +104,7 @@ public class OpenAIConfiguration implements Cloneable {
private Map<String, Object> additionalHeader;
@UriParam
- @Metadata(description = "The model to use for chat completion")
+ @Metadata(description = "The model to use for the selected operation")
private String model;
@UriParam
diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIDecisionRefusalException.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIDecisionRefusalException.java
new file mode 100644
index 000000000000..04c8d1f263f8
--- /dev/null
+++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIDecisionRefusalException.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.openai;
+
+import org.apache.camel.RuntimeCamelException;
+
+/** The provider declined to answer at least one question. */
+public class OpenAIDecisionRefusalException extends RuntimeCamelException {
+ public OpenAIDecisionRefusalException(String message) {
+ super(message);
+ }
+
+ public OpenAIDecisionRefusalException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIDecisionResponseException.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIDecisionResponseException.java
new file mode 100644
index 000000000000..651488e4f4c8
--- /dev/null
+++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIDecisionResponseException.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.openai;
+
+import org.apache.camel.RuntimeCamelException;
+
+/** The provider returned an invalid or incomplete answer. */
+public class OpenAIDecisionResponseException extends RuntimeCamelException {
+ public OpenAIDecisionResponseException(String message) {
+ super(message);
+ }
+
+ public OpenAIDecisionResponseException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIDecisionServiceException.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIDecisionServiceException.java
new file mode 100644
index 000000000000..18297cb243f5
--- /dev/null
+++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIDecisionServiceException.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.openai;
+
+import org.apache.camel.RuntimeCamelException;
+
+/** A transport or service failure prevented the assessment. */
+public class OpenAIDecisionServiceException extends RuntimeCamelException {
+ public OpenAIDecisionServiceException(String message) {
+ super(message);
+ }
+
+ public OpenAIDecisionServiceException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIDecisionsProducer.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIDecisionsProducer.java
new file mode 100644
index 000000000000..b616fd8cd878
--- /dev/null
+++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIDecisionsProducer.java
@@ -0,0 +1,36 @@
+/*
+ * 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 com.openai.models.decisions.DecisionCreateParams;
+import org.apache.camel.Exchange;
+import org.apache.camel.support.DefaultProducer;
+
+/** Sends native Decisions requests without executing tools or applying decision policy. */
+public class OpenAIDecisionsProducer extends DefaultProducer {
+ public OpenAIDecisionsProducer(OpenAIEndpoint endpoint) {
+ super(endpoint);
+ }
+
+ @Override
+ public void process(Exchange exchange) throws Exception {
+ if (!(exchange.getMessage().getBody() instanceof DecisionCreateParams request)) {
+ throw new IllegalArgumentException("Decisions requires a DecisionCreateParams body");
+ }
+ exchange.getMessage().setBody(((OpenAIEndpoint) getEndpoint()).decide(request));
+ }
+}
diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIDecisionsSupport.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIDecisionsSupport.java
new file mode 100644
index 000000000000..751e54bc0f59
--- /dev/null
+++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIDecisionsSupport.java
@@ -0,0 +1,276 @@
+/*
+ * 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.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+import com.openai.client.OpenAIClient;
+import com.openai.errors.OpenAIException;
+import com.openai.errors.OpenAIInvalidDataException;
+import com.openai.models.decisions.Decision;
+import com.openai.models.decisions.DecisionCreateParams;
+import com.openai.models.decisions.DecisionInputImage;
+
+/** Native Decisions validation shared by component callers and provider adapters. */
+public final class OpenAIDecisionsSupport {
+ private OpenAIDecisionsSupport() {
+ }
+
+ public static void validateRequest(DecisionCreateParams request) {
+ try {
+ if (request == null || request.questions().isEmpty() || request.questions().size() > 200) {
+ throw new IllegalArgumentException("Decisions requires between 1 and 200 questions");
+ }
+ request._body().validate();
+ if (!request._additionalBodyProperties().isEmpty()) {
+ throw new IllegalArgumentException("Unsupported Decisions request fields");
+ }
+ Set<String> names = new HashSet<>();
+ for (var question : request.questions()) {
+ Optional<String> name = name(question);
+ if (name.isPresent() && !names.add(name.get())) {
+ throw new IllegalArgumentException("Decisions question names must be unique");
+ }
+ if (question.isChoice()) {
+ var choices = question.asChoice().choices();
+ Set<Object> values = new HashSet<>();
+ if (choices.size() < 2 || choices.size() > 255
+ || choices.stream().anyMatch(
+ c -> !values.add(c.value().isString() ? c.value().asString() : c.value().asBool()))) {
+ throw new IllegalArgumentException("Decisions choice requires 2 to 255 distinct values");
+ }
+ } else if (question.isScore()) {
+ var levels = question.asScore().levels();
+ Set<String> labels = new HashSet<>();
+ if (levels.isEmpty() || levels.stream().anyMatch(l -> !labels.add(l.label()))) {
+ throw new IllegalArgumentException("Decisions score requires distinct ordered levels");
+ }
+ }
+ }
+ validateInput(request.input());
+ } catch (OpenAIInvalidDataException e) {
+ throw new IllegalArgumentException("Invalid Decisions request structure");
+ }
+ }
+
+ public static void validateInput(DecisionCreateParams.Input input) {
+ if (input == null) {
+ throw new IllegalArgumentException("Decisions input is required");
+ }
+ try {
+ input.validate();
+ validateContent(input);
+ } catch (OpenAIInvalidDataException e) {
+ throw new IllegalArgumentException("Invalid Decisions input structure");
+ }
+ }
+
+ private static void validateContent(DecisionCreateParams.Input input) {
+ if (input.isString()) {
+ validateText(input.asString());
+ return;
+ }
+ var messages = input.asDecisionInputMessages();
+ if (messages.isEmpty()) {
+ throw new IllegalArgumentException("Decisions input messages must not be empty");
+ }
+ int images = 0;
+ for (var message : messages) {
+ var content = message.content();
+ if (!message._additionalProperties().isEmpty()) {
+ throw new IllegalArgumentException("Unsupported Decisions input message fields");
+ }
+ if (content.isString()) {
+ validateText(content.asString());
+ } else {
+ if (content.asParts().isEmpty()) {
+ throw new IllegalArgumentException("Decisions input parts must not be empty");
+ }
+ for (var part : content.asParts()) {
+ if (part.isInputText()) {
+ if (!part.asInputText()._additionalProperties().isEmpty()) {
+ throw new IllegalArgumentException("Unsupported Decisions text fields");
+ }
+ validateText(part.asInputText().text());
+ } else if (part.isInputImage()) {
+ if (++images > 128) {
+ throw new IllegalArgumentException("Decisions accepts at most 128 images per request");
+ }
+ validateImage(part.asInputImage());
+ } else {
+ throw new IllegalArgumentException("Decisions accepts only text and image input parts");
+ }
+ }
+ }
+ }
+ }
+
+ private static void validateText(String text) {
+ if (text.length() > 10_485_760) {
+ throw new IllegalArgumentException("Decisions text exceeds the provider input limit");
+ }
+ }
+
+ private static void validateImage(DecisionInputImage image) {
+ String data = image.imageUrl();
+ int start = data.indexOf(";base64,");
+ if (!image._additionalProperties().isEmpty() || start < 0
+ || !Set.of("data:image/png", "data:image/jpeg", "data:image/webp", "data:image/gif")
+ .contains(data.substring(0, start))
+ || data.length() > 1_073_741_824) {
+ throw new IllegalArgumentException("Decisions requires inline PNG, JPEG, WebP or GIF base64 image data URLs");
+ }
+ start += 8;
+ int length = data.length() - start;
+ if (length == 0 || length % 4 != 0) {
+ throw new IllegalArgumentException("Invalid image base64 encoding");
+ }
+ // Scan without decoding to avoid allocating another copy of image data up to 1 GiB.
+ int padding = 0;
+ for (int i = start; i < data.length(); i++) {
+ char c = data.charAt(i);
+ if (c == '=' && i >= data.length() - 2) {
+ padding++;
+ } else if (padding != 0 || !(c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z'
+ || c >= '0' && c <= '9' || c == '+' || c == '/')) {
+ throw new IllegalArgumentException("Invalid image base64 encoding");
+ }
+ }
+ }
+
+ static Decision execute(OpenAIClient client, DecisionCreateParams request) throws InterruptedException {
+ try {
+ Decision response = client.decisions().create(request);
+ if (Thread.currentThread().isInterrupted()) {
+ throw new InterruptedException("OpenAI decision interrupted");
+ }
+ validateResponse(request, response);
+ return response;
+ } catch (OpenAIInvalidDataException e) {
+ throw new OpenAIDecisionResponseException("Invalid OpenAI decision response", e);
+ } catch (OpenAIException e) {
+ throw new OpenAIDecisionServiceException("OpenAI decision request failed", e);
+ }
+ }
+
+ private static Optional<String> name(DecisionCreateParams.Question question) {
+ if (question.isPredicate()) {
+ return question.asPredicate().name();
+ } else if (question.isChoice()) {
+ return question.asChoice().name();
+ }
+ return question.asScore().name();
+ }
+
+ private static void validateResponse(DecisionCreateParams request, Decision response) {
+ if (response == null || response.answers().size() != request.questions().size()) {
+ throw invalid("Decisions returned an unexpected answer count");
+ }
+ if (response.model().isBlank()) {
+ throw invalid("Decisions response is missing its model");
+ }
+ if (!response._usage().isMissing() && !response._usage().isNull()) {
+ var usage = response.usage();
+ if (usage.inputTokens() < 0 || usage.outputTokens() < 0 || usage.totalTokens() < 0) {
+ throw invalid("Decisions response contains invalid token usage");
+ }
+ }
+ for (int i = 0; i < request.questions().size(); i++) {
+ var question = request.questions().get(i);
+ var answer = response.answers().get(i);
+ Optional<String> answerName;
+ if (answer.isRefusal()) {
+ throw new OpenAIDecisionRefusalException("OpenAI declined to answer a decision question");
+ } else if (question.isPredicate() && answer.isPredicate()) {
+ probability(answer.asPredicate().probability());
+ answerName = answer.asPredicate().name();
+ } else if (question.isChoice() && answer.isChoice()) {
+ choice(question.asChoice(), answer.asChoice());
+ answerName = answer.asChoice().name();
+ } else if (question.isScore() && answer.isScore()) {
+ score(question.asScore(), answer.asScore());
+ answerName = answer.asScore().name();
+ } else {
+ throw invalid("Decisions answer type does not match its question");
+ }
+ if (!Objects.equals(name(question), answerName)) {
+ throw invalid("Decisions answer name does not match its question");
+ }
+ }
+ }
+
+ private static void choice(DecisionCreateParams.Question.Choice question, Decision.Answer.Choice answer) {
+ Set<Object> expected = new HashSet<>();
+ question.choices().forEach(c -> expected.add(c.value().isString() ? c.value().asString() : c.value().asBool()));
+ if (!expected.contains(answer.choice().isString() ? answer.choice().asString() : answer.choice().asBool())) {
+ throw invalid("Decisions choice is outside the declared domain");
+ }
+ if (!answer._confidence().isMissing() && !answer._confidence().isNull()) {
+ probability(answer.confidence());
+ }
+ if (!answer._probabilities().isMissing() && !answer._probabilities().isNull()) {
+ Set<Object> actual = new HashSet<>();
+ for (var p : answer.probabilities()) {
+ probability(p.probability());
+ if (!actual.add(p.value().isString() ? p.value().asString() : p.value().asBool())) {
+ throw invalid("Duplicate Decisions choice probability");
+ }
+ }
+ if (!actual.equals(expected)) {
+ throw invalid("Decisions probabilities do not cover the declared choices");
+ }
+ }
+ }
+
+ private static void score(DecisionCreateParams.Question.Score question, Decision.Answer.Score answer) {
+ List<DecisionCreateParams.Question.Score.Level> levels = question.levels();
+ double score = answer.score();
+ if (!Double.isFinite(score) || score < 0 || score > levels.size() - 1) {
+ throw invalid("Decisions score is outside the ordered rubric");
+ }
+ if (!answer._confidence().isMissing() && !answer._confidence().isNull()) {
+ probability(answer.confidence());
+ }
+ if (!answer._probabilities().isMissing() && !answer._probabilities().isNull()) {
+ Set<Long> actual = new HashSet<>();
+ for (var p : answer.probabilities()) {
+ probability(p.probability());
+ if (p.value() < 0 || p.value() >= levels.size() || !actual.add(p.value())
+ || !levels.get((int) p.value()).label().equals(p.label())) {
+ throw invalid("Decisions probability does not match the ordered rubric");
+ }
+ }
+ if (actual.size() != levels.size()) {
+ throw invalid("Decisions probabilities do not cover the ordered rubric");
+ }
+ }
+ }
+
+ private static void probability(double value) {
+ if (!Double.isFinite(value) || value < 0 || value > 1) {
+ throw invalid("Decisions probability or confidence is outside [0,1]");
+ }
+ }
+
+ private static OpenAIDecisionResponseException invalid(String message) {
+ return new OpenAIDecisionResponseException(message);
+ }
+}
diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIEndpoint.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIEndpoint.java
index 9076e97bca27..1b65e239cb5b 100644
--- a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIEndpoint.java
+++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIEndpoint.java
@@ -43,6 +43,8 @@ import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.core.Timeout;
import com.openai.models.chat.completions.ChatCompletionFunctionTool;
+import com.openai.models.decisions.Decision;
+import com.openai.models.decisions.DecisionCreateParams;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
@@ -68,6 +70,7 @@ import org.apache.camel.spi.UriPath;
import org.apache.camel.support.DefaultEndpoint;
import org.apache.camel.support.OAuthHelper;
import org.apache.camel.support.jsse.SSLContextParameters;
+import org.apache.camel.support.service.ServiceHelper;
import org.apache.camel.util.ObjectHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -88,7 +91,7 @@ public class OpenAIEndpoint extends DefaultEndpoint {
@UriPath
@Metadata(required = true,
- description = "The operation to perform: 'chat-completion', 'responses', 'responses-retrieve', "
+ description = "The operation to perform: 'chat-completion', 'decisions', 'responses', 'responses-retrieve', "
+ "'responses-cancel', 'batch', 'batch-retrieve', 'batch-cancel', 'batch-results', "
+ "'embeddings', 'tool-execution', 'audio-transcription', "
+ "'audio-translation', 'audio-speech', 'moderation', 'image-generation', "
@@ -128,6 +131,7 @@ public class OpenAIEndpoint extends DefaultEndpoint {
public Producer createProducer() throws Exception {
return switch (operation) {
case chatCompletion -> new OpenAIProducer(this);
+ case decisions -> new OpenAIDecisionsProducer(this);
case responses -> new OpenAIResponsesProducer(this);
case responsesRetrieve -> new OpenAIResponsesStoredProducer(this, false);
case responsesCancel -> new OpenAIResponsesStoredProducer(this, true);
@@ -180,9 +184,11 @@ public class OpenAIEndpoint extends DefaultEndpoint {
// the webhook consumer verifies signatures with a client of its own and needs no API key
client = createClient();
}
- registerRouteToolRegistryListener();
- initializeMcpServers();
- refreshRouteTools();
+ if (operation != OpenAIOperations.decisions) {
+ registerRouteToolRegistryListener();
+ initializeMcpServers();
+ refreshRouteTools();
+ }
}
@Override
@@ -960,6 +966,26 @@ public class OpenAIEndpoint extends DefaultEndpoint {
return configuration;
}
+ /** Executes a Decisions request using this endpoint's managed client and configuration. */
+ public Decision decide(DecisionCreateParams request) throws Exception {
+ if (operation != OpenAIOperations.decisions) {
+ throw new IllegalStateException("Decisions requires an openai:decisions endpoint");
+ }
+ OpenAIDecisionsSupport.validateRequest(request);
+ if (Thread.currentThread().isInterrupted()) {
+ throw new InterruptedException("OpenAI decision interrupted");
+ }
+ if (getCamelContext().isStopping() || getCamelContext().isStopped()) {
+ throw new OpenAIDecisionServiceException("OpenAI Decisions requires a running CamelContext");
+ }
+ try {
+ ServiceHelper.startService(this);
+ } catch (Exception e) {
+ throw new OpenAIDecisionServiceException("Unable to start the OpenAI Decisions client", e);
+ }
+ return OpenAIDecisionsSupport.execute(client, request);
+ }
+
public OpenAIClient getClient() {
return client;
}
diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIOperations.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIOperations.java
index 5772c7a9c2da..a1e29a6ec5e6 100644
--- a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIOperations.java
+++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIOperations.java
@@ -19,6 +19,7 @@ package org.apache.camel.component.openai;
public enum OpenAIOperations {
chatCompletion("chat-completion"),
+ decisions("decisions"),
responses("responses"),
responsesRetrieve("responses-retrieve"),
responsesCancel("responses-cancel"),
@@ -50,7 +51,7 @@ public enum OpenAIOperations {
}
throw new IllegalArgumentException(
"Unknown operation: " + value
- + ". Supported: chat-completion, responses, responses-retrieve, "
+ + ". Supported: chat-completion, decisions, responses, responses-retrieve, "
+ "responses-cancel, batch, batch-retrieve, batch-cancel, batch-results, "
+ "embeddings, tool-execution, "
+ "audio-transcription, audio-translation, audio-speech, moderation, "
diff --git a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIDecisionsHttpTest.java b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIDecisionsHttpTest.java
new file mode 100644
index 000000000000..50ca4aed1a39
--- /dev/null
+++ b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIDecisionsHttpTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import com.openai.models.decisions.DecisionChoiceOption;
+import com.openai.models.decisions.DecisionCreateParams;
+import com.openai.models.decisions.DecisionInputImage;
+import com.openai.models.decisions.DecisionInputMessage;
+import com.openai.models.decisions.DecisionInputPart;
+import com.sun.net.httpserver.HttpServer;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class OpenAIDecisionsHttpTest {
+ private HttpServer server;
+ private DefaultCamelContext context;
+ private OpenAIEndpoint endpoint;
+ private final AtomicReference<String> received = new AtomicReference<>();
+ private final AtomicReference<String> method = new AtomicReference<>();
+ private volatile String response;
+ private volatile int status = 200;
+
+ @BeforeEach
+ void start() throws Exception {
+ server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
+ server.createContext("/v1/decisions", exchange -> {
+ method.set(exchange.getRequestMethod());
+ received.set(new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8));
+ byte[] bytes = response.getBytes(StandardCharsets.UTF_8);
+ exchange.getResponseHeaders().add("Content-Type", "application/json");
+ exchange.sendResponseHeaders(status, bytes.length);
+ try (var body = exchange.getResponseBody()) {
+ body.write(bytes);
+ }
+ });
+ server.start();
+ context = new DefaultCamelContext();
+ endpoint = context.getEndpoint("openai:decisions?apiKey=test&maxRetries=0&baseUrl=http://127.0.0.1:"
+ + server.getAddress().getPort() + "/v1",
+ OpenAIEndpoint.class);
+ context.start();
+ }
+
+ @AfterEach
+ void stop() throws Exception {
+ try {
+ if (context != null) {
+ context.close();
+ }
+ } finally {
+ if (server != null) {
+ server.stop(0);
+ }
+ }
+ }
+
+ @Test
+ void serializesNativeImageAndDeserializesPredicate() throws Exception {
+ response = """
+ {"model":"gpt-6-luna","answers":[{"type":"predicate","name":"test","probability":0.96}]}
+ """;
+ var image = DecisionInputPart.ofInputImage(DecisionInputImage.builder()
+ .imageUrl("data:image/png;base64,AA==").build());
+ var request = request().toBuilder().inputOfDecisionInputMessages(List.of(
+ DecisionInputMessage.builder().contentOfParts(List.of(image)).build())).build();
+ assertThat(endpoint.decide(request).answers().get(0).asPredicate().probability()).isEqualTo(0.96);
+ assertThat(method.get()).isEqualTo("POST");
+ assertThat(received.get()).contains("input_image", "data:image/png;base64,AA==", "predicate");
+ }
+
+ @Test
+ void absentOptionalChoiceUncertaintyRemainsAbsent() throws Exception {
+ response = """
+ {"model":"gpt-6-luna","answers":[{"type":"choice","name":"test","choice":"a"}]}
+ """;
+ var request = DecisionCreateParams.builder().model("gpt-6-luna").input("Evidence")
+ .addQuestion(DecisionCreateParams.Question.Choice.builder().name("test").instructions("Choose")
+ .addChoice(DecisionChoiceOption.builder().value("a").build())
+ .addChoice(DecisionChoiceOption.builder().value("b").build()).build())
+ .build();
+ var answer = endpoint.decide(request).answers().get(0).asChoice();
+ assertThat(answer._confidence().isMissing()).isTrue();
+ assertThat(answer._probabilities().isMissing()).isTrue();
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = { "probability", "model", "usage" })
+ void malformedResponsesHaveDedicatedException(String field) {
+ response = """
+ {"model":"gpt-6-luna","answers":[{"type":"predicate","name":"test","probability":0.9}]}
+ """;
+ response = switch (field) {
+ case "probability" -> response.replace("0.9", "\"invalid\"");
+ case "model" -> response.replace("\"model\":\"gpt-6-luna\",", "");
+ default -> response.replace("}]}", "}],\"usage\":{\"input_tokens\":\"invalid\"}}");
+ };
+ assertThatThrownBy(() -> endpoint.decide(request())).isInstanceOf(OpenAIDecisionResponseException.class);
+ }
+
+ @Test
+ void serviceFailuresHaveDedicatedException() {
+ status = 503;
+ response = "{\"error\":{\"message\":\"unavailable\"}}";
+ assertThatThrownBy(() -> endpoint.decide(request())).isInstanceOf(OpenAIDecisionServiceException.class);
+ }
+
+ private static DecisionCreateParams request() {
+ return DecisionCreateParams.builder().model("gpt-6-luna").input("Evidence")
+ .addQuestion(DecisionCreateParams.Question.Predicate.builder()
+ .name("test").instructions("Is it relevant?").build())
+ .build();
+ }
+}
diff --git a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIDecisionsTest.java b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIDecisionsTest.java
new file mode 100644
index 000000000000..ee976ee25529
--- /dev/null
+++ b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIDecisionsTest.java
@@ -0,0 +1,138 @@
+/*
+ * 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.Collections;
+import java.util.List;
+import java.util.Map;
+
+import com.openai.client.OpenAIClient;
+import com.openai.models.decisions.Decision;
+import com.openai.models.decisions.DecisionCreateParams;
+import com.openai.models.decisions.DecisionInputImage;
+import com.openai.models.decisions.DecisionInputMessage;
+import com.openai.models.decisions.DecisionInputPart;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class OpenAIDecisionsTest {
+ @Test
+ void usesManagedClientWithoutStartingConfiguredMcp() throws Exception {
+ OpenAIClient client = mock(OpenAIClient.class, RETURNS_DEEP_STUBS);
+ DecisionCreateParams request = request();
+ Decision answer = Decision.builder().model("gpt-6-luna")
+ .addAnswer(Decision.Answer.Predicate.builder().name("test").probability(0.91).build())
+ .usage(usage()).build();
+ when(client.decisions().create(request)).thenReturn(answer);
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ context.start();
+ OpenAIComponent component = new OpenAIComponent();
+ component.setCamelContext(context);
+ OpenAIConfiguration configuration = new OpenAIConfiguration();
+ configuration.setMcpServer(Map.of("unused.transportType", "invalid"));
+ OpenAIEndpoint endpoint = new OpenAIEndpoint("openai:decisions", component, configuration) {
+ @Override
+ protected OpenAIClient createClient() {
+ return client;
+ }
+ };
+ endpoint.setOperation(OpenAIOperations.decisions);
+ try {
+ assertThat(endpoint.decide(request)).isSameAs(answer);
+ } finally {
+ endpoint.stop();
+ }
+ }
+ verify(client).close();
+ }
+
+ @Test
+ void refusesWholeRequest() {
+ OpenAIClient client = mock(OpenAIClient.class, RETURNS_DEEP_STUBS);
+ DecisionCreateParams request = request();
+ when(client.decisions().create(request)).thenReturn(Decision.builder().model("gpt-6-luna")
+ .addAnswer(Decision.Answer.Refusal.builder().name("test").build()).usage(usage()).build());
+ assertThatThrownBy(() -> OpenAIDecisionsSupport.execute(client, request))
+ .isInstanceOf(OpenAIDecisionRefusalException.class);
+ }
+
+ @Test
+ void validatesNativeImageFormsAndCount() {
+ var image = DecisionInputPart.ofInputImage(DecisionInputImage.builder()
+ .imageUrl("data:image/png;base64,AA==").detail(DecisionInputImage.Detail.HIGH).build());
+ var valid = request().toBuilder().inputOfDecisionInputMessages(List.of(
+ DecisionInputMessage.builder().contentOfParts(List.of(image)).build())).build();
+ OpenAIDecisionsSupport.validateRequest(valid);
+ for (String data : List.of("https://example.com/image.png", "data:audio/wav;base64,AA==",
+ "data:image/png;base64,invalid!")) {
+ var invalid = valid.toBuilder().inputOfDecisionInputMessages(List.of(DecisionInputMessage.builder()
+ .contentOfParts(
+ List.of(DecisionInputPart.ofInputImage(DecisionInputImage.builder().imageUrl(data).build())))
+ .build())).build();
+ assertThatThrownBy(() -> OpenAIDecisionsSupport.validateRequest(invalid))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageNotContaining(data);
+ }
+ var excessive = valid.toBuilder().inputOfDecisionInputMessages(List.of(DecisionInputMessage.builder()
+ .contentOfParts(Collections.nCopies(129, image)).build())).build();
+ assertThatThrownBy(() -> OpenAIDecisionsSupport.validateRequest(excessive)).hasMessageContaining("128 images");
+ }
+
+ @Test
+ void rejectsInvalidResponseBeforeReturningAnyAnswer() {
+ var request = request();
+ for (Decision response : List.of(
+ Decision.builder().model("gpt-6-luna").answers(List.of()).usage(usage()).build(),
+ Decision.builder().model("gpt-6-luna").usage(usage()).addAnswer(Decision.Answer.Predicate.builder()
+ .name("wrong").probability(0.9).build()).build(),
+ Decision.builder().model("gpt-6-luna").usage(usage()).addAnswer(Decision.Answer.Predicate.builder()
+ .name("test").probability(Double.NaN).build()).build())) {
+ OpenAIClient client = mock(OpenAIClient.class, RETURNS_DEEP_STUBS);
+ when(client.decisions().create(request)).thenReturn(response);
+ assertThatThrownBy(() -> OpenAIDecisionsSupport.execute(client, request))
+ .isInstanceOf(OpenAIDecisionResponseException.class);
+ }
+ }
+
+ @Test
+ void duplicateQuestionNamesAndOversizedBatchesAreRejected() {
+ assertThatThrownBy(() -> OpenAIDecisionsSupport.validateRequest(request().toBuilder()
+ .addQuestion(request().questions().get(0)).build())).hasMessageContaining("unique");
+ assertThatThrownBy(() -> OpenAIDecisionsSupport.validateRequest(request().toBuilder()
+ .questions(Collections.nCopies(201, request().questions().get(0))).build())).hasMessageContaining("200");
+ }
+
+ private static DecisionCreateParams request() {
+ return DecisionCreateParams.builder().model("gpt-6-luna").input("Evidence")
+ .addQuestion(DecisionCreateParams.Question.Predicate.builder()
+ .name("test").instructions("Is it relevant?").build())
+ .build();
+ }
+
+ private static Decision.Usage usage() {
+ return Decision.Usage.builder().inputTokens(1).outputTokens(0).totalTokens(1)
+ .inputTokensDetails(Decision.Usage.InputTokensDetails.builder().cachedTokens(0).cacheWriteTokens(0).build())
+ .outputTokensDetails(Decision.Usage.OutputTokensDetails.builder().reasoningTokens(0).build()).build();
+ }
+}
diff --git a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/integration/OpenAIDecisionsExternalServiceIT.java b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/integration/OpenAIDecisionsExternalServiceIT.java
new file mode 100644
index 000000000000..39dafa2bc658
--- /dev/null
+++ b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/integration/OpenAIDecisionsExternalServiceIT.java
@@ -0,0 +1,98 @@
+/*
+ * 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.integration;
+
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.util.Base64;
+import java.util.List;
+
+import javax.imageio.ImageIO;
+
+import com.openai.models.decisions.Decision;
+import com.openai.models.decisions.DecisionChoiceOption;
+import com.openai.models.decisions.DecisionCreateParams;
+import com.openai.models.decisions.DecisionInputImage;
+import com.openai.models.decisions.DecisionInputMessage;
+import com.openai.models.decisions.DecisionInputPart;
+import com.openai.models.decisions.DecisionInputText;
+import org.apache.camel.builder.RouteBuilder;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnabledIfSystemProperty(named = OpenAIExternalServiceTestSupport.ENABLE_LIVE_TESTS, matches = "true",
+ disabledReason = "Set -Dopenai.live.tests=true and configure a Decisions API service and model")
+public class OpenAIDecisionsExternalServiceIT extends OpenAIExternalServiceTestSupport {
+ private static final String DECISIONS_MODEL = "openai.live.decisions.model";
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:decisions").to("openai:decisions?requestTimeout=60000&maxRetries=0");
+ }
+ };
+ }
+
+ @Test
+ void textBatchReturnsNativePredicateChoiceAndScore() {
+ var request = DecisionCreateParams.builder().model(requiredProperty(DECISIONS_MODEL))
+ .input("My invoice contains a duplicate charge. Please help.")
+ .addQuestion(DecisionCreateParams.Question.Predicate.builder()
+ .name("request").instructions("Does the message ask for assistance?").build())
+ .addQuestion(DecisionCreateParams.Question.Choice.builder().name("category")
+ .instructions("Which team should assess this message?")
+ .addChoice(DecisionChoiceOption.builder().value("billing").description("Invoices and charges").build())
+ .addChoice(DecisionChoiceOption.builder().value("technical").description("Product defects").build())
+ .build())
+ .addQuestion(DecisionCreateParams.Question.Score.builder().name("urgency")
+ .instructions("How urgent is the request?")
+ .addLevel(DecisionCreateParams.Question.Score.Level.builder().label("routine").build())
+ .addLevel(DecisionCreateParams.Question.Score.Level.builder().label("urgent").build()).build())
+ .build();
+ Decision response = template.requestBody("direct:decisions", request, Decision.class);
+ assertThat(response.model()).isNotBlank();
+ assertThat(response.answers()).hasSize(3);
+ assertThat(response.answers().get(0).asPredicate().probability()).isBetween(0.0, 1.0);
+ assertThat(response.answers().get(1).asChoice().choice().asString()).isIn("billing", "technical");
+ assertThat(response.answers().get(2).asScore().score()).isBetween(0.0, 1.0);
+ }
+
+ @Test
+ void mixedTextAndInlineImageAreAcceptedWithoutTranscodingServices() throws Exception {
+ var image = new BufferedImage(32, 32, BufferedImage.TYPE_INT_RGB);
+ var bytes = new ByteArrayOutputStream();
+ ImageIO.write(image, "png", bytes);
+ var request = DecisionCreateParams.builder().model(requiredProperty(DECISIONS_MODEL))
+ .inputOfDecisionInputMessages(List.of(DecisionInputMessage.builder().contentOfParts(List.of(
+ DecisionInputPart.ofInputText(DecisionInputText.builder().text("Inspect this image.").build()),
+ DecisionInputPart.ofInputImage(DecisionInputImage.builder()
+ .imageUrl("data:image/png;base64," + Base64.getEncoder().encodeToString(bytes.toByteArray()))
+ .build())))
+ .build()))
+ .addQuestion(DecisionCreateParams.Question.Predicate.builder()
+ .name("dark").instructions("Is the image predominantly dark?").build())
+ .build();
+ Decision response = template.requestBody("direct:decisions", request, Decision.class);
+ assertThat(response.model()).isNotBlank();
+ assertThat(response.answers()).hasSize(1);
+ assertThat(response.answers().get(0).asPredicate().probability()).isBetween(0.0, 1.0);
+ }
+}
diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java
index a39ba3501a4d..85d6a1c5b689 100644
--- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java
+++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java
@@ -11289,14 +11289,14 @@ public class StaticEndpointBuilders {
* Syntax: <code>llm:operation</code>
*
* Path parameter: operation (required)
- * The operation to perform: 'chat-completion', 'responses',
+ * The operation to perform: 'chat-completion', 'decisions', 'responses',
* 'responses-retrieve', 'responses-cancel', 'batch', 'batch-retrieve',
* 'batch-cancel', 'batch-results', 'embeddings', 'tool-execution',
* 'audio-transcription', 'audio-translation', 'audio-speech', 'moderation',
* 'image-generation', 'image-edit', or 'webhook' (a consumer)
- * There are 17 enums and the value can be one of: chat-completion,
- * responses, responses-retrieve, responses-cancel, batch, batch-retrieve,
- * batch-cancel, batch-results, embeddings, tool-execution,
+ * There are 18 enums and the value can be one of: chat-completion,
+ * decisions, responses, responses-retrieve, responses-cancel, batch,
+ * batch-retrieve, batch-cancel, batch-results, embeddings, tool-execution,
* audio-transcription, audio-translation, audio-speech, moderation,
* image-generation, image-edit, webhook
*
@@ -12853,14 +12853,14 @@ public class StaticEndpointBuilders {
* Syntax: <code>openai:operation</code>
*
* Path parameter: operation (required)
- * The operation to perform: 'chat-completion', 'responses',
+ * The operation to perform: 'chat-completion', 'decisions', 'responses',
* 'responses-retrieve', 'responses-cancel', 'batch', 'batch-retrieve',
* 'batch-cancel', 'batch-results', 'embeddings', 'tool-execution',
* 'audio-transcription', 'audio-translation', 'audio-speech', 'moderation',
* 'image-generation', 'image-edit', or 'webhook' (a consumer)
- * There are 17 enums and the value can be one of: chat-completion,
- * responses, responses-retrieve, responses-cancel, batch, batch-retrieve,
- * batch-cancel, batch-results, embeddings, tool-execution,
+ * There are 18 enums and the value can be one of: chat-completion,
+ * decisions, responses, responses-retrieve, responses-cancel, batch,
+ * batch-retrieve, batch-cancel, batch-results, embeddings, tool-execution,
* audio-transcription, audio-translation, audio-speech, moderation,
* image-generation, image-edit, webhook
*
@@ -12883,14 +12883,14 @@ public class StaticEndpointBuilders {
* Syntax: <code>openai:operation</code>
*
* Path parameter: operation (required)
- * The operation to perform: 'chat-completion', 'responses',
+ * The operation to perform: 'chat-completion', 'decisions', 'responses',
* 'responses-retrieve', 'responses-cancel', 'batch', 'batch-retrieve',
* 'batch-cancel', 'batch-results', 'embeddings', 'tool-execution',
* 'audio-transcription', 'audio-translation', 'audio-speech', 'moderation',
* 'image-generation', 'image-edit', or 'webhook' (a consumer)
- * There are 17 enums and the value can be one of: chat-completion,
- * responses, responses-retrieve, responses-cancel, batch, batch-retrieve,
- * batch-cancel, batch-results, embeddings, tool-execution,
+ * There are 18 enums and the value can be one of: chat-completion,
+ * decisions, responses, responses-retrieve, responses-cancel, batch,
+ * batch-retrieve, batch-cancel, batch-results, embeddings, tool-execution,
* audio-transcription, audio-translation, audio-speech, moderation,
* image-generation, image-edit, webhook
*
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 a5007e168214..b2d36c953b41 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
@@ -1443,7 +1443,7 @@ public interface OpenAIEndpointBuilderFactory {
return this;
}
/**
- * The model to use for chat completion.
+ * The model to use for the selected operation.
*
* The option is a: <code>java.lang.String</code> type.
*
@@ -3875,7 +3875,7 @@ public interface OpenAIEndpointBuilderFactory {
return this;
}
/**
- * The model to use for chat completion.
+ * The model to use for the selected operation.
*
* The option is a: <code>java.lang.String</code> type.
*
@@ -6156,7 +6156,7 @@ public interface OpenAIEndpointBuilderFactory {
return this;
}
/**
- * The model to use for chat completion.
+ * The model to use for the selected operation.
*
* The option is a: <code>java.lang.String</code> type.
*
@@ -7014,14 +7014,14 @@ public interface OpenAIEndpointBuilderFactory {
* Syntax: <code>openai:operation</code>
*
* Path parameter: operation (required)
- * The operation to perform: 'chat-completion', 'responses',
- * 'responses-retrieve', 'responses-cancel', 'batch', 'batch-retrieve',
- * 'batch-cancel', 'batch-results', 'embeddings', 'tool-execution',
- * 'audio-transcription', 'audio-translation', 'audio-speech',
- * 'moderation', 'image-generation', 'image-edit', or 'webhook' (a
- * consumer)
- * There are 17 enums and the value can be one of: chat-completion,
- * responses, responses-retrieve, responses-cancel, batch,
+ * The operation to perform: 'chat-completion', 'decisions',
+ * 'responses', 'responses-retrieve', 'responses-cancel', 'batch',
+ * 'batch-retrieve', 'batch-cancel', 'batch-results', 'embeddings',
+ * 'tool-execution', 'audio-transcription', 'audio-translation',
+ * 'audio-speech', 'moderation', 'image-generation', 'image-edit', or
+ * 'webhook' (a consumer)
+ * There are 18 enums and the value can be one of: chat-completion,
+ * decisions, responses, responses-retrieve, responses-cancel, batch,
* batch-retrieve, batch-cancel, batch-results, embeddings,
* tool-execution, audio-transcription, audio-translation, audio-speech,
* moderation, image-generation, image-edit, webhook
@@ -7045,14 +7045,14 @@ public interface OpenAIEndpointBuilderFactory {
* Syntax: <code>openai:operation</code>
*
* Path parameter: operation (required)
- * The operation to perform: 'chat-completion', 'responses',
- * 'responses-retrieve', 'responses-cancel', 'batch', 'batch-retrieve',
- * 'batch-cancel', 'batch-results', 'embeddings', 'tool-execution',
- * 'audio-transcription', 'audio-translation', 'audio-speech',
- * 'moderation', 'image-generation', 'image-edit', or 'webhook' (a
- * consumer)
- * There are 17 enums and the value can be one of: chat-completion,
- * responses, responses-retrieve, responses-cancel, batch,
+ * The operation to perform: 'chat-completion', 'decisions',
+ * 'responses', 'responses-retrieve', 'responses-cancel', 'batch',
+ * 'batch-retrieve', 'batch-cancel', 'batch-results', 'embeddings',
+ * 'tool-execution', 'audio-transcription', 'audio-translation',
+ * 'audio-speech', 'moderation', 'image-generation', 'image-edit', or
+ * 'webhook' (a consumer)
+ * There are 18 enums and the value can be one of: chat-completion,
+ * decisions, responses, responses-retrieve, responses-cancel, batch,
* batch-retrieve, batch-cancel, batch-results, embeddings,
* tool-execution, audio-transcription, audio-translation, audio-speech,
* moderation, image-generation, image-edit, webhook
@@ -7078,14 +7078,14 @@ public interface OpenAIEndpointBuilderFactory {
* Syntax: <code>llm:operation</code>
*
* Path parameter: operation (required)
- * The operation to perform: 'chat-completion', 'responses',
- * 'responses-retrieve', 'responses-cancel', 'batch', 'batch-retrieve',
- * 'batch-cancel', 'batch-results', 'embeddings', 'tool-execution',
- * 'audio-transcription', 'audio-translation', 'audio-speech',
- * 'moderation', 'image-generation', 'image-edit', or 'webhook' (a
- * consumer)
- * There are 17 enums and the value can be one of: chat-completion,
- * responses, responses-retrieve, responses-cancel, batch,
+ * The operation to perform: 'chat-completion', 'decisions',
+ * 'responses', 'responses-retrieve', 'responses-cancel', 'batch',
+ * 'batch-retrieve', 'batch-cancel', 'batch-results', 'embeddings',
+ * 'tool-execution', 'audio-transcription', 'audio-translation',
+ * 'audio-speech', 'moderation', 'image-generation', 'image-edit', or
+ * 'webhook' (a consumer)
+ * There are 18 enums and the value can be one of: chat-completion,
+ * decisions, responses, responses-retrieve, responses-cancel, batch,
* batch-retrieve, batch-cancel, batch-results, embeddings,
* tool-execution, audio-transcription, audio-translation, audio-speech,
* moderation, image-generation, image-edit, webhook
diff --git a/parent/pom.xml b/parent/pom.xml
index 2ede68d3e2b4..73ac4037d8c7 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -424,7 +424,7 @@
<mcp-java-sdk-version>2.0.1</mcp-java-sdk-version>
<mssql-jdbc-version>12.10.0.jre11</mssql-jdbc-version>
<ojdbc-version>23.8.0.25.04</ojdbc-version>
- <openai-java-version>4.76.0</openai-java-version>
+ <openai-java-version>4.78.0</openai-java-version>
<openapi-generator-version>7.25.0</openapi-generator-version>
<openjpa-version>4.1.1</openjpa-version>
<opensearch-rest-client-version>3.9.0</opensearch-rest-client-version>