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 <[email protected]> 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 <[email protected]> Signed-off-by: Luigi De Masi <[email protected]> --- .../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>
