From cad1ca3d93926e320d3624a0330c21b84f455c24 Mon Sep 17 00:00:00 2001 From: atiaomar1978-hub Date: Sat, 18 Jul 2026 17:05:36 -0700 Subject: [PATCH 1/6] CAMEL-23393: Enforce agentic loop token budget and expose cumulative usage Add maxAgenticTokens to cap cumulative prompt plus completion tokens across MCP agentic iterations, expose cumulative usage headers, and extend the OpenAI mock to return configurable usage metadata for tests. --- .../openai/OpenAIEndpointConfigurer.java | 6 + .../openai/OpenAIEndpointUriFactory.java | 3 +- .../apache/camel/component/openai/openai.json | 96 +++++++------ .../openai/OpenAIAgenticTokenTracker.java | 53 +++++++ .../component/openai/OpenAIConfiguration.java | 13 ++ .../component/openai/OpenAIConstants.java | 6 + .../component/openai/OpenAIProducer.java | 23 +++ .../openai/OpenAIAgenticTokenBudgetTest.java | 134 ++++++++++++++++++ .../openai/OpenAIAgenticTokenTrackerTest.java | 45 ++++++ .../infra/openai/mock/MockExpectation.java | 15 ++ .../infra/openai/mock/OpenAIMockBuilder.java | 6 + .../infra/openai/mock/RequestHandler.java | 24 +++- .../infra/openai/mock/ResponseBuilder.java | 39 ++++- 13 files changed, 410 insertions(+), 53 deletions(-) create mode 100644 components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIAgenticTokenTracker.java create mode 100644 components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIAgenticTokenBudgetTest.java create mode 100644 components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIAgenticTokenTrackerTest.java diff --git a/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointConfigurer.java b/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointConfigurer.java index d1a7248c90d90..8b0110559df47 100644 --- a/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointConfigurer.java +++ b/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointConfigurer.java @@ -60,6 +60,8 @@ public boolean configure(CamelContext camelContext, Object obj, String name, Obj case "jsonSchema": target.getConfiguration().setJsonSchema(property(camelContext, java.lang.String.class, value)); return true; case "lazystartproducer": case "lazyStartProducer": target.setLazyStartProducer(property(camelContext, boolean.class, value)); return true; + case "maxagentictokens": + case "maxAgenticTokens": target.getConfiguration().setMaxAgenticTokens(property(camelContext, long.class, value)); return true; case "maxtokens": case "maxTokens": target.getConfiguration().setMaxTokens(property(camelContext, java.lang.Integer.class, value)); return true; case "maxtooliterations": @@ -157,6 +159,8 @@ public Class getOptionType(String name, boolean ignoreCase) { case "jsonSchema": return java.lang.String.class; case "lazystartproducer": case "lazyStartProducer": return boolean.class; + case "maxagentictokens": + case "maxAgenticTokens": return long.class; case "maxtokens": case "maxTokens": return java.lang.Integer.class; case "maxtooliterations": @@ -255,6 +259,8 @@ public Object getOptionValue(Object obj, String name, boolean ignoreCase) { case "jsonSchema": return target.getConfiguration().getJsonSchema(); case "lazystartproducer": case "lazyStartProducer": return target.isLazyStartProducer(); + case "maxagentictokens": + case "maxAgenticTokens": return target.getConfiguration().getMaxAgenticTokens(); case "maxtokens": case "maxTokens": return target.getConfiguration().getMaxTokens(); case "maxtooliterations": diff --git a/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointUriFactory.java b/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointUriFactory.java index e606b91548d88..1fea1c0d0d4c9 100644 --- a/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointUriFactory.java +++ b/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointUriFactory.java @@ -24,7 +24,7 @@ public class OpenAIEndpointUriFactory extends org.apache.camel.support.component private static final Set ENDPOINT_IDENTITY_PROPERTY_NAMES; private static final Map MULTI_VALUE_PREFIXES; static { - Set props = new HashSet<>(48); + Set props = new HashSet<>(49); props.add("additionalBodyProperty"); props.add("additionalResponseHeader"); props.add("apiKey"); @@ -44,6 +44,7 @@ public class OpenAIEndpointUriFactory extends org.apache.camel.support.component props.add("encodingFormat"); props.add("jsonSchema"); props.add("lazyStartProducer"); + props.add("maxAgenticTokens"); props.add("maxTokens"); props.add("maxToolIterations"); props.add("mcpProtocolVersions"); 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 ae4a63a23c818..239519f393c35 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 @@ -57,23 +57,26 @@ "CamelOpenAIToolIterations": { "index": 20, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Number of tool call iterations performed in the agentic loop", "constantName": "org.apache.camel.component.openai.OpenAIConstants#TOOL_ITERATIONS" }, "CamelOpenAIMcpToolCalls": { "index": 21, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "java.util.List", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "List of tool names called during the agentic loop", "constantName": "org.apache.camel.component.openai.OpenAIConstants#MCP_TOOL_CALLS" }, "CamelOpenAIMcpReturnDirect": { "index": 22, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Whether the response came directly from a tool with returnDirect=true, rather than from the LLM", "constantName": "org.apache.camel.component.openai.OpenAIConstants#MCP_RETURN_DIRECT" }, - "CamelOpenAIResponse": { "index": 23, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "com.openai.models.ChatCompletion", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The complete OpenAI response object", "constantName": "org.apache.camel.component.openai.OpenAIConstants#RESPONSE" }, - "CamelOpenAIEmbeddingModel": { "index": 24, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The model to use for embeddings", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_MODEL" }, - "CamelOpenAIEmbeddingDimensions": { "index": 25, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Number of output dimensions", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_DIMENSIONS" }, - "CamelOpenAIEmbeddingResponseModel": { "index": 26, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The embedding model used in the response", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_RESPONSE_MODEL" }, - "CamelOpenAIEmbeddingCount": { "index": 27, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Number of embeddings returned", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_COUNT" }, - "CamelOpenAIEmbeddingVectorSize": { "index": 28, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Vector dimensions of the embeddings", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_VECTOR_SIZE" }, - "CamelOpenAIReferenceEmbedding": { "index": 29, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "List", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Reference embedding vector for similarity comparison", "constantName": "org.apache.camel.component.openai.OpenAIConstants#REFERENCE_EMBEDDING" }, - "CamelOpenAISimilarityScore": { "index": 30, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Calculated cosine similarity score (0.0 to 1.0)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SIMILARITY_SCORE" }, - "CamelOpenAIOriginalText": { "index": 31, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String or List", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Original text content when embeddings operation is used", "constantName": "org.apache.camel.component.openai.OpenAIConstants#ORIGINAL_TEXT" }, - "CamelOpenAIAudioModel": { "index": 32, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The model to use for audio transcription", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_MODEL" }, - "CamelOpenAIAudioLanguage": { "index": 33, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The language of the input audio (ISO-639-1)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_LANGUAGE" }, - "CamelOpenAIAudioResponseFormat": { "index": 34, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The response format for audio transcription (json, text, srt, verbose_json, vtt)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_RESPONSE_FORMAT" }, - "CamelOpenAIAudioTemperature": { "index": 35, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sampling temperature for audio transcription (0.0 to 1.0)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_TEMPERATURE" }, - "CamelOpenAIAudioPrompt": { "index": 36, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Optional text to guide the model's style or continue a previous audio segment", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_PROMPT" }, - "CamelOpenAIAudioTimestampGranularities": { "index": 37, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Comma-separated timestamp granularities: word, segment, or word,segment (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_TIMESTAMP_GRANULARITIES" }, - "CamelOpenAIAudioDuration": { "index": 38, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Duration of the audio in seconds (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_DURATION" }, - "CamelOpenAIAudioDetectedLanguage": { "index": 39, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Language detected in the audio (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_DETECTED_LANGUAGE" } + "CamelOpenAIAgenticPromptTokens": { "index": 23, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Cumulative prompt tokens consumed across all agentic loop iterations", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AGENTIC_PROMPT_TOKENS" }, + "CamelOpenAIAgenticCompletionTokens": { "index": 24, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Cumulative completion tokens consumed across all agentic loop iterations", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AGENTIC_COMPLETION_TOKENS" }, + "CamelOpenAIAgenticTotalTokens": { "index": 25, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Cumulative total tokens consumed across all agentic loop iterations", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AGENTIC_TOTAL_TOKENS" }, + "CamelOpenAIResponse": { "index": 26, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "com.openai.models.ChatCompletion", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The complete OpenAI response object", "constantName": "org.apache.camel.component.openai.OpenAIConstants#RESPONSE" }, + "CamelOpenAIEmbeddingModel": { "index": 27, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The model to use for embeddings", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_MODEL" }, + "CamelOpenAIEmbeddingDimensions": { "index": 28, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Number of output dimensions", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_DIMENSIONS" }, + "CamelOpenAIEmbeddingResponseModel": { "index": 29, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The embedding model used in the response", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_RESPONSE_MODEL" }, + "CamelOpenAIEmbeddingCount": { "index": 30, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Number of embeddings returned", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_COUNT" }, + "CamelOpenAIEmbeddingVectorSize": { "index": 31, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Vector dimensions of the embeddings", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_VECTOR_SIZE" }, + "CamelOpenAIReferenceEmbedding": { "index": 32, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "List", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Reference embedding vector for similarity comparison", "constantName": "org.apache.camel.component.openai.OpenAIConstants#REFERENCE_EMBEDDING" }, + "CamelOpenAISimilarityScore": { "index": 33, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Calculated cosine similarity score (0.0 to 1.0)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SIMILARITY_SCORE" }, + "CamelOpenAIOriginalText": { "index": 34, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String or List", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Original text content when embeddings operation is used", "constantName": "org.apache.camel.component.openai.OpenAIConstants#ORIGINAL_TEXT" }, + "CamelOpenAIAudioModel": { "index": 35, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The model to use for audio transcription", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_MODEL" }, + "CamelOpenAIAudioLanguage": { "index": 36, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The language of the input audio (ISO-639-1)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_LANGUAGE" }, + "CamelOpenAIAudioResponseFormat": { "index": 37, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The response format for audio transcription (json, text, srt, verbose_json, vtt)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_RESPONSE_FORMAT" }, + "CamelOpenAIAudioTemperature": { "index": 38, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sampling temperature for audio transcription (0.0 to 1.0)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_TEMPERATURE" }, + "CamelOpenAIAudioPrompt": { "index": 39, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Optional text to guide the model's style or continue a previous audio segment", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_PROMPT" }, + "CamelOpenAIAudioTimestampGranularities": { "index": 40, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Comma-separated timestamp granularities: word, segment, or word,segment (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_TIMESTAMP_GRANULARITIES" }, + "CamelOpenAIAudioDuration": { "index": 41, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Duration of the audio in seconds (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_DURATION" }, + "CamelOpenAIAudioDetectedLanguage": { "index": 42, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Language detected in the audio (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_DETECTED_LANGUAGE" } }, "properties": { "operation": { "index": 0, "kind": "path", "displayName": "Operation", "group": "producer", "label": "", "required": true, "type": "enum", "javaType": "org.apache.camel.component.openai.OpenAIOperations", "enum": [ "chat-completion", "embeddings", "tool-execution", "audio-transcription" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The operation to perform: 'chat-completion', 'embeddings', 'tool-execution', or 'audio-transcription'" }, @@ -95,34 +98,35 @@ "embeddingModel": { "index": 16, "kind": "parameter", "displayName": "Embedding Model", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The model to use for embeddings" }, "encodingFormat": { "index": 17, "kind": "parameter", "displayName": "Encoding Format", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "float", "base64" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "base64", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The format for embedding output: 'float' for list of floats, 'base64' for compressed format" }, "jsonSchema": { "index": 18, "kind": "parameter", "displayName": "Json Schema", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "supportFileReference": true, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "JSON schema for structured output validation" }, - "maxTokens": { "index": 19, "kind": "parameter", "displayName": "Max Tokens", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "java.lang.Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Maximum number of tokens to generate" }, - "maxToolIterations": { "index": 20, "kind": "parameter", "displayName": "Max Tool Iterations", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 50, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Maximum number of tool call loop iterations to prevent infinite loops" }, - "mcpProtocolVersions": { "index": 21, "kind": "parameter", "displayName": "Mcp Protocol Versions", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Comma-separated list of MCP protocol versions to advertise when connecting to MCP servers using Streamable HTTP transport. When not set, the SDK default is used. Example: 2024-11-05,2025-03-26,2025-06-18" }, - "mcpReconnect": { "index": 22, "kind": "parameter", "displayName": "Mcp Reconnect", "group": "producer", "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": "Automatically reconnect to MCP servers when a tool call fails due to a transport error, and retry the call once." }, - "mcpServer": { "index": 23, "kind": "parameter", "displayName": "Mcp Server", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "java.util.Map", "prefix": "mcpServer.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "MCP (Model Context Protocol) server configurations. Define servers using prefix notation: mcpServer..transportType=stdiossestreamableHttp, (Note that sse is deprecated) mcpServer..command= (stdio), mcpServer..args= (stdio), mcpServer..url= (sse\/streamableHttp), mcpServer..oauthProfile= (OAuth profile for HTTP auth, requires camel-oauth). This is a multi-value option with prefix: mcpServer." }, - "mcpTimeout": { "index": 24, "kind": "parameter", "displayName": "Mcp Timeout", "group": "producer", "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 including tool execution and initialization." }, - "model": { "index": 25, "kind": "parameter", "displayName": "Model", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The model to use for chat completion" }, - "outputClass": { "index": 26, "kind": "parameter", "displayName": "Output Class", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Fully qualified class name for structured output using response format" }, - "storeFullResponse": { "index": 27, "kind": "parameter", "displayName": "Store Full Response", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Store the full response in the exchange property 'CamelOpenAIResponse' in non-streaming mode" }, - "streaming": { "index": 28, "kind": "parameter", "displayName": "Streaming", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Enable streaming responses" }, - "stripThinking": { "index": 29, "kind": "parameter", "displayName": "Strip Thinking", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Strip ... blocks from model responses (used by reasoning models like Qwen3, DeepSeek-R1). The thinking content is stored in the CamelOpenAIThinkingContent header." }, - "systemMessage": { "index": 30, "kind": "parameter", "displayName": "System Message", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "System message to prepend. When set and conversationMemory is enabled, the conversation history is reset." }, - "temperature": { "index": 31, "kind": "parameter", "displayName": "Temperature", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Temperature for response generation (0.0 to 2.0)" }, - "topP": { "index": 32, "kind": "parameter", "displayName": "Top P", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Top P for response generation (0.0 to 1.0)" }, - "userMessage": { "index": 33, "kind": "parameter", "displayName": "User Message", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Default user message text to use when no prompt is provided" }, - "lazyStartProducer": { "index": 34, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel's routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing." }, - "oauthProfile": { "index": 35, "kind": "parameter", "displayName": "Oauth Profile", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "OAuth profile name for obtaining an access token via the OAuth 2.0 Client Credentials grant. When set, the token is acquired from the configured identity provider and used instead of apiKey. Requires camel-oauth on the classpath. The profile properties are resolved from camel.oauth..client-id, camel.oauth..client-secret, and camel.oauth..token-endpoint." }, - "sslContextParameters": { "index": 36, "kind": "parameter", "displayName": "Ssl Context Parameters", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.apache.camel.support.jsse.SSLContextParameters", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "SSLContextParameters to use for configuring SSL\/TLS. When set, takes precedence over the individual sslTruststore, sslKeystore, and sslProtocol options." }, - "sslEndpointAlgorithm": { "index": 37, "kind": "parameter", "displayName": "Ssl Endpoint Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "security": "insecure:ssl", "defaultValue": "https", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The endpoint identification algorithm to validate the server hostname using the server certificate. Set to an empty string or 'none' to disable hostname verification" }, - "sslKeymanagerAlgorithm": { "index": 38, "kind": "parameter", "displayName": "Ssl Keymanager Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "SunX509", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The algorithm used by the key manager factory for SSL connections" }, - "sslKeyPassword": { "index": 39, "kind": "parameter", "displayName": "Ssl Key Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The password of the private key in the key store file" }, - "sslKeystoreLocation": { "index": 40, "kind": "parameter", "displayName": "Ssl Keystore Location", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The location of the key store file. This is optional and can be used for two-way authentication for the OpenAI API" }, - "sslKeystorePassword": { "index": 41, "kind": "parameter", "displayName": "Ssl Keystore Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The store password for the key store file" }, - "sslKeystoreType": { "index": 42, "kind": "parameter", "displayName": "Ssl Keystore Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "JKS", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The file format of the key store file" }, - "sslProtocol": { "index": 43, "kind": "parameter", "displayName": "Ssl Protocol", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "TLSv1.3", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The SSL protocol used to generate the SSLContext" }, - "sslTrustmanagerAlgorithm": { "index": 44, "kind": "parameter", "displayName": "Ssl Trustmanager Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "PKIX", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The algorithm used by the trust manager factory for SSL connections" }, - "sslTruststoreLocation": { "index": 45, "kind": "parameter", "displayName": "Ssl Truststore Location", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The location of the trust store file, used to validate the server's certificate" }, - "sslTruststorePassword": { "index": 46, "kind": "parameter", "displayName": "Ssl Truststore Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The password for the trust store file. If a password is not set, the configured trust store can still be used, but integrity checking is disabled" }, - "sslTruststoreType": { "index": 47, "kind": "parameter", "displayName": "Ssl Truststore Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "JKS", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The file format of the trust store file" } + "maxAgenticTokens": { "index": 19, "kind": "parameter", "displayName": "Max Agentic Tokens", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Maximum cumulative prompt plus completion tokens allowed across the MCP agentic loop. When 0, no token budget is enforced." }, + "maxTokens": { "index": 20, "kind": "parameter", "displayName": "Max Tokens", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "java.lang.Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Maximum number of tokens to generate" }, + "maxToolIterations": { "index": 21, "kind": "parameter", "displayName": "Max Tool Iterations", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 50, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Maximum number of tool call loop iterations to prevent infinite loops" }, + "mcpProtocolVersions": { "index": 22, "kind": "parameter", "displayName": "Mcp Protocol Versions", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Comma-separated list of MCP protocol versions to advertise when connecting to MCP servers using Streamable HTTP transport. When not set, the SDK default is used. Example: 2024-11-05,2025-03-26,2025-06-18" }, + "mcpReconnect": { "index": 23, "kind": "parameter", "displayName": "Mcp Reconnect", "group": "producer", "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": "Automatically reconnect to MCP servers when a tool call fails due to a transport error, and retry the call once." }, + "mcpServer": { "index": 24, "kind": "parameter", "displayName": "Mcp Server", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "java.util.Map", "prefix": "mcpServer.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "MCP (Model Context Protocol) server configurations. Define servers using prefix notation: mcpServer..transportType=stdiossestreamableHttp, (Note that sse is deprecated) mcpServer..command= (stdio), mcpServer..args= (stdio), mcpServer..url= (sse\/streamableHttp), mcpServer..oauthProfile= (OAuth profile for HTTP auth, requires camel-oauth). This is a multi-value option with prefix: mcpServer." }, + "mcpTimeout": { "index": 25, "kind": "parameter", "displayName": "Mcp Timeout", "group": "producer", "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 including tool execution and initialization." }, + "model": { "index": 26, "kind": "parameter", "displayName": "Model", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The model to use for chat completion" }, + "outputClass": { "index": 27, "kind": "parameter", "displayName": "Output Class", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Fully qualified class name for structured output using response format" }, + "storeFullResponse": { "index": 28, "kind": "parameter", "displayName": "Store Full Response", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Store the full response in the exchange property 'CamelOpenAIResponse' in non-streaming mode" }, + "streaming": { "index": 29, "kind": "parameter", "displayName": "Streaming", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Enable streaming responses" }, + "stripThinking": { "index": 30, "kind": "parameter", "displayName": "Strip Thinking", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Strip ... blocks from model responses (used by reasoning models like Qwen3, DeepSeek-R1). The thinking content is stored in the CamelOpenAIThinkingContent header." }, + "systemMessage": { "index": 31, "kind": "parameter", "displayName": "System Message", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "System message to prepend. When set and conversationMemory is enabled, the conversation history is reset." }, + "temperature": { "index": 32, "kind": "parameter", "displayName": "Temperature", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Temperature for response generation (0.0 to 2.0)" }, + "topP": { "index": 33, "kind": "parameter", "displayName": "Top P", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Top P for response generation (0.0 to 1.0)" }, + "userMessage": { "index": 34, "kind": "parameter", "displayName": "User Message", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Default user message text to use when no prompt is provided" }, + "lazyStartProducer": { "index": 35, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel's routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing." }, + "oauthProfile": { "index": 36, "kind": "parameter", "displayName": "Oauth Profile", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "OAuth profile name for obtaining an access token via the OAuth 2.0 Client Credentials grant. When set, the token is acquired from the configured identity provider and used instead of apiKey. Requires camel-oauth on the classpath. The profile properties are resolved from camel.oauth..client-id, camel.oauth..client-secret, and camel.oauth..token-endpoint." }, + "sslContextParameters": { "index": 37, "kind": "parameter", "displayName": "Ssl Context Parameters", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.apache.camel.support.jsse.SSLContextParameters", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "SSLContextParameters to use for configuring SSL\/TLS. When set, takes precedence over the individual sslTruststore, sslKeystore, and sslProtocol options." }, + "sslEndpointAlgorithm": { "index": 38, "kind": "parameter", "displayName": "Ssl Endpoint Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "security": "insecure:ssl", "defaultValue": "https", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The endpoint identification algorithm to validate the server hostname using the server certificate. Set to an empty string or 'none' to disable hostname verification" }, + "sslKeymanagerAlgorithm": { "index": 39, "kind": "parameter", "displayName": "Ssl Keymanager Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "SunX509", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The algorithm used by the key manager factory for SSL connections" }, + "sslKeyPassword": { "index": 40, "kind": "parameter", "displayName": "Ssl Key Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The password of the private key in the key store file" }, + "sslKeystoreLocation": { "index": 41, "kind": "parameter", "displayName": "Ssl Keystore Location", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The location of the key store file. This is optional and can be used for two-way authentication for the OpenAI API" }, + "sslKeystorePassword": { "index": 42, "kind": "parameter", "displayName": "Ssl Keystore Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The store password for the key store file" }, + "sslKeystoreType": { "index": 43, "kind": "parameter", "displayName": "Ssl Keystore Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "JKS", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The file format of the key store file" }, + "sslProtocol": { "index": 44, "kind": "parameter", "displayName": "Ssl Protocol", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "TLSv1.3", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The SSL protocol used to generate the SSLContext" }, + "sslTrustmanagerAlgorithm": { "index": 45, "kind": "parameter", "displayName": "Ssl Trustmanager Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "PKIX", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The algorithm used by the trust manager factory for SSL connections" }, + "sslTruststoreLocation": { "index": 46, "kind": "parameter", "displayName": "Ssl Truststore Location", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The location of the trust store file, used to validate the server's certificate" }, + "sslTruststorePassword": { "index": 47, "kind": "parameter", "displayName": "Ssl Truststore Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The password for the trust store file. If a password is not set, the configured trust store can still be used, but integrity checking is disabled" }, + "sslTruststoreType": { "index": 48, "kind": "parameter", "displayName": "Ssl Truststore Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "JKS", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The file format of the trust store file" } } } diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIAgenticTokenTracker.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIAgenticTokenTracker.java new file mode 100644 index 0000000000000..9103fe1b603ef --- /dev/null +++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIAgenticTokenTracker.java @@ -0,0 +1,53 @@ +/* + * 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.chat.completions.ChatCompletion; +import com.openai.models.completions.CompletionUsage; + +/** + * Tracks cumulative token usage across the MCP agentic loop. + */ +final class OpenAIAgenticTokenTracker { + + private long promptTokens; + private long completionTokens; + + void addUsage(ChatCompletion response) { + if (response == null) { + return; + } + response.usage().ifPresent(this::addUsage); + } + + void addUsage(CompletionUsage usage) { + promptTokens += usage.promptTokens(); + completionTokens += usage.completionTokens(); + } + + long getPromptTokens() { + return promptTokens; + } + + long getCompletionTokens() { + return completionTokens; + } + + long getTotalTokens() { + return promptTokens + completionTokens; + } +} 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 a5e51d1148323..4b54031036e9d 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 @@ -128,6 +128,11 @@ public class OpenAIConfiguration implements Cloneable { @Metadata(description = "Maximum number of tool call loop iterations to prevent infinite loops") private int maxToolIterations = 50; + @UriParam(defaultValue = "0") + @Metadata(description = "Maximum cumulative prompt plus completion tokens allowed across the MCP agentic loop. " + + "When 0, no token budget is enforced.") + private long maxAgenticTokens; + @UriParam(defaultValue = "true") @Metadata(description = "When true and MCP servers are configured, automatically execute tool calls " + "and loop back to the model. When false, tool calls are returned as the message body for manual handling.") @@ -485,6 +490,14 @@ public void setMaxToolIterations(int maxToolIterations) { this.maxToolIterations = maxToolIterations; } + public long getMaxAgenticTokens() { + return maxAgenticTokens; + } + + public void setMaxAgenticTokens(long maxAgenticTokens) { + this.maxAgenticTokens = maxAgenticTokens; + } + public boolean isAutoToolExecution() { return autoToolExecution; } diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConstants.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConstants.java index a6bac9df580b5..3d55ed76a212e 100644 --- a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConstants.java +++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConstants.java @@ -84,6 +84,12 @@ public final class OpenAIConstants { + "rather than from the LLM", javaType = "Boolean") public static final String MCP_RETURN_DIRECT = "CamelOpenAIMcpReturnDirect"; + @Metadata(description = "Cumulative prompt tokens consumed across all agentic loop iterations", javaType = "Long") + public static final String AGENTIC_PROMPT_TOKENS = "CamelOpenAIAgenticPromptTokens"; + @Metadata(description = "Cumulative completion tokens consumed across all agentic loop iterations", javaType = "Long") + public static final String AGENTIC_COMPLETION_TOKENS = "CamelOpenAIAgenticCompletionTokens"; + @Metadata(description = "Cumulative total tokens consumed across all agentic loop iterations", javaType = "Long") + public static final String AGENTIC_TOTAL_TOKENS = "CamelOpenAIAgenticTotalTokens"; // Output Exchange Properties @Metadata(description = "The complete OpenAI response object", javaType = "com.openai.models.ChatCompletion") diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java index 37a79794a6ab3..87cb0e8176612 100644 --- a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java +++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java @@ -481,10 +481,15 @@ private void processNonStreamingAgentic( List agenticMessages = new ArrayList<>(); List toolCallsLog = new ArrayList<>(); + OpenAIAgenticTokenTracker tokenTracker = new OpenAIAgenticTokenTracker(); int iteration = 0; while (iteration < maxIterations) { ChatCompletion response = getEndpoint().getClient().chat().completions().create(paramsBuilder.build()); + tokenTracker.addUsage(response); + setAgenticTokenHeaders(exchange.getMessage(), tokenTracker); + enforceAgenticTokenBudget(config, tokenTracker, iteration); + ChatCompletion.Choice choice = requireFirstChoice(exchange, response); if (!isToolCallsFinishReason(choice)) { @@ -606,6 +611,24 @@ private void processNonStreamingAgentic( "Max tool iterations (%d) exceeded. Tools called: %s".formatted(maxIterations, toolCallsLog)); } + private void setAgenticTokenHeaders(Message message, OpenAIAgenticTokenTracker tokenTracker) { + message.setHeader(OpenAIConstants.AGENTIC_PROMPT_TOKENS, tokenTracker.getPromptTokens()); + message.setHeader(OpenAIConstants.AGENTIC_COMPLETION_TOKENS, tokenTracker.getCompletionTokens()); + message.setHeader(OpenAIConstants.AGENTIC_TOTAL_TOKENS, tokenTracker.getTotalTokens()); + } + + private void enforceAgenticTokenBudget( + OpenAIConfiguration config, OpenAIAgenticTokenTracker tokenTracker, int iteration) { + long maxAgenticTokens = config.getMaxAgenticTokens(); + if (maxAgenticTokens <= 0 || tokenTracker.getTotalTokens() <= maxAgenticTokens) { + return; + } + throw new IllegalStateException( + "Max agentic tokens (%d) exceeded at iteration %d. Cumulative usage: prompt=%d, completion=%d, total=%d" + .formatted(maxAgenticTokens, iteration, tokenTracker.getPromptTokens(), + tokenTracker.getCompletionTokens(), tokenTracker.getTotalTokens())); + } + private String extractTextContent(List contents) { if (contents == null || contents.isEmpty()) { return ""; diff --git a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIAgenticTokenBudgetTest.java b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIAgenticTokenBudgetTest.java new file mode 100644 index 0000000000000..e08570f52bac5 --- /dev/null +++ b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIAgenticTokenBudgetTest.java @@ -0,0 +1,134 @@ +/* + * 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.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.modelcontextprotocol.client.McpSyncClient; +import io.modelcontextprotocol.spec.McpSchema; +import org.apache.camel.Exchange; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.test.infra.openai.mock.OpenAIMock; +import org.apache.camel.test.junit6.CamelTestSupport; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class OpenAIAgenticTokenBudgetTest extends CamelTestSupport { + + @RegisterExtension + public OpenAIMock tokenMock = new OpenAIMock().builder() + .when("call one tool") + .invokeTool("get_weather") + .withParam("city", "London") + .replyWith("The weather in London is sunny.") + .end() + .when("expensive tool call") + .withUsage(70, 50) + .invokeTool("get_weather") + .withParam("city", "Paris") + .replyWith("Should not reach this response") + .end() + .build(); + + @Override + protected RouteBuilder createRouteBuilder() { + String base = tokenMock.getBaseUrl() + "/v1"; + return new RouteBuilder() { + @Override + public void configure() { + from("direct:agentic-tokens") + .to("openai:chat-completion?model=gpt-5&apiKey=dummy&autoToolExecution=true&baseUrl=" + base); + + from("direct:agentic-token-budget") + .to("openai:chat-completion?model=gpt-5&apiKey=dummy&autoToolExecution=true" + + "&maxAgenticTokens=100&maxToolIterations=5&baseUrl=" + base); + } + }; + } + + @Test + void agenticLoopShouldExposeCumulativeTokenHeaders() { + String endpointUri = "openai:chat-completion?model=gpt-5&apiKey=dummy&autoToolExecution=true&baseUrl=" + + tokenMock.getBaseUrl() + "/v1"; + injectMcpTools(endpointUri, createWeatherToolClients()); + + Exchange result = template.request("direct:agentic-tokens", e -> e.getIn().setBody("call one tool")); + + assertThat(result.getException()).isNull(); + assertThat(result.getMessage().getHeader(OpenAIConstants.AGENTIC_PROMPT_TOKENS, Long.class)).isEqualTo(20L); + assertThat(result.getMessage().getHeader(OpenAIConstants.AGENTIC_COMPLETION_TOKENS, Long.class)).isEqualTo(10L); + assertThat(result.getMessage().getHeader(OpenAIConstants.AGENTIC_TOTAL_TOKENS, Long.class)).isEqualTo(30L); + } + + @Test + void maxAgenticTokensShouldStopLoopWithDescriptiveException() { + String endpointUri = "openai:chat-completion?model=gpt-5&apiKey=dummy&autoToolExecution=true" + + "&maxAgenticTokens=100&maxToolIterations=5&baseUrl=" + + tokenMock.getBaseUrl() + "/v1"; + injectMcpTools(endpointUri, createWeatherToolClients()); + + Exchange result = template.request("direct:agentic-token-budget", e -> e.getIn().setBody("expensive tool call")); + + assertThat(result.getException()).isNotNull(); + assertInstanceOf(IllegalStateException.class, result.getException()); + assertThat(result.getException().getMessage()) + .contains("Max agentic tokens (100) exceeded at iteration 0") + .contains("prompt=70") + .contains("completion=50") + .contains("total=120"); + assertThat(result.getMessage().getHeader(OpenAIConstants.AGENTIC_TOTAL_TOKENS, Long.class)).isEqualTo(120L); + } + + private Map createWeatherToolClients() { + Map toolClients = new HashMap<>(); + toolClients.put("get_weather", createMockMcpClient("Sunny, 22°C")); + return toolClients; + } + + private McpSyncClient createMockMcpClient(String resultText) { + McpSyncClient client = mock(McpSyncClient.class); + McpSchema.CallToolResult result = McpSchema.CallToolResult.builder() + .content(List.of(new McpSchema.TextContent(null, resultText, null))) + .isError(false) + .build(); + when(client.callTool(any(McpSchema.CallToolRequest.class))).thenReturn(result); + return client; + } + + private void injectMcpTools(String endpointKey, Map toolClients) { + OpenAIEndpoint endpoint = context.getEndpoint(endpointKey, OpenAIEndpoint.class); + List mcpTools = toolClients.keySet().stream() + .map(name -> McpSchema.Tool.builder(name, Map.of("type", "object")) + .description("Mock tool: " + name) + .build()) + .toList(); + endpoint.setMcpToolState(new McpToolState( + McpToolConverter.convert(mcpTools), + toolClients, + Map.of(), + Set.of())); + } +} diff --git a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIAgenticTokenTrackerTest.java b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIAgenticTokenTrackerTest.java new file mode 100644 index 0000000000000..f22e6754e5451 --- /dev/null +++ b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIAgenticTokenTrackerTest.java @@ -0,0 +1,45 @@ +/* + * 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.completions.CompletionUsage; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class OpenAIAgenticTokenTrackerTest { + + @Test + void shouldAccumulatePromptAndCompletionTokens() { + OpenAIAgenticTokenTracker tracker = new OpenAIAgenticTokenTracker(); + + tracker.addUsage(CompletionUsage.builder() + .promptTokens(30) + .completionTokens(20) + .totalTokens(50) + .build()); + tracker.addUsage(CompletionUsage.builder() + .promptTokens(10) + .completionTokens(5) + .totalTokens(15) + .build()); + + assertThat(tracker.getPromptTokens()).isEqualTo(40); + assertThat(tracker.getCompletionTokens()).isEqualTo(25); + assertThat(tracker.getTotalTokens()).isEqualTo(65); + } +} diff --git a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/MockExpectation.java b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/MockExpectation.java index dabb78bf2a0e7..e6d2246cae299 100644 --- a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/MockExpectation.java +++ b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/MockExpectation.java @@ -33,6 +33,8 @@ public class MockExpectation { private String toolContentResponse; private BiFunction customResponseFunction; private Consumer requestAssertion; + private Integer usagePromptTokens; + private Integer usageCompletionTokens; public MockExpectation(String expectedInput) { this.expectedInput = expectedInput; @@ -89,6 +91,19 @@ public void setToolContentResponse(String toolContentResponse) { this.toolContentResponse = toolContentResponse; } + public void setUsage(int promptTokens, int completionTokens) { + this.usagePromptTokens = promptTokens; + this.usageCompletionTokens = completionTokens; + } + + public Integer getUsagePromptTokens() { + return usagePromptTokens; + } + + public Integer getUsageCompletionTokens() { + return usageCompletionTokens; + } + // Tool sequence delegation methods public void addToolExecutionStep(ToolExecutionStep step) { toolSequence.addStep(step); diff --git a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/OpenAIMockBuilder.java b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/OpenAIMockBuilder.java index ee98cd6be2f6c..3732516af5618 100644 --- a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/OpenAIMockBuilder.java +++ b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/OpenAIMockBuilder.java @@ -97,6 +97,12 @@ public OpenAIMockBuilder andInvokeTool(String toolName) { return this; } + public OpenAIMockBuilder withUsage(int promptTokens, int completionTokens) { + validateCurrentExpectation("withUsage()"); + currentExpectation.setUsage(promptTokens, completionTokens); + return this; + } + public OpenAIMockBuilder withParam(String key, Object value) { validateCurrentExpectation("withParam()"); validateHasToolSteps("withParam()"); diff --git a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/RequestHandler.java b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/RequestHandler.java index b6f350f280b63..2515dd56cd297 100644 --- a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/RequestHandler.java +++ b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/RequestHandler.java @@ -90,7 +90,8 @@ private String handleToolSequenceResponse(RequestContext context, HttpExchange e } else { LOG.debug("Tool sequence completed for expectation: {}", originalInput); return responseBuilder.createFinalToolResponse(context.getMessagesNode(), expectation.getExpectedResponse(), - expectation.getToolContentResponse(), expectation.getReasoningContent()); + expectation.getToolContentResponse(), expectation.getReasoningContent(), + resolvePromptTokens(expectation), resolveCompletionTokens(expectation)); } } @@ -129,7 +130,10 @@ private String createResponse(MockExpectation expectation, String userInput, Htt default: LOG.debug("Creating simple text response"); return responseBuilder.createSimpleTextResponse( - expectation.getExpectedResponse(), expectation.getReasoningContent()); + expectation.getExpectedResponse(), + expectation.getReasoningContent(), + resolvePromptTokens(expectation), + resolveCompletionTokens(expectation)); } } @@ -137,7 +141,21 @@ private String createToolCallResponse(MockExpectation expectation) throws Except ToolExecutionStep currentStep = expectation.getCurrentToolStep(); return responseBuilder.createToolCallResponse( expectation.getExpectedResponse(), - currentStep.getToolCalls()); + currentStep.getToolCalls(), + resolvePromptTokens(expectation), + resolveCompletionTokens(expectation)); + } + + private int resolvePromptTokens(MockExpectation expectation) { + return expectation.getUsagePromptTokens() != null + ? expectation.getUsagePromptTokens() + : ResponseBuilder.DEFAULT_PROMPT_TOKENS; + } + + private int resolveCompletionTokens(MockExpectation expectation) { + return expectation.getUsageCompletionTokens() != null + ? expectation.getUsageCompletionTokens() + : ResponseBuilder.DEFAULT_COMPLETION_TOKENS; } private MockExpectation findExpectationByInput(String input) { diff --git a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/ResponseBuilder.java b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/ResponseBuilder.java index 8fa7bb7f3658d..10861b1ca8059 100644 --- a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/ResponseBuilder.java +++ b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/ResponseBuilder.java @@ -34,6 +34,9 @@ * Builder class for creating different types of OpenAI API mock responses. */ public class ResponseBuilder { + static final int DEFAULT_PROMPT_TOKENS = 10; + static final int DEFAULT_COMPLETION_TOKENS = 5; + private final ObjectMapper objectMapper; public ResponseBuilder(ObjectMapper objectMapper) { @@ -45,6 +48,12 @@ public String createSimpleTextResponse(String content) throws Exception { } public String createSimpleTextResponse(String content, String reasoningContent) throws Exception { + return createSimpleTextResponse(content, reasoningContent, DEFAULT_PROMPT_TOKENS, DEFAULT_COMPLETION_TOKENS); + } + + public String createSimpleTextResponse( + String content, String reasoningContent, int promptTokens, int completionTokens) + throws Exception { Map responseMessage = createBaseMessage(); responseMessage.put("content", content); if (reasoningContent != null) { @@ -52,18 +61,24 @@ public String createSimpleTextResponse(String content, String reasoningContent) } Map choice = createBaseChoice("stop", responseMessage); - Map chatCompletion = createBaseChatCompletion(choice); + Map chatCompletion = createBaseChatCompletion(choice, promptTokens, completionTokens); return objectMapper.writeValueAsString(chatCompletion); } public String createToolCallResponse(String content, List toolCalls) throws Exception { + return createToolCallResponse(content, toolCalls, DEFAULT_PROMPT_TOKENS, DEFAULT_COMPLETION_TOKENS); + } + + public String createToolCallResponse( + String content, List toolCalls, int promptTokens, int completionTokens) + throws Exception { Map responseMessage = createBaseMessage(); responseMessage.put("content", content); responseMessage.put("tool_calls", buildToolCallsList(toolCalls)); Map choice = createBaseChoice("tool_calls", responseMessage); - Map chatCompletion = createBaseChatCompletion(choice); + Map chatCompletion = createBaseChatCompletion(choice, promptTokens, completionTokens); return objectMapper.writeValueAsString(chatCompletion); } @@ -72,6 +87,14 @@ public String createFinalToolResponse( JsonNode messagesNode, String fallbackContent, String toolContentResponse, String reasoningContent) throws Exception { + return createFinalToolResponse(messagesNode, fallbackContent, toolContentResponse, reasoningContent, + DEFAULT_PROMPT_TOKENS, DEFAULT_COMPLETION_TOKENS); + } + + public String createFinalToolResponse( + JsonNode messagesNode, String fallbackContent, String toolContentResponse, + String reasoningContent, int promptTokens, int completionTokens) + throws Exception { Map responseMessage = createBaseMessage(); String content; @@ -89,7 +112,7 @@ public String createFinalToolResponse( } Map choice = createBaseChoice("stop", responseMessage); - Map chatCompletion = createBaseChatCompletion(choice); + Map chatCompletion = createBaseChatCompletion(choice, promptTokens, completionTokens); chatCompletion.put("history", messagesNode); return objectMapper.writeValueAsString(chatCompletion); @@ -127,12 +150,22 @@ private Map createBaseChoice(String finishReason, Map createBaseChatCompletion(Map choice) { + return createBaseChatCompletion(choice, DEFAULT_PROMPT_TOKENS, DEFAULT_COMPLETION_TOKENS); + } + + private Map createBaseChatCompletion( + Map choice, int promptTokens, int completionTokens) { Map chatCompletion = new HashMap<>(); chatCompletion.put("id", UUID.randomUUID().toString()); chatCompletion.put("choices", Collections.singletonList(choice)); chatCompletion.put("created", System.currentTimeMillis() / 1000L); chatCompletion.put("model", "openai-mock"); chatCompletion.put("object", "chat.completion"); + Map usage = new HashMap<>(); + usage.put("prompt_tokens", promptTokens); + usage.put("completion_tokens", completionTokens); + usage.put("total_tokens", promptTokens + completionTokens); + chatCompletion.put("usage", usage); return chatCompletion; } From 4dc63feab6d81889c16aa3643e5c0edee0f0e7c0 Mon Sep 17 00:00:00 2001 From: atiaomar1978-hub Date: Sun, 19 Jul 2026 09:09:10 -0700 Subject: [PATCH 2/6] CAMEL-23393: Regenerate OpenAI metadata for agentic token budget Commit generated catalog, component schema, and endpoint DSL artifacts for maxAgenticTokens so CI clean-tree check passes. Co-Authored-By: Cursor --- .../camel/catalog/components/openai.json | 96 ++++++++++--------- .../apache/camel/component/openai/openai.json | 2 +- .../dsl/OpenAIEndpointBuilderFactory.java | 32 +++++++ 3 files changed, 83 insertions(+), 47 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/openai.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/openai.json index 6c38a7b582f49..b8f23b66aa6e9 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 @@ -57,23 +57,26 @@ "CamelOpenAIToolIterations": { "index": 20, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Number of tool call iterations performed in the agentic loop", "constantName": "org.apache.camel.component.openai.OpenAIConstants#TOOL_ITERATIONS" }, "CamelOpenAIMcpToolCalls": { "index": 21, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "java.util.List", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "List of tool names called during the agentic loop", "constantName": "org.apache.camel.component.openai.OpenAIConstants#MCP_TOOL_CALLS" }, "CamelOpenAIMcpReturnDirect": { "index": 22, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Whether the response came directly from a tool with returnDirect=true, rather than from the LLM", "constantName": "org.apache.camel.component.openai.OpenAIConstants#MCP_RETURN_DIRECT" }, - "CamelOpenAIResponse": { "index": 23, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "com.openai.models.ChatCompletion", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The complete OpenAI response object", "constantName": "org.apache.camel.component.openai.OpenAIConstants#RESPONSE" }, - "CamelOpenAIEmbeddingModel": { "index": 24, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The model to use for embeddings", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_MODEL" }, - "CamelOpenAIEmbeddingDimensions": { "index": 25, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Number of output dimensions", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_DIMENSIONS" }, - "CamelOpenAIEmbeddingResponseModel": { "index": 26, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The embedding model used in the response", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_RESPONSE_MODEL" }, - "CamelOpenAIEmbeddingCount": { "index": 27, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Number of embeddings returned", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_COUNT" }, - "CamelOpenAIEmbeddingVectorSize": { "index": 28, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Vector dimensions of the embeddings", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_VECTOR_SIZE" }, - "CamelOpenAIReferenceEmbedding": { "index": 29, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "List", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Reference embedding vector for similarity comparison", "constantName": "org.apache.camel.component.openai.OpenAIConstants#REFERENCE_EMBEDDING" }, - "CamelOpenAISimilarityScore": { "index": 30, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Calculated cosine similarity score (0.0 to 1.0)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SIMILARITY_SCORE" }, - "CamelOpenAIOriginalText": { "index": 31, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String or List", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Original text content when embeddings operation is used", "constantName": "org.apache.camel.component.openai.OpenAIConstants#ORIGINAL_TEXT" }, - "CamelOpenAIAudioModel": { "index": 32, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The model to use for audio transcription", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_MODEL" }, - "CamelOpenAIAudioLanguage": { "index": 33, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The language of the input audio (ISO-639-1)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_LANGUAGE" }, - "CamelOpenAIAudioResponseFormat": { "index": 34, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The response format for audio transcription (json, text, srt, verbose_json, vtt)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_RESPONSE_FORMAT" }, - "CamelOpenAIAudioTemperature": { "index": 35, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sampling temperature for audio transcription (0.0 to 1.0)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_TEMPERATURE" }, - "CamelOpenAIAudioPrompt": { "index": 36, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Optional text to guide the model's style or continue a previous audio segment", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_PROMPT" }, - "CamelOpenAIAudioTimestampGranularities": { "index": 37, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Comma-separated timestamp granularities: word, segment, or word,segment (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_TIMESTAMP_GRANULARITIES" }, - "CamelOpenAIAudioDuration": { "index": 38, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Duration of the audio in seconds (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_DURATION" }, - "CamelOpenAIAudioDetectedLanguage": { "index": 39, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Language detected in the audio (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_DETECTED_LANGUAGE" } + "CamelOpenAIAgenticPromptTokens": { "index": 23, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Cumulative prompt tokens consumed across all agentic loop iterations", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AGENTIC_PROMPT_TOKENS" }, + "CamelOpenAIAgenticCompletionTokens": { "index": 24, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Cumulative completion tokens consumed across all agentic loop iterations", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AGENTIC_COMPLETION_TOKENS" }, + "CamelOpenAIAgenticTotalTokens": { "index": 25, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Cumulative total tokens consumed across all agentic loop iterations", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AGENTIC_TOTAL_TOKENS" }, + "CamelOpenAIResponse": { "index": 26, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "com.openai.models.ChatCompletion", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The complete OpenAI response object", "constantName": "org.apache.camel.component.openai.OpenAIConstants#RESPONSE" }, + "CamelOpenAIEmbeddingModel": { "index": 27, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The model to use for embeddings", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_MODEL" }, + "CamelOpenAIEmbeddingDimensions": { "index": 28, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Number of output dimensions", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_DIMENSIONS" }, + "CamelOpenAIEmbeddingResponseModel": { "index": 29, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The embedding model used in the response", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_RESPONSE_MODEL" }, + "CamelOpenAIEmbeddingCount": { "index": 30, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Number of embeddings returned", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_COUNT" }, + "CamelOpenAIEmbeddingVectorSize": { "index": 31, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Vector dimensions of the embeddings", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_VECTOR_SIZE" }, + "CamelOpenAIReferenceEmbedding": { "index": 32, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "List", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Reference embedding vector for similarity comparison", "constantName": "org.apache.camel.component.openai.OpenAIConstants#REFERENCE_EMBEDDING" }, + "CamelOpenAISimilarityScore": { "index": 33, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Calculated cosine similarity score (0.0 to 1.0)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SIMILARITY_SCORE" }, + "CamelOpenAIOriginalText": { "index": 34, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String or List", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Original text content when embeddings operation is used", "constantName": "org.apache.camel.component.openai.OpenAIConstants#ORIGINAL_TEXT" }, + "CamelOpenAIAudioModel": { "index": 35, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The model to use for audio transcription", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_MODEL" }, + "CamelOpenAIAudioLanguage": { "index": 36, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The language of the input audio (ISO-639-1)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_LANGUAGE" }, + "CamelOpenAIAudioResponseFormat": { "index": 37, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The response format for audio transcription (json, text, srt, verbose_json, vtt)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_RESPONSE_FORMAT" }, + "CamelOpenAIAudioTemperature": { "index": 38, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sampling temperature for audio transcription (0.0 to 1.0)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_TEMPERATURE" }, + "CamelOpenAIAudioPrompt": { "index": 39, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Optional text to guide the model's style or continue a previous audio segment", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_PROMPT" }, + "CamelOpenAIAudioTimestampGranularities": { "index": 40, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Comma-separated timestamp granularities: word, segment, or word,segment (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_TIMESTAMP_GRANULARITIES" }, + "CamelOpenAIAudioDuration": { "index": 41, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Duration of the audio in seconds (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_DURATION" }, + "CamelOpenAIAudioDetectedLanguage": { "index": 42, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Language detected in the audio (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_DETECTED_LANGUAGE" } }, "properties": { "operation": { "index": 0, "kind": "path", "displayName": "Operation", "group": "producer", "label": "", "required": true, "type": "enum", "javaType": "org.apache.camel.component.openai.OpenAIOperations", "enum": [ "chat-completion", "embeddings", "tool-execution", "audio-transcription" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The operation to perform: 'chat-completion', 'embeddings', 'tool-execution', or 'audio-transcription'" }, @@ -95,34 +98,35 @@ "embeddingModel": { "index": 16, "kind": "parameter", "displayName": "Embedding Model", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The model to use for embeddings" }, "encodingFormat": { "index": 17, "kind": "parameter", "displayName": "Encoding Format", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "float", "base64" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "base64", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The format for embedding output: 'float' for list of floats, 'base64' for compressed format" }, "jsonSchema": { "index": 18, "kind": "parameter", "displayName": "Json Schema", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "supportFileReference": true, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "JSON schema for structured output validation" }, - "maxTokens": { "index": 19, "kind": "parameter", "displayName": "Max Tokens", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "java.lang.Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Maximum number of tokens to generate" }, - "maxToolIterations": { "index": 20, "kind": "parameter", "displayName": "Max Tool Iterations", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 50, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Maximum number of tool call loop iterations to prevent infinite loops" }, - "mcpProtocolVersions": { "index": 21, "kind": "parameter", "displayName": "Mcp Protocol Versions", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Comma-separated list of MCP protocol versions to advertise when connecting to MCP servers using Streamable HTTP transport. When not set, the SDK default is used. Example: 2024-11-05,2025-03-26,2025-06-18" }, - "mcpReconnect": { "index": 22, "kind": "parameter", "displayName": "Mcp Reconnect", "group": "producer", "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": "Automatically reconnect to MCP servers when a tool call fails due to a transport error, and retry the call once." }, - "mcpServer": { "index": 23, "kind": "parameter", "displayName": "Mcp Server", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "java.util.Map", "prefix": "mcpServer.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "MCP (Model Context Protocol) server configurations. Define servers using prefix notation: mcpServer..transportType=stdiossestreamableHttp, (Note that sse is deprecated) mcpServer..command= (stdio), mcpServer..args= (stdio), mcpServer..url= (sse\/streamableHttp), mcpServer..oauthProfile= (OAuth profile for HTTP auth, requires camel-oauth). This is a multi-value option with prefix: mcpServer." }, - "mcpTimeout": { "index": 24, "kind": "parameter", "displayName": "Mcp Timeout", "group": "producer", "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 including tool execution and initialization." }, - "model": { "index": 25, "kind": "parameter", "displayName": "Model", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The model to use for chat completion" }, - "outputClass": { "index": 26, "kind": "parameter", "displayName": "Output Class", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Fully qualified class name for structured output using response format" }, - "storeFullResponse": { "index": 27, "kind": "parameter", "displayName": "Store Full Response", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Store the full response in the exchange property 'CamelOpenAIResponse' in non-streaming mode" }, - "streaming": { "index": 28, "kind": "parameter", "displayName": "Streaming", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Enable streaming responses" }, - "stripThinking": { "index": 29, "kind": "parameter", "displayName": "Strip Thinking", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Strip ... blocks from model responses (used by reasoning models like Qwen3, DeepSeek-R1). The thinking content is stored in the CamelOpenAIThinkingContent header." }, - "systemMessage": { "index": 30, "kind": "parameter", "displayName": "System Message", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "System message to prepend. When set and conversationMemory is enabled, the conversation history is reset." }, - "temperature": { "index": 31, "kind": "parameter", "displayName": "Temperature", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Temperature for response generation (0.0 to 2.0)" }, - "topP": { "index": 32, "kind": "parameter", "displayName": "Top P", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Top P for response generation (0.0 to 1.0)" }, - "userMessage": { "index": 33, "kind": "parameter", "displayName": "User Message", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Default user message text to use when no prompt is provided" }, - "lazyStartProducer": { "index": 34, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel's routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing." }, - "oauthProfile": { "index": 35, "kind": "parameter", "displayName": "Oauth Profile", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "OAuth profile name for obtaining an access token via the OAuth 2.0 Client Credentials grant. When set, the token is acquired from the configured identity provider and used instead of apiKey. Requires camel-oauth on the classpath. The profile properties are resolved from camel.oauth..client-id, camel.oauth..client-secret, and camel.oauth..token-endpoint." }, - "sslContextParameters": { "index": 36, "kind": "parameter", "displayName": "Ssl Context Parameters", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.apache.camel.support.jsse.SSLContextParameters", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "SSLContextParameters to use for configuring SSL\/TLS. When set, takes precedence over the individual sslTruststore, sslKeystore, and sslProtocol options." }, - "sslEndpointAlgorithm": { "index": 37, "kind": "parameter", "displayName": "Ssl Endpoint Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "security": "insecure:ssl", "insecureValue": "none", "defaultValue": "https", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The endpoint identification algorithm to validate the server hostname using the server certificate. Set to an empty string or 'none' to disable hostname verification" }, - "sslKeymanagerAlgorithm": { "index": 38, "kind": "parameter", "displayName": "Ssl Keymanager Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "SunX509", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The algorithm used by the key manager factory for SSL connections" }, - "sslKeyPassword": { "index": 39, "kind": "parameter", "displayName": "Ssl Key Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The password of the private key in the key store file" }, - "sslKeystoreLocation": { "index": 40, "kind": "parameter", "displayName": "Ssl Keystore Location", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The location of the key store file. This is optional and can be used for two-way authentication for the OpenAI API" }, - "sslKeystorePassword": { "index": 41, "kind": "parameter", "displayName": "Ssl Keystore Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The store password for the key store file" }, - "sslKeystoreType": { "index": 42, "kind": "parameter", "displayName": "Ssl Keystore Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "JKS", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The file format of the key store file" }, - "sslProtocol": { "index": 43, "kind": "parameter", "displayName": "Ssl Protocol", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "TLSv1.3", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The SSL protocol used to generate the SSLContext" }, - "sslTrustmanagerAlgorithm": { "index": 44, "kind": "parameter", "displayName": "Ssl Trustmanager Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "PKIX", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The algorithm used by the trust manager factory for SSL connections" }, - "sslTruststoreLocation": { "index": 45, "kind": "parameter", "displayName": "Ssl Truststore Location", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The location of the trust store file, used to validate the server's certificate" }, - "sslTruststorePassword": { "index": 46, "kind": "parameter", "displayName": "Ssl Truststore Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The password for the trust store file. If a password is not set, the configured trust store can still be used, but integrity checking is disabled" }, - "sslTruststoreType": { "index": 47, "kind": "parameter", "displayName": "Ssl Truststore Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "JKS", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The file format of the trust store file" } + "maxAgenticTokens": { "index": 19, "kind": "parameter", "displayName": "Max Agentic Tokens", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Maximum cumulative prompt plus completion tokens allowed across the MCP agentic loop. When 0, no token budget is enforced." }, + "maxTokens": { "index": 20, "kind": "parameter", "displayName": "Max Tokens", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "java.lang.Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Maximum number of tokens to generate" }, + "maxToolIterations": { "index": 21, "kind": "parameter", "displayName": "Max Tool Iterations", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 50, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Maximum number of tool call loop iterations to prevent infinite loops" }, + "mcpProtocolVersions": { "index": 22, "kind": "parameter", "displayName": "Mcp Protocol Versions", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Comma-separated list of MCP protocol versions to advertise when connecting to MCP servers using Streamable HTTP transport. When not set, the SDK default is used. Example: 2024-11-05,2025-03-26,2025-06-18" }, + "mcpReconnect": { "index": 23, "kind": "parameter", "displayName": "Mcp Reconnect", "group": "producer", "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": "Automatically reconnect to MCP servers when a tool call fails due to a transport error, and retry the call once." }, + "mcpServer": { "index": 24, "kind": "parameter", "displayName": "Mcp Server", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "java.util.Map", "prefix": "mcpServer.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "MCP (Model Context Protocol) server configurations. Define servers using prefix notation: mcpServer..transportType=stdiossestreamableHttp, (Note that sse is deprecated) mcpServer..command= (stdio), mcpServer..args= (stdio), mcpServer..url= (sse\/streamableHttp), mcpServer..oauthProfile= (OAuth profile for HTTP auth, requires camel-oauth). This is a multi-value option with prefix: mcpServer." }, + "mcpTimeout": { "index": 25, "kind": "parameter", "displayName": "Mcp Timeout", "group": "producer", "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 including tool execution and initialization." }, + "model": { "index": 26, "kind": "parameter", "displayName": "Model", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The model to use for chat completion" }, + "outputClass": { "index": 27, "kind": "parameter", "displayName": "Output Class", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Fully qualified class name for structured output using response format" }, + "storeFullResponse": { "index": 28, "kind": "parameter", "displayName": "Store Full Response", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Store the full response in the exchange property 'CamelOpenAIResponse' in non-streaming mode" }, + "streaming": { "index": 29, "kind": "parameter", "displayName": "Streaming", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Enable streaming responses" }, + "stripThinking": { "index": 30, "kind": "parameter", "displayName": "Strip Thinking", "group": "producer", "label": "", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Strip ... blocks from model responses (used by reasoning models like Qwen3, DeepSeek-R1). The thinking content is stored in the CamelOpenAIThinkingContent header." }, + "systemMessage": { "index": 31, "kind": "parameter", "displayName": "System Message", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "System message to prepend. When set and conversationMemory is enabled, the conversation history is reset." }, + "temperature": { "index": 32, "kind": "parameter", "displayName": "Temperature", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Temperature for response generation (0.0 to 2.0)" }, + "topP": { "index": 33, "kind": "parameter", "displayName": "Top P", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Top P for response generation (0.0 to 1.0)" }, + "userMessage": { "index": 34, "kind": "parameter", "displayName": "User Message", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Default user message text to use when no prompt is provided" }, + "lazyStartProducer": { "index": 35, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel's routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing." }, + "oauthProfile": { "index": 36, "kind": "parameter", "displayName": "Oauth Profile", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "OAuth profile name for obtaining an access token via the OAuth 2.0 Client Credentials grant. When set, the token is acquired from the configured identity provider and used instead of apiKey. Requires camel-oauth on the classpath. The profile properties are resolved from camel.oauth..client-id, camel.oauth..client-secret, and camel.oauth..token-endpoint." }, + "sslContextParameters": { "index": 37, "kind": "parameter", "displayName": "Ssl Context Parameters", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.apache.camel.support.jsse.SSLContextParameters", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "SSLContextParameters to use for configuring SSL\/TLS. When set, takes precedence over the individual sslTruststore, sslKeystore, and sslProtocol options." }, + "sslEndpointAlgorithm": { "index": 38, "kind": "parameter", "displayName": "Ssl Endpoint Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "security": "insecure:ssl", "insecureValue": "none", "defaultValue": "https", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The endpoint identification algorithm to validate the server hostname using the server certificate. Set to an empty string or 'none' to disable hostname verification" }, + "sslKeymanagerAlgorithm": { "index": 39, "kind": "parameter", "displayName": "Ssl Keymanager Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "SunX509", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The algorithm used by the key manager factory for SSL connections" }, + "sslKeyPassword": { "index": 40, "kind": "parameter", "displayName": "Ssl Key Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The password of the private key in the key store file" }, + "sslKeystoreLocation": { "index": 41, "kind": "parameter", "displayName": "Ssl Keystore Location", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The location of the key store file. This is optional and can be used for two-way authentication for the OpenAI API" }, + "sslKeystorePassword": { "index": 42, "kind": "parameter", "displayName": "Ssl Keystore Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The store password for the key store file" }, + "sslKeystoreType": { "index": 43, "kind": "parameter", "displayName": "Ssl Keystore Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "JKS", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The file format of the key store file" }, + "sslProtocol": { "index": 44, "kind": "parameter", "displayName": "Ssl Protocol", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "TLSv1.3", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The SSL protocol used to generate the SSLContext" }, + "sslTrustmanagerAlgorithm": { "index": 45, "kind": "parameter", "displayName": "Ssl Trustmanager Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "PKIX", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The algorithm used by the trust manager factory for SSL connections" }, + "sslTruststoreLocation": { "index": 46, "kind": "parameter", "displayName": "Ssl Truststore Location", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The location of the trust store file, used to validate the server's certificate" }, + "sslTruststorePassword": { "index": 47, "kind": "parameter", "displayName": "Ssl Truststore Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The password for the trust store file. If a password is not set, the configured trust store can still be used, but integrity checking is disabled" }, + "sslTruststoreType": { "index": 48, "kind": "parameter", "displayName": "Ssl Truststore Type", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "JKS", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The file format of the trust store file" } } } diff --git a/components/camel-ai/camel-openai/src/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 239519f393c35..b8f23b66aa6e9 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 @@ -117,7 +117,7 @@ "lazyStartProducer": { "index": 35, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel's routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing." }, "oauthProfile": { "index": 36, "kind": "parameter", "displayName": "Oauth Profile", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "OAuth profile name for obtaining an access token via the OAuth 2.0 Client Credentials grant. When set, the token is acquired from the configured identity provider and used instead of apiKey. Requires camel-oauth on the classpath. The profile properties are resolved from camel.oauth..client-id, camel.oauth..client-secret, and camel.oauth..token-endpoint." }, "sslContextParameters": { "index": 37, "kind": "parameter", "displayName": "Ssl Context Parameters", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.apache.camel.support.jsse.SSLContextParameters", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "SSLContextParameters to use for configuring SSL\/TLS. When set, takes precedence over the individual sslTruststore, sslKeystore, and sslProtocol options." }, - "sslEndpointAlgorithm": { "index": 38, "kind": "parameter", "displayName": "Ssl Endpoint Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "security": "insecure:ssl", "defaultValue": "https", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The endpoint identification algorithm to validate the server hostname using the server certificate. Set to an empty string or 'none' to disable hostname verification" }, + "sslEndpointAlgorithm": { "index": 38, "kind": "parameter", "displayName": "Ssl Endpoint Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "security": "insecure:ssl", "insecureValue": "none", "defaultValue": "https", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The endpoint identification algorithm to validate the server hostname using the server certificate. Set to an empty string or 'none' to disable hostname verification" }, "sslKeymanagerAlgorithm": { "index": 39, "kind": "parameter", "displayName": "Ssl Keymanager Algorithm", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "SunX509", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The algorithm used by the key manager factory for SSL connections" }, "sslKeyPassword": { "index": 40, "kind": "parameter", "displayName": "Ssl Key Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The password of the private key in the key store file" }, "sslKeystoreLocation": { "index": 41, "kind": "parameter", "displayName": "Ssl Keystore Location", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The location of the key store file. This is optional and can be used for two-way authentication for the OpenAI API" }, 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 5d353f4ddde8e..78b249d5fdafb 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 @@ -437,6 +437,38 @@ default OpenAIEndpointBuilder jsonSchema(String jsonSchema) { doSetProperty("jsonSchema", jsonSchema); return this; } + /** + * Maximum cumulative prompt plus completion tokens allowed across the + * MCP agentic loop. When 0, no token budget is enforced. + * + * The option is a: long type. + * + * Default: 0 + * Group: producer + * + * @param maxAgenticTokens the value to set + * @return the dsl builder + */ + default OpenAIEndpointBuilder maxAgenticTokens(long maxAgenticTokens) { + doSetProperty("maxAgenticTokens", maxAgenticTokens); + return this; + } + /** + * Maximum cumulative prompt plus completion tokens allowed across the + * MCP agentic loop. When 0, no token budget is enforced. + * + * The option will be converted to a long type. + * + * Default: 0 + * Group: producer + * + * @param maxAgenticTokens the value to set + * @return the dsl builder + */ + default OpenAIEndpointBuilder maxAgenticTokens(String maxAgenticTokens) { + doSetProperty("maxAgenticTokens", maxAgenticTokens); + return this; + } /** * Maximum number of tokens to generate. * From 02e2532d9416b73c27e1bc499912097772431a0c Mon Sep 17 00:00:00 2001 From: atiaomar1978-hub Date: Sun, 19 Jul 2026 09:46:12 -0700 Subject: [PATCH 3/6] CAMEL-23393: Regenerate endpoint DSL headers for agentic token usage Add OpenAIAgenticPromptTokens, OpenAIAgenticCompletionTokens, and OpenAIAgenticTotalTokens header accessors to the generated endpoint DSL so CI clean-tree check passes. Co-Authored-By: Cursor --- .../dsl/OpenAIEndpointBuilderFactory.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) 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 78b249d5fdafb..28b05bb0c9d9a 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 @@ -1511,6 +1511,43 @@ public String openAIMcpToolCalls() { public String openAIMcpReturnDirect() { return "CamelOpenAIMcpReturnDirect"; } + /** + * Cumulative prompt tokens consumed across all agentic loop iterations. + * + * The option is a: {@code Long} type. + * + * Group: producer + * + * @return the name of the header {@code OpenAIAgenticPromptTokens}. + */ + public String openAIAgenticPromptTokens() { + return "CamelOpenAIAgenticPromptTokens"; + } + /** + * Cumulative completion tokens consumed across all agentic loop + * iterations. + * + * The option is a: {@code Long} type. + * + * Group: producer + * + * @return the name of the header {@code OpenAIAgenticCompletionTokens}. + */ + public String openAIAgenticCompletionTokens() { + return "CamelOpenAIAgenticCompletionTokens"; + } + /** + * Cumulative total tokens consumed across all agentic loop iterations. + * + * The option is a: {@code Long} type. + * + * Group: producer + * + * @return the name of the header {@code OpenAIAgenticTotalTokens}. + */ + public String openAIAgenticTotalTokens() { + return "CamelOpenAIAgenticTotalTokens"; + } /** * The complete OpenAI response object. * From e3193e93a7ab50f1a2bcb33e3f1680da0f2cd54a Mon Sep 17 00:00:00 2001 From: Omar Atie Date: Mon, 20 Jul 2026 10:40:44 -0700 Subject: [PATCH 4/6] CAMEL-23393: Address agentic token budget review feedback Do not discard a final text response when cumulative usage exceeds maxAgenticTokens; enforce the budget only before continuing the tool loop. Document post-call overshoot and cumulative vs per-call headers, correct per-call token header types to Long, and expand budget tests. Co-authored-by: Cursor --- .../apache/camel/component/openai/openai.json | 8 +- .../src/main/docs/openai-mcp.adoc | 8 ++ .../component/openai/OpenAIConfiguration.java | 5 +- .../component/openai/OpenAIConstants.java | 7 +- .../component/openai/OpenAIProducer.java | 3 +- .../openai/OpenAIAgenticTokenBudgetTest.java | 79 +++++++++++++++++++ 6 files changed, 101 insertions(+), 9 deletions(-) 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 b8f23b66aa6e9..91b859dd3b6c4 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 @@ -51,9 +51,9 @@ "CamelOpenAIResponseModel": { "index": 14, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The model used for the completion response", "constantName": "org.apache.camel.component.openai.OpenAIConstants#RESPONSE_MODEL" }, "CamelOpenAIResponseId": { "index": 15, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The unique identifier for the completion response", "constantName": "org.apache.camel.component.openai.OpenAIConstants#RESPONSE_ID" }, "CamelOpenAIFinishReason": { "index": 16, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The reason the completion finished (e.g., stop, length, content_filter)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#FINISH_REASON" }, - "CamelOpenAIPromptTokens": { "index": 17, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The number of tokens used in the prompt", "constantName": "org.apache.camel.component.openai.OpenAIConstants#PROMPT_TOKENS" }, - "CamelOpenAICompletionTokens": { "index": 18, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The number of tokens used in the completion", "constantName": "org.apache.camel.component.openai.OpenAIConstants#COMPLETION_TOKENS" }, - "CamelOpenAITotalTokens": { "index": 19, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The total number of tokens used (prompt completion)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#TOTAL_TOKENS" }, + "CamelOpenAIPromptTokens": { "index": 17, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The number of tokens used in the prompt for the latest API call", "constantName": "org.apache.camel.component.openai.OpenAIConstants#PROMPT_TOKENS" }, + "CamelOpenAICompletionTokens": { "index": 18, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The number of tokens used in the completion for the latest API call", "constantName": "org.apache.camel.component.openai.OpenAIConstants#COMPLETION_TOKENS" }, + "CamelOpenAITotalTokens": { "index": 19, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The total number of tokens used (prompt completion) for the latest API call", "constantName": "org.apache.camel.component.openai.OpenAIConstants#TOTAL_TOKENS" }, "CamelOpenAIToolIterations": { "index": 20, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Number of tool call iterations performed in the agentic loop", "constantName": "org.apache.camel.component.openai.OpenAIConstants#TOOL_ITERATIONS" }, "CamelOpenAIMcpToolCalls": { "index": 21, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "java.util.List", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "List of tool names called during the agentic loop", "constantName": "org.apache.camel.component.openai.OpenAIConstants#MCP_TOOL_CALLS" }, "CamelOpenAIMcpReturnDirect": { "index": 22, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Whether the response came directly from a tool with returnDirect=true, rather than from the LLM", "constantName": "org.apache.camel.component.openai.OpenAIConstants#MCP_RETURN_DIRECT" }, @@ -98,7 +98,7 @@ "embeddingModel": { "index": 16, "kind": "parameter", "displayName": "Embedding Model", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The model to use for embeddings" }, "encodingFormat": { "index": 17, "kind": "parameter", "displayName": "Encoding Format", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "float", "base64" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "base64", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The format for embedding output: 'float' for list of floats, 'base64' for compressed format" }, "jsonSchema": { "index": 18, "kind": "parameter", "displayName": "Json Schema", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "supportFileReference": true, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "JSON schema for structured output validation" }, - "maxAgenticTokens": { "index": 19, "kind": "parameter", "displayName": "Max Agentic Tokens", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Maximum cumulative prompt plus completion tokens allowed across the MCP agentic loop. When 0, no token budget is enforced." }, + "maxAgenticTokens": { "index": 19, "kind": "parameter", "displayName": "Max Agentic Tokens", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Maximum cumulative prompt plus completion tokens allowed across the MCP agentic loop. When 0 or negative, no token budget is enforced. Enforcement runs after each API call that requests further tool execution, so actual spend may exceed the configured budget by up to one call (typically the largest, as the prompt grows each iteration). A final text response is returned even when cumulative usage exceeds the budget." }, "maxTokens": { "index": 20, "kind": "parameter", "displayName": "Max Tokens", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "java.lang.Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Maximum number of tokens to generate" }, "maxToolIterations": { "index": 21, "kind": "parameter", "displayName": "Max Tool Iterations", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 50, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Maximum number of tool call loop iterations to prevent infinite loops" }, "mcpProtocolVersions": { "index": 22, "kind": "parameter", "displayName": "Mcp Protocol Versions", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Comma-separated list of MCP protocol versions to advertise when connecting to MCP servers using Streamable HTTP transport. When not set, the SDK default is used. Example: 2024-11-05,2025-03-26,2025-06-18" }, diff --git a/components/camel-ai/camel-openai/src/main/docs/openai-mcp.adoc b/components/camel-ai/camel-openai/src/main/docs/openai-mcp.adoc index c5ee338255da0..866621a92f51b 100644 --- a/components/camel-ai/camel-openai/src/main/docs/openai-mcp.adoc +++ b/components/camel-ai/camel-openai/src/main/docs/openai-mcp.adoc @@ -187,6 +187,14 @@ When the model responds with tool calls, the component automatically: The `maxToolIterations` option (default: 50) prevents infinite loops. If exceeded, an `IllegalStateException` is thrown. +The `maxAgenticTokens` option (default: `0`, unlimited) caps cumulative prompt plus completion tokens across the agentic loop. +When the budget is exceeded after an API call that requests further tool execution, an `IllegalStateException` is thrown and no additional API calls are made. +Enforcement is post-call, so actual spend may exceed the configured budget by up to one API call. +A final text response is still returned when cumulative usage exceeds the budget, because no further spend occurs after that point. + +During the agentic loop, `CamelOpenAIAgenticPromptTokens`, `CamelOpenAIAgenticCompletionTokens`, and `CamelOpenAIAgenticTotalTokens` expose cumulative usage across all iterations. +The per-call headers `CamelOpenAIPromptTokens`, `CamelOpenAICompletionTokens`, and `CamelOpenAITotalTokens` reflect only the latest API call. + Set `autoToolExecution=false` to disable the agentic loop and receive raw tool calls in the message body instead: [tabs] 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 e00e31e50350e..12cb183192f5c 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 @@ -130,7 +130,10 @@ public class OpenAIConfiguration implements Cloneable { @UriParam(defaultValue = "0") @Metadata(description = "Maximum cumulative prompt plus completion tokens allowed across the MCP agentic loop. " - + "When 0, no token budget is enforced.") + + "When 0 or negative, no token budget is enforced. Enforcement runs after each API call " + + "that requests further tool execution, so actual spend may exceed the configured budget " + + "by up to one call (typically the largest, as the prompt grows each iteration). " + + "A final text response is returned even when cumulative usage exceeds the budget.") private long maxAgenticTokens; @UriParam(defaultValue = "true") diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConstants.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConstants.java index 3d55ed76a212e..76c729e7d188b 100644 --- a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConstants.java +++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConstants.java @@ -68,11 +68,12 @@ public final class OpenAIConstants { public static final String RESPONSE_ID = "CamelOpenAIResponseId"; @Metadata(description = "The reason the completion finished (e.g., stop, length, content_filter)", javaType = "String") public static final String FINISH_REASON = "CamelOpenAIFinishReason"; - @Metadata(description = "The number of tokens used in the prompt", javaType = "Integer") + @Metadata(description = "The number of tokens used in the prompt for the latest API call", javaType = "Long") public static final String PROMPT_TOKENS = "CamelOpenAIPromptTokens"; - @Metadata(description = "The number of tokens used in the completion", javaType = "Integer") + @Metadata(description = "The number of tokens used in the completion for the latest API call", javaType = "Long") public static final String COMPLETION_TOKENS = "CamelOpenAICompletionTokens"; - @Metadata(description = "The total number of tokens used (prompt + completion)", javaType = "Integer") + @Metadata(description = "The total number of tokens used (prompt + completion) for the latest API call", + javaType = "Long") public static final String TOTAL_TOKENS = "CamelOpenAITotalTokens"; // MCP Tool Call Headers diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java index 87cb0e8176612..85940f9054b69 100644 --- a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java +++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java @@ -488,7 +488,6 @@ private void processNonStreamingAgentic( ChatCompletion response = getEndpoint().getClient().chat().completions().create(paramsBuilder.build()); tokenTracker.addUsage(response); setAgenticTokenHeaders(exchange.getMessage(), tokenTracker); - enforceAgenticTokenBudget(config, tokenTracker, iteration); ChatCompletion.Choice choice = requireFirstChoice(exchange, response); @@ -512,6 +511,8 @@ private void processNonStreamingAgentic( return; } + enforceAgenticTokenBudget(config, tokenTracker, iteration); + iteration++; LOG.debug("Iteration {}: model requested {} tool call(s)", iteration, choice.message().toolCalls().map(List::size).orElse(0)); diff --git a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIAgenticTokenBudgetTest.java b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIAgenticTokenBudgetTest.java index e08570f52bac5..7142b189a58fc 100644 --- a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIAgenticTokenBudgetTest.java +++ b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIAgenticTokenBudgetTest.java @@ -34,6 +34,8 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class OpenAIAgenticTokenBudgetTest extends CamelTestSupport { @@ -51,6 +53,24 @@ class OpenAIAgenticTokenBudgetTest extends CamelTestSupport { .withParam("city", "Paris") .replyWith("Should not reach this response") .end() + .when("expensive direct answer") + .withUsage(70, 50) + .replyWith("Direct answer over budget") + .end() + .when("at budget boundary") + .withUsage(60, 40) + .invokeTool("get_weather") + .withParam("city", "London") + .replyWith("The weather in London is sunny.") + .end() + .when("accumulate over budget") + .withUsage(40, 10) + .invokeTool("get_weather") + .withParam("city", "A") + .andThenInvokeTool("get_weather") + .withParam("city", "B") + .replyWith("Should not reach this response") + .end() .build(); @Override @@ -65,6 +85,14 @@ public void configure() { from("direct:agentic-token-budget") .to("openai:chat-completion?model=gpt-5&apiKey=dummy&autoToolExecution=true" + "&maxAgenticTokens=100&maxToolIterations=5&baseUrl=" + base); + + from("direct:agentic-token-budget-boundary") + .to("openai:chat-completion?model=gpt-5&apiKey=dummy&autoToolExecution=true" + + "&maxAgenticTokens=100&maxToolIterations=5&baseUrl=" + base); + + from("direct:agentic-token-budget-multi") + .to("openai:chat-completion?model=gpt-5&apiKey=dummy&autoToolExecution=true" + + "&maxAgenticTokens=80&maxToolIterations=5&baseUrl=" + base); } }; } @@ -102,6 +130,57 @@ void maxAgenticTokensShouldStopLoopWithDescriptiveException() { assertThat(result.getMessage().getHeader(OpenAIConstants.AGENTIC_TOTAL_TOKENS, Long.class)).isEqualTo(120L); } + @Test + void finalAnswerOverBudgetShouldStillReturnResponse() { + String endpointUri = "openai:chat-completion?model=gpt-5&apiKey=dummy&autoToolExecution=true" + + "&maxAgenticTokens=100&maxToolIterations=5&baseUrl=" + + tokenMock.getBaseUrl() + "/v1"; + injectMcpTools(endpointUri, createWeatherToolClients()); + + Exchange result = template.request("direct:agentic-token-budget", + e -> e.getIn().setBody("expensive direct answer")); + + assertThat(result.getException()).isNull(); + assertThat(result.getMessage().getBody(String.class)).isEqualTo("Direct answer over budget"); + assertThat(result.getMessage().getHeader(OpenAIConstants.AGENTIC_TOTAL_TOKENS, Long.class)).isEqualTo(120L); + } + + @Test + void maxAgenticTokensAtExactBoundaryShouldAllowLoopToContinue() { + String endpointUri = "openai:chat-completion?model=gpt-5&apiKey=dummy&autoToolExecution=true" + + "&maxAgenticTokens=100&maxToolIterations=5&baseUrl=" + + tokenMock.getBaseUrl() + "/v1"; + injectMcpTools(endpointUri, createWeatherToolClients()); + + Exchange result = template.request("direct:agentic-token-budget-boundary", + e -> e.getIn().setBody("at budget boundary")); + + assertThat(result.getException()).isNull(); + assertThat(result.getMessage().getBody(String.class)).isEqualTo("The weather in London is sunny."); + assertThat(result.getMessage().getHeader(OpenAIConstants.AGENTIC_TOTAL_TOKENS, Long.class)).isEqualTo(200L); + } + + @Test + void maxAgenticTokensShouldStopAfterMultipleIterations() { + String endpointUri = "openai:chat-completion?model=gpt-5&apiKey=dummy&autoToolExecution=true" + + "&maxAgenticTokens=80&maxToolIterations=5&baseUrl=" + + tokenMock.getBaseUrl() + "/v1"; + McpSyncClient weatherClient = createMockMcpClient("Sunny, 22°C"); + Map toolClients = Map.of("get_weather", weatherClient); + injectMcpTools(endpointUri, toolClients); + + Exchange result = template.request("direct:agentic-token-budget-multi", + e -> e.getIn().setBody("accumulate over budget")); + + assertThat(result.getException()).isNotNull(); + assertInstanceOf(IllegalStateException.class, result.getException()); + assertThat(result.getException().getMessage()) + .contains("Max agentic tokens (80) exceeded at iteration 1") + .contains("total=100"); + assertThat(result.getMessage().getHeader(OpenAIConstants.AGENTIC_TOTAL_TOKENS, Long.class)).isEqualTo(100L); + verify(weatherClient, times(1)).callTool(any(McpSchema.CallToolRequest.class)); + } + private Map createWeatherToolClients() { Map toolClients = new HashMap<>(); toolClients.put("get_weather", createMockMcpClient("Sunny, 22°C")); From 647fa3d9b381010971eee830dbc1d33ee42e304d Mon Sep 17 00:00:00 2001 From: Omar Atie Date: Mon, 20 Jul 2026 14:01:07 -0700 Subject: [PATCH 5/6] CAMEL-23393: sync generated OpenAI endpoint DSL metadata Regenerate OpenAIEndpointBuilderFactory to match the component metadata: move maxRetries after maxAgenticTokens, update token header types to Long, and align header descriptions with OpenAIConstants. Co-authored-by: Claude Opus 4.8 Co-authored-by: Cursor --- .../dsl/OpenAIEndpointBuilderFactory.java | 84 +++++++++---------- 1 file changed, 42 insertions(+), 42 deletions(-) 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 a95bdd29457f1..e27cf36241b57 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 @@ -480,40 +480,6 @@ default OpenAIEndpointBuilder jsonSchema(String jsonSchema) { doSetProperty("jsonSchema", jsonSchema); return this; } - /** - * Maximum number of times the OpenAI SDK client retries failed - * requests. The SDK retry is rate-limit aware (honors Retry-After on - * 429). - * - * The option is a: int type. - * - * Default: 2 - * Group: producer - * - * @param maxRetries the value to set - * @return the dsl builder - */ - default OpenAIEndpointBuilder maxRetries(int maxRetries) { - doSetProperty("maxRetries", maxRetries); - return this; - } - /** - * Maximum number of times the OpenAI SDK client retries failed - * requests. The SDK retry is rate-limit aware (honors Retry-After on - * 429). - * - * The option will be converted to a int type. - * - * Default: 2 - * Group: producer - * - * @param maxRetries the value to set - * @return the dsl builder - */ - default OpenAIEndpointBuilder maxRetries(String maxRetries) { - doSetProperty("maxRetries", maxRetries); - return this; - } /** * Maximum cumulative prompt plus completion tokens allowed across the * MCP agentic loop. When 0 or negative, no token budget is enforced. @@ -556,6 +522,40 @@ default OpenAIEndpointBuilder maxAgenticTokens(String maxAgenticTokens) { doSetProperty("maxAgenticTokens", maxAgenticTokens); return this; } + /** + * Maximum number of times the OpenAI SDK client retries failed + * requests. The SDK retry is rate-limit aware (honors Retry-After on + * 429). + * + * The option is a: int type. + * + * Default: 2 + * Group: producer + * + * @param maxRetries the value to set + * @return the dsl builder + */ + default OpenAIEndpointBuilder maxRetries(int maxRetries) { + doSetProperty("maxRetries", maxRetries); + return this; + } + /** + * Maximum number of times the OpenAI SDK client retries failed + * requests. The SDK retry is rate-limit aware (honors Retry-After on + * 429). + * + * The option will be converted to a int type. + * + * Default: 2 + * Group: producer + * + * @param maxRetries the value to set + * @return the dsl builder + */ + default OpenAIEndpointBuilder maxRetries(String maxRetries) { + doSetProperty("maxRetries", maxRetries); + return this; + } /** * Maximum number of tokens to generate. * @@ -1558,9 +1558,9 @@ public String openAIFinishReason() { return "CamelOpenAIFinishReason"; } /** - * The number of tokens used in the prompt. + * The number of tokens used in the prompt for the latest API call. * - * The option is a: {@code Integer} type. + * The option is a: {@code Long} type. * * Group: producer * @@ -1570,9 +1570,9 @@ public String openAIPromptTokens() { return "CamelOpenAIPromptTokens"; } /** - * The number of tokens used in the completion. + * The number of tokens used in the completion for the latest API call. * - * The option is a: {@code Integer} type. + * The option is a: {@code Long} type. * * Group: producer * @@ -1582,9 +1582,10 @@ public String openAICompletionTokens() { return "CamelOpenAICompletionTokens"; } /** - * The total number of tokens used (prompt completion). + * The total number of tokens used (prompt completion) for the latest + * API call. * - * The option is a: {@code Integer} type. + * The option is a: {@code Long} type. * * Group: producer * @@ -1631,8 +1632,7 @@ public String openAIMcpReturnDirect() { return "CamelOpenAIMcpReturnDirect"; } /** - * Cumulative prompt tokens consumed across all agentic loop - * iterations. + * Cumulative prompt tokens consumed across all agentic loop iterations. * * The option is a: {@code Long} type. * From b061625bf42b2e1b9dadfc6451f98b4348da26aa Mon Sep 17 00:00:00 2001 From: Omar Atie Date: Tue, 21 Jul 2026 08:46:33 -0700 Subject: [PATCH 6/6] CAMEL-23393: address davsclaus review feedback Use AssertJ-only assertions in agentic token budget tests and document per-call token header type change (Integer to Long) in the 4.22 upgrade guide. Co-authored-by: Cursor --- .../camel/component/openai/OpenAIAgenticTokenBudgetTest.java | 5 ++--- .../modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc | 5 +++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIAgenticTokenBudgetTest.java b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIAgenticTokenBudgetTest.java index 7142b189a58fc..b0d33131d416a 100644 --- a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIAgenticTokenBudgetTest.java +++ b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIAgenticTokenBudgetTest.java @@ -31,7 +31,6 @@ import org.junit.jupiter.api.extension.RegisterExtension; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -121,7 +120,7 @@ void maxAgenticTokensShouldStopLoopWithDescriptiveException() { Exchange result = template.request("direct:agentic-token-budget", e -> e.getIn().setBody("expensive tool call")); assertThat(result.getException()).isNotNull(); - assertInstanceOf(IllegalStateException.class, result.getException()); + assertThat(result.getException()).isInstanceOf(IllegalStateException.class); assertThat(result.getException().getMessage()) .contains("Max agentic tokens (100) exceeded at iteration 0") .contains("prompt=70") @@ -173,7 +172,7 @@ void maxAgenticTokensShouldStopAfterMultipleIterations() { e -> e.getIn().setBody("accumulate over budget")); assertThat(result.getException()).isNotNull(); - assertInstanceOf(IllegalStateException.class, result.getException()); + assertThat(result.getException()).isInstanceOf(IllegalStateException.class); assertThat(result.getException().getMessage()) .contains("Max agentic tokens (80) exceeded at iteration 1") .contains("total=100"); diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc index 5737bcc898a82..d230f8fb3c402 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc @@ -197,6 +197,11 @@ Previously the option was accepted on the endpoint but ignored, so `safeSearch=f === camel-openai +The per-call token usage headers `CamelOpenAIPromptTokens`, `CamelOpenAICompletionTokens`, and +`CamelOpenAITotalTokens` now declare `javaType = Long` in component metadata (previously `Integer`). +The OpenAI SDK returns `long` values, so routes that read these headers with `Integer.class` should +switch to `Long.class`. + The `conversationMemory` feature on the `chat-completion` operation has two behavior fixes: * User messages are now stored in the `CamelOpenAIConversationHistory` exchange property (configurable via