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 3784ba5f85792..08defe1693e56 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 @@ -96,36 +96,38 @@ "embeddingModel": { "index": 17, "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": 18, "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": 19, "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" }, - "maxRetries": { "index": 20, "kind": "parameter", "displayName": "Max Retries", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 2, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Maximum number of times the OpenAI SDK client retries failed requests. The SDK retry is rate-limit aware (honors Retry-After on 429)." }, - "maxTokens": { "index": 21, "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": 22, "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": 23, "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": 24, "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": 25, "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": 26, "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": 27, "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": 28, "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" }, - "requestTimeout": { "index": 29, "kind": "parameter", "displayName": "Request Timeout", "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": "HTTP request timeout in milliseconds for the OpenAI SDK client. When 0 or negative, the SDK default (10 minutes) is used." }, - "storeFullResponse": { "index": 30, "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": 31, "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": 32, "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": 33, "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": 34, "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": 35, "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": 36, "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": 37, "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": 38, "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": 39, "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": 40, "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": 41, "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": 42, "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": 43, "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": 44, "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": 45, "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": 46, "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": 47, "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": 48, "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": 49, "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": 50, "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" } + "maxHistoryMessages": { "index": 20, "kind": "parameter", "displayName": "Max History Messages", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "When conversationMemory is enabled, retain at most this many messages in the exchange conversation history. System and developer messages are prepended separately and are not stored in history. Assistant tool-call blocks are kept intact and may retain slightly more than this limit to preserve tool result pairing. When 0, no message limit is applied." }, + "maxHistoryTokens": { "index": 21, "kind": "parameter", "displayName": "Max History Tokens", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "When conversationMemory is enabled, trim conversation history using a token estimate (character count \/ 4, including image payload size for multi-modal user messages). Oldest segments are dropped first until the estimated tokens are within this limit. Assistant tool-call blocks are removed as a unit with their tool results. The most recent segment is always retained, even when it alone exceeds this limit. When 0, no token limit is applied." }, + "maxRetries": { "index": 22, "kind": "parameter", "displayName": "Max Retries", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 2, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Maximum number of times the OpenAI SDK client retries failed requests. The SDK retry is rate-limit aware (honors Retry-After on 429)." }, + "maxTokens": { "index": 23, "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": 24, "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": 25, "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": 26, "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": 27, "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": 28, "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": 29, "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": 30, "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" }, + "requestTimeout": { "index": 31, "kind": "parameter", "displayName": "Request Timeout", "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": "HTTP request timeout in milliseconds for the OpenAI SDK client. When 0 or negative, the SDK default (10 minutes) is used." }, + "storeFullResponse": { "index": 32, "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": 33, "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": 34, "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": 35, "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": 36, "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": 37, "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": 38, "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": 39, "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": 40, "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": 41, "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": 42, "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": 43, "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": 44, "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": 45, "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": 46, "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": 47, "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": 48, "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": 49, "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": 50, "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": 51, "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": 52, "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/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 4b16f943cc3a0..59cbe85688bfa 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 @@ -62,6 +62,10 @@ 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 "maxhistorymessages": + case "maxHistoryMessages": target.getConfiguration().setMaxHistoryMessages(property(camelContext, int.class, value)); return true; + case "maxhistorytokens": + case "maxHistoryTokens": target.getConfiguration().setMaxHistoryTokens(property(camelContext, int.class, value)); return true; case "maxretries": case "maxRetries": target.getConfiguration().setMaxRetries(property(camelContext, int.class, value)); return true; case "maxtokens": @@ -165,6 +169,10 @@ public Class getOptionType(String name, boolean ignoreCase) { case "jsonSchema": return java.lang.String.class; case "lazystartproducer": case "lazyStartProducer": return boolean.class; + case "maxhistorymessages": + case "maxHistoryMessages": return int.class; + case "maxhistorytokens": + case "maxHistoryTokens": return int.class; case "maxretries": case "maxRetries": return int.class; case "maxtokens": @@ -269,6 +277,10 @@ public Object getOptionValue(Object obj, String name, boolean ignoreCase) { case "jsonSchema": return target.getConfiguration().getJsonSchema(); case "lazystartproducer": case "lazyStartProducer": return target.isLazyStartProducer(); + case "maxhistorymessages": + case "maxHistoryMessages": return target.getConfiguration().getMaxHistoryMessages(); + case "maxhistorytokens": + case "maxHistoryTokens": return target.getConfiguration().getMaxHistoryTokens(); case "maxretries": case "maxRetries": return target.getConfiguration().getMaxRetries(); case "maxtokens": 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 98171fe606ea1..75f9eae5ba4b3 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<>(51); + Set props = new HashSet<>(53); props.add("additionalBodyProperty"); props.add("additionalHeader"); props.add("additionalResponseHeader"); @@ -45,6 +45,8 @@ public class OpenAIEndpointUriFactory extends org.apache.camel.support.component props.add("encodingFormat"); props.add("jsonSchema"); props.add("lazyStartProducer"); + props.add("maxHistoryMessages"); + props.add("maxHistoryTokens"); props.add("maxRetries"); props.add("maxTokens"); props.add("maxToolIterations"); 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 3784ba5f85792..08defe1693e56 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 @@ -96,36 +96,38 @@ "embeddingModel": { "index": 17, "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": 18, "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": 19, "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" }, - "maxRetries": { "index": 20, "kind": "parameter", "displayName": "Max Retries", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 2, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Maximum number of times the OpenAI SDK client retries failed requests. The SDK retry is rate-limit aware (honors Retry-After on 429)." }, - "maxTokens": { "index": 21, "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": 22, "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": 23, "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": 24, "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": 25, "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": 26, "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": 27, "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": 28, "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" }, - "requestTimeout": { "index": 29, "kind": "parameter", "displayName": "Request Timeout", "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": "HTTP request timeout in milliseconds for the OpenAI SDK client. When 0 or negative, the SDK default (10 minutes) is used." }, - "storeFullResponse": { "index": 30, "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": 31, "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": 32, "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": 33, "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": 34, "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": 35, "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": 36, "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": 37, "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": 38, "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": 39, "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": 40, "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": 41, "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": 42, "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": 43, "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": 44, "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": 45, "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": 46, "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": 47, "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": 48, "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": 49, "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": 50, "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" } + "maxHistoryMessages": { "index": 20, "kind": "parameter", "displayName": "Max History Messages", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "When conversationMemory is enabled, retain at most this many messages in the exchange conversation history. System and developer messages are prepended separately and are not stored in history. Assistant tool-call blocks are kept intact and may retain slightly more than this limit to preserve tool result pairing. When 0, no message limit is applied." }, + "maxHistoryTokens": { "index": 21, "kind": "parameter", "displayName": "Max History Tokens", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 0, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "When conversationMemory is enabled, trim conversation history using a token estimate (character count \/ 4, including image payload size for multi-modal user messages). Oldest segments are dropped first until the estimated tokens are within this limit. Assistant tool-call blocks are removed as a unit with their tool results. The most recent segment is always retained, even when it alone exceeds this limit. When 0, no token limit is applied." }, + "maxRetries": { "index": 22, "kind": "parameter", "displayName": "Max Retries", "group": "producer", "label": "", "required": false, "type": "integer", "javaType": "int", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 2, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Maximum number of times the OpenAI SDK client retries failed requests. The SDK retry is rate-limit aware (honors Retry-After on 429)." }, + "maxTokens": { "index": 23, "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": 24, "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": 25, "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": 26, "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": 27, "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": 28, "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": 29, "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": 30, "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" }, + "requestTimeout": { "index": 31, "kind": "parameter", "displayName": "Request Timeout", "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": "HTTP request timeout in milliseconds for the OpenAI SDK client. When 0 or negative, the SDK default (10 minutes) is used." }, + "storeFullResponse": { "index": 32, "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": 33, "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": 34, "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": 35, "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": 36, "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": 37, "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": 38, "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": 39, "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": 40, "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": 41, "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": 42, "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": 43, "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": 44, "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": 45, "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": 46, "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": 47, "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": 48, "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": 49, "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": 50, "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": 51, "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": 52, "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/docs/openai-component.adoc b/components/camel-ai/camel-openai/src/main/docs/openai-component.adoc index ee3cac44cb516..9a6c87ccf5374 100644 --- a/components/camel-ai/camel-openai/src/main/docs/openai-component.adoc +++ b/components/camel-ai/camel-openai/src/main/docs/openai-component.adoc @@ -672,6 +672,8 @@ The JSON schema must be a valid JSON object. Invalid schema strings will result When `conversationMemory=true`, the component maintains conversation history in the `CamelOpenAIConversationHistory` exchange property (configurable via `conversationHistoryProperty` option). This history is scoped to a single Exchange and allows multi-turn conversations within a route. +Use `maxHistoryMessages` and `maxHistoryTokens` to bound how much history is retained in that property. Oldest conversation segments are dropped first; assistant tool-call blocks are always removed together with their tool results. The most recent segment is always kept, even when it alone exceeds `maxHistoryMessages` or `maxHistoryTokens`. Token limits use a character-count / 4 estimate (including image payload size for multi-modal user messages). + IMPORTANT: * Conversation history is automatically updated with each assistant response for **non-streaming** responses only 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 21ca68ffcc4a6..66dc4ee206af6 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 @@ -101,6 +101,22 @@ public class OpenAIConfiguration implements Cloneable { @Metadata(description = "Exchange property name for storing conversation history") private String conversationHistoryProperty = "CamelOpenAIConversationHistory"; + @UriParam(defaultValue = "0") + @Metadata(description = "When conversationMemory is enabled, retain at most this many messages in the exchange " + + "conversation history. System and developer messages are prepended separately and are not " + + "stored in history. Assistant tool-call blocks are kept intact and may retain slightly " + + "more than this limit to preserve tool result pairing. When 0, no message limit is applied.") + private int maxHistoryMessages; + + @UriParam(defaultValue = "0") + @Metadata(description = "When conversationMemory is enabled, trim conversation history using a token estimate " + + "(character count / 4, including image payload size for multi-modal user messages). " + + "Oldest segments are dropped first until the estimated tokens are within this limit. " + + "Assistant tool-call blocks are removed as a unit with their tool results. The most recent " + + "segment is always retained, even when it alone exceeds this limit. When 0, " + + "no token limit is applied.") + private int maxHistoryTokens; + @UriParam @Metadata(description = "Default user message text to use when no prompt is provided", largeInput = true) private String userMessage; @@ -382,6 +398,22 @@ public void setConversationHistoryProperty(String conversationHistoryProperty) { this.conversationHistoryProperty = conversationHistoryProperty; } + public int getMaxHistoryMessages() { + return maxHistoryMessages; + } + + public void setMaxHistoryMessages(int maxHistoryMessages) { + this.maxHistoryMessages = maxHistoryMessages; + } + + public int getMaxHistoryTokens() { + return maxHistoryTokens; + } + + public void setMaxHistoryTokens(int maxHistoryTokens) { + this.maxHistoryTokens = maxHistoryTokens; + } + public String getUserMessage() { return userMessage; } diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConversationHistoryTrimmer.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConversationHistoryTrimmer.java new file mode 100644 index 0000000000000..39f63d1489ab1 --- /dev/null +++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConversationHistoryTrimmer.java @@ -0,0 +1,248 @@ +/* + * 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.ArrayList; +import java.util.List; + +import com.openai.models.chat.completions.ChatCompletionAssistantMessageParam; +import com.openai.models.chat.completions.ChatCompletionContentPart; +import com.openai.models.chat.completions.ChatCompletionMessageParam; +import com.openai.models.chat.completions.ChatCompletionMessageToolCall; +import com.openai.models.chat.completions.ChatCompletionToolMessageParam; +import com.openai.models.chat.completions.ChatCompletionUserMessageParam; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Applies sliding-window limits to exchange-scoped OpenAI conversation history. Trimming removes whole segments from + * the oldest side so assistant tool-call blocks stay paired with their tool result messages. + */ +final class OpenAIConversationHistoryTrimmer { + + private static final Logger LOG = LoggerFactory.getLogger(OpenAIConversationHistoryTrimmer.class); + private static final int CHARS_PER_TOKEN = 4; + + private OpenAIConversationHistoryTrimmer() { + } + + static List trim( + List history, OpenAIConfiguration config) { + if (history == null) { + return null; + } + if (history.isEmpty()) { + return new ArrayList<>(); + } + + int maxMessages = config.getMaxHistoryMessages(); + int maxTokens = config.getMaxHistoryTokens(); + if (maxMessages <= 0 && maxTokens <= 0) { + return history; + } + + int originalSize = history.size(); + List segments = buildSegments(history); + int firstSegment = 0; + + if (maxMessages > 0) { + firstSegment = findFirstSegmentForMessageLimit(segments, maxMessages); + } + + if (maxTokens > 0) { + firstSegment = Math.max(firstSegment, + findFirstSegmentForTokenLimit(history, segments, maxTokens)); + } + + List trimmed; + if (firstSegment >= segments.size()) { + trimmed = new ArrayList<>(); + } else { + Segment first = segments.get(firstSegment); + Segment last = segments.get(segments.size() - 1); + trimmed = new ArrayList<>(history.subList(first.start, last.end + 1)); + } + + int dropped = originalSize - trimmed.size(); + if (dropped > 0) { + LOG.debug("Trimmed conversation history: dropped {} message(s), retained {}", dropped, trimmed.size()); + } + + return trimmed; + } + + static int estimateTokens(List messages) { + return estimateTokens(messages, 0, messages.size()); + } + + private static int estimateTokens(List messages, int from, int to) { + long chars = 0; + for (int i = from; i < to; i++) { + chars += estimateMessageChars(messages.get(i)); + } + return tokensFromChars(chars); + } + + private static int tokensFromChars(long chars) { + if (chars <= 0) { + return 0; + } + long maxChars = (long) Integer.MAX_VALUE * CHARS_PER_TOKEN; + if (chars > maxChars) { + return Integer.MAX_VALUE; + } + return (int) ((chars + CHARS_PER_TOKEN - 1) / CHARS_PER_TOKEN); + } + + private static int findFirstSegmentForMessageLimit(List segments, int maxMessages) { + int firstSegment = segments.size() - 1; + int messageCount = segments.get(firstSegment).messageCount(); + while (firstSegment > 0) { + Segment previous = segments.get(firstSegment - 1); + int nextCount = messageCount + previous.messageCount(); + if (nextCount > maxMessages) { + break; + } + firstSegment--; + messageCount = nextCount; + } + return firstSegment; + } + + private static int findFirstSegmentForTokenLimit( + List history, List segments, int maxTokens) { + int lastSegment = segments.size() - 1; + Segment last = segments.get(lastSegment); + int lastSegmentTokens = estimateTokens(history, last.start, last.end + 1); + if (lastSegmentTokens > maxTokens) { + LOG.warn( + "Last conversation segment alone exceeds maxHistoryTokens (estimated {} > {}); " + + "retaining it anyway", + lastSegmentTokens, maxTokens); + return lastSegment; + } + + for (int firstSegment = 0; firstSegment < segments.size(); firstSegment++) { + Segment first = segments.get(firstSegment); + if (estimateTokens(history, first.start, last.end + 1) <= maxTokens) { + return firstSegment; + } + } + return lastSegment; + } + + private static List buildSegments(List history) { + List segments = new ArrayList<>(); + int index = 0; + while (index < history.size()) { + ChatCompletionMessageParam message = history.get(index); + if (isAssistantWithToolCalls(message)) { + int end = index; + while (end + 1 < history.size() && history.get(end + 1).tool().isPresent()) { + end++; + } + segments.add(new Segment(index, end)); + index = end + 1; + } else { + segments.add(new Segment(index, index)); + index++; + } + } + return segments; + } + + private static boolean isAssistantWithToolCalls(ChatCompletionMessageParam message) { + return message.assistant() + .map(assistant -> !assistant.toolCalls().orElse(List.of()).isEmpty()) + .orElse(false); + } + + private static int estimateMessageChars(ChatCompletionMessageParam message) { + return message.user() + .map(OpenAIConversationHistoryTrimmer::estimateUserMessageChars) + .orElseGet(() -> message.assistant() + .map(OpenAIConversationHistoryTrimmer::estimateAssistantMessageChars) + .orElseGet(() -> message.tool() + .map(OpenAIConversationHistoryTrimmer::estimateToolMessageChars) + .orElse(0))); + } + + private static int estimateUserMessageChars(ChatCompletionUserMessageParam user) { + ChatCompletionUserMessageParam.Content content = user.content(); + if (content.isText()) { + return content.asText().length(); + } + if (content.isArrayOfContentParts()) { + return content.asArrayOfContentParts().stream() + .mapToInt(OpenAIConversationHistoryTrimmer::estimateContentPartChars) + .sum(); + } + return 0; + } + + private static int estimateAssistantMessageChars(ChatCompletionAssistantMessageParam assistant) { + int chars = assistant.content() + .filter(ChatCompletionAssistantMessageParam.Content::isText) + .map(content -> content.asText().length()) + .orElse(0); + + for (ChatCompletionMessageToolCall toolCall : assistant.toolCalls().orElse(List.of())) { + chars += toolCall.asFunction().function().name().length(); + chars += toolCall.asFunction().function().arguments().length(); + } + + return chars; + } + + private static int estimateToolMessageChars(ChatCompletionToolMessageParam tool) { + ChatCompletionToolMessageParam.Content content = tool.content(); + if (content.isText()) { + return content.asText().length(); + } + if (content.isArrayOfContentParts()) { + return content.asArrayOfContentParts().stream() + .mapToInt(part -> part.text().length()) + .sum(); + } + return 0; + } + + private static int estimateContentPartChars(ChatCompletionContentPart part) { + int chars = 0; + if (part.text().isPresent()) { + chars += part.asText().text().length(); + } + if (part.imageUrl().isPresent()) { + chars += part.asImageUrl().imageUrl().url().length(); + } + return chars; + } + + private static final class Segment { + private final int start; + private final int end; + + private Segment(int start, int end) { + this.start = start; + this.end = end; + } + + private int messageCount() { + return end - start + 1; + } + } +} 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..a9493c2d50dc9 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 @@ -272,11 +272,16 @@ private void addConversationHistory( return; } - List history = in.getExchange().getProperty( + Exchange exchange = in.getExchange(); + List history = exchange.getProperty( config.getConversationHistoryProperty(), List.class); if (history != null) { - messages.addAll(history); + List trimmed = OpenAIConversationHistoryTrimmer.trim(history, config); + if (trimmed.size() != history.size()) { + exchange.setProperty(config.getConversationHistoryProperty(), trimmed); + } + messages.addAll(trimmed); } } @@ -726,7 +731,8 @@ private void updateConversationHistory( .build()); history.add(assistantMessage); - exchange.setProperty(config.getConversationHistoryProperty(), history); + exchange.setProperty(config.getConversationHistoryProperty(), + OpenAIConversationHistoryTrimmer.trim(history, config)); } private void updateConversationHistory( @@ -758,9 +764,10 @@ private void updateConversationHistory( .build()); history.add(assistantMessage); - exchange.setProperty(config.getConversationHistoryProperty(), history); + List trimmed = OpenAIConversationHistoryTrimmer.trim(history, config); + exchange.setProperty(config.getConversationHistoryProperty(), trimmed); LOG.debug("Updated conversation history with {} agentic messages + final response, total entries: {}", - agenticMessages.size(), history.size()); + agenticMessages.size(), trimmed.size()); } private void updateConversationHistory( @@ -791,9 +798,10 @@ private void updateConversationHistory( .build()); history.add(assistantMessage); - exchange.setProperty(config.getConversationHistoryProperty(), history); + List trimmed = OpenAIConversationHistoryTrimmer.trim(history, config); + exchange.setProperty(config.getConversationHistoryProperty(), trimmed); LOG.debug("Updated conversation history with {} agentic messages + returnDirect result, total entries: {}", - agenticMessages.size(), history.size()); + agenticMessages.size(), trimmed.size()); } private ResponseFormatJsonSchema.JsonSchema.Schema buildSchemaFromJson(String jsonSchemaString) throws Exception { diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIToolExecutionProducer.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIToolExecutionProducer.java index 857e0e04f1f1e..e5a01352dddf1 100644 --- a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIToolExecutionProducer.java +++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIToolExecutionProducer.java @@ -173,7 +173,7 @@ public void process(Exchange exchange) throws Exception { } // Update conversation history and clear body for the next chat-completion call - exchange.setProperty(historyProperty, history); + exchange.setProperty(historyProperty, OpenAIConversationHistoryTrimmer.trim(history, config)); exchange.getMessage().setBody(null); exchange.getMessage().setHeader(OpenAIConstants.TOOL_ITERATIONS, executedCount); } diff --git a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIConversationHistorySizeTest.java b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIConversationHistorySizeTest.java new file mode 100644 index 0000000000000..9055b6b76232b --- /dev/null +++ b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIConversationHistorySizeTest.java @@ -0,0 +1,104 @@ +/* + * 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.List; +import java.util.concurrent.atomic.AtomicReference; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +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; + +class OpenAIConversationHistorySizeTest extends CamelTestSupport { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final AtomicReference fourthRequestBody = new AtomicReference<>(); + + @RegisterExtension + public OpenAIMock openAIMock = new OpenAIMock().builder() + .when("turn-1") + .replyWith("reply-1") + .end() + .when("turn-2") + .replyWith("reply-2") + .end() + .when("turn-3") + .replyWith("reply-3") + .end() + .when("turn-4") + .assertRequest(fourthRequestBody::set) + .replyWith("reply-4") + .end() + .build(); + + @Override + protected RouteBuilder createRouteBuilder() { + String endpoint = "openai:chat-completion?model=gpt-5&apiKey=dummy&conversationMemory=true" + + "&maxHistoryMessages=2&baseUrl=" + openAIMock.getBaseUrl() + "/v1"; + + return new RouteBuilder() { + @Override + public void configure() { + from("direct:multi-turn") + .to(endpoint) + .setBody(constant("turn-2")) + .to(endpoint) + .setBody(constant("turn-3")) + .to(endpoint) + .setBody(constant("turn-4")) + .to(endpoint); + } + }; + } + + @Test + void maxHistoryMessagesShouldRetainOnlyRecentHistoryInRequestsAndExchangeProperty() throws Exception { + Exchange result = template.request("direct:multi-turn", e -> e.getIn().setBody("turn-1")); + + assertThat(result.getException()).isNull(); + assertThat(result.getMessage().getBody(String.class)).isEqualTo("reply-4"); + + @SuppressWarnings("unchecked") + List history = result.getProperty("CamelOpenAIConversationHistory", List.class); + assertThat(history).hasSize(2); + + String request = fourthRequestBody.get(); + assertThat(request).isNotNull(); + + JsonNode messages = MAPPER.readTree(request).get("messages"); + assertThat(messages).isNotNull(); + assertThat(messages) + .noneMatch(message -> "turn-1".equals(message.path("content").asText()) + || "reply-1".equals(message.path("content").asText()) + || "turn-2".equals(message.path("content").asText()) + || "reply-2".equals(message.path("content").asText())); + assertThat(messages) + .anyMatch(message -> "turn-3".equals(message.path("content").asText())); + assertThat(messages) + .anyMatch(message -> "reply-3".equals(message.path("content").asText())); + assertThat(messages) + .anyMatch(message -> "turn-4".equals(message.path("content").asText())); + } +} diff --git a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIConversationHistoryTokenLimitTest.java b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIConversationHistoryTokenLimitTest.java new file mode 100644 index 0000000000000..2072271837034 --- /dev/null +++ b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIConversationHistoryTokenLimitTest.java @@ -0,0 +1,110 @@ +/* + * 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.List; +import java.util.concurrent.atomic.AtomicReference; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.openai.models.chat.completions.ChatCompletionMessageParam; +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; + +class OpenAIConversationHistoryTokenLimitTest extends CamelTestSupport { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final String TURN_1 = "turn-1" + "a".repeat(34); + private static final String TURN_2 = "turn-2" + "b".repeat(34); + private static final String TURN_3 = "turn-3" + "c".repeat(34); + private static final String TURN_4 = "turn-4" + "d".repeat(34); + + private final AtomicReference fourthRequestBody = new AtomicReference<>(); + + @RegisterExtension + public OpenAIMock openAIMock = new OpenAIMock().builder() + .when(TURN_1) + .replyWith("reply-1") + .end() + .when(TURN_2) + .replyWith("reply-2") + .end() + .when(TURN_3) + .replyWith("reply-3") + .end() + .when(TURN_4) + .assertRequest(fourthRequestBody::set) + .replyWith("reply-4") + .end() + .build(); + + @Override + protected RouteBuilder createRouteBuilder() { + String endpoint = "openai:chat-completion?model=gpt-5&apiKey=dummy&conversationMemory=true" + + "&maxHistoryTokens=15&baseUrl=" + openAIMock.getBaseUrl() + "/v1"; + + return new RouteBuilder() { + @Override + public void configure() { + from("direct:token-limited") + .to(endpoint) + .setBody(constant(TURN_2)) + .to(endpoint) + .setBody(constant(TURN_3)) + .to(endpoint) + .setBody(constant(TURN_4)) + .to(endpoint); + } + }; + } + + @Test + void maxHistoryTokensShouldTrimHistoryAndAllowSubsequentCalls() throws Exception { + Exchange result = template.request("direct:token-limited", e -> e.getIn().setBody(TURN_1)); + + assertThat(result.getException()).isNull(); + assertThat(result.getMessage().getBody(String.class)).isEqualTo("reply-4"); + + @SuppressWarnings("unchecked") + List history + = result.getProperty("CamelOpenAIConversationHistory", List.class); + assertThat(history).isNotEmpty(); + assertThat(OpenAIConversationHistoryTrimmer.estimateTokens(history)).isLessThanOrEqualTo(15); + + String request = fourthRequestBody.get(); + assertThat(request).isNotNull(); + + JsonNode messages = MAPPER.readTree(request).get("messages"); + assertThat(messages).isNotNull(); + assertThat(messages) + .noneMatch(message -> TURN_1.equals(message.path("content").asText()) + || "reply-1".equals(message.path("content").asText()) + || TURN_2.equals(message.path("content").asText())); + assertThat(messages) + .anyMatch(message -> TURN_3.equals(message.path("content").asText())); + assertThat(messages) + .anyMatch(message -> "reply-3".equals(message.path("content").asText())); + assertThat(messages) + .anyMatch(message -> TURN_4.equals(message.path("content").asText())); + } +} diff --git a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIConversationHistoryTrimmerTest.java b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIConversationHistoryTrimmerTest.java new file mode 100644 index 0000000000000..78f5a64ebbe48 --- /dev/null +++ b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIConversationHistoryTrimmerTest.java @@ -0,0 +1,274 @@ +/* + * 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.ArrayList; +import java.util.List; + +import com.openai.models.chat.completions.ChatCompletionAssistantMessageParam; +import com.openai.models.chat.completions.ChatCompletionContentPart; +import com.openai.models.chat.completions.ChatCompletionContentPartImage; +import com.openai.models.chat.completions.ChatCompletionContentPartText; +import com.openai.models.chat.completions.ChatCompletionMessageFunctionToolCall; +import com.openai.models.chat.completions.ChatCompletionMessageParam; +import com.openai.models.chat.completions.ChatCompletionMessageToolCall; +import com.openai.models.chat.completions.ChatCompletionToolMessageParam; +import com.openai.models.chat.completions.ChatCompletionUserMessageParam; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class OpenAIConversationHistoryTrimmerTest { + + @Test + void trimShouldReturnSameListWhenNoLimitsConfigured() { + List history = List.of( + userMessage("one"), + assistantMessage("two")); + + List trimmed = OpenAIConversationHistoryTrimmer.trim(history, config(0, 0)); + + assertThat(trimmed).isSameAs(history); + } + + @Test + void trimShouldRetainMostRecentMessagesWhenMaxHistoryMessagesIsSet() { + List history = new ArrayList<>(); + history.add(userMessage("msg-1")); + history.add(assistantMessage("reply-1")); + history.add(userMessage("msg-2")); + history.add(assistantMessage("reply-2")); + history.add(userMessage("msg-3")); + history.add(assistantMessage("reply-3")); + + List trimmed = OpenAIConversationHistoryTrimmer.trim(history, config(2, 0)); + + assertThat(trimmed).hasSize(2); + assertThat(extractText(trimmed.get(0))).isEqualTo("msg-3"); + assertThat(extractText(trimmed.get(1))).isEqualTo("reply-3"); + } + + @Test + void trimShouldDropOldestMessagesWhenMaxHistoryTokensIsExceeded() { + List history = List.of( + userMessage(repeat('a', 40)), + assistantMessage(repeat('b', 40)), + userMessage(repeat('c', 40))); + + List trimmed = OpenAIConversationHistoryTrimmer.trim(history, config(0, 15)); + + assertThat(OpenAIConversationHistoryTrimmer.estimateTokens(trimmed)).isLessThanOrEqualTo(15); + assertThat(trimmed).hasSize(1); + assertThat(extractText(trimmed.get(0))).isEqualTo(repeat('c', 40)); + } + + @Test + void trimShouldApplyBothMessageAndTokenLimits() { + List history = new ArrayList<>(); + for (int i = 1; i <= 6; i++) { + history.add(userMessage("user-" + i + repeat('x', 36))); + history.add(assistantMessage("assistant-" + i)); + } + + List trimmed = OpenAIConversationHistoryTrimmer.trim(history, config(4, 20)); + + assertThat(trimmed.size()).isLessThanOrEqualTo(4); + assertThat(OpenAIConversationHistoryTrimmer.estimateTokens(trimmed)).isLessThanOrEqualTo(20); + assertThat(trimmed.stream().map(OpenAIConversationHistoryTrimmerTest::extractText)) + .noneMatch(text -> text.contains("user-1") || text.contains("assistant-1")); + assertThat(trimmed.stream().map(OpenAIConversationHistoryTrimmerTest::extractText)) + .anyMatch(text -> text.contains("user-6")); + } + + @Test + void trimShouldCountToolCallArgumentsTowardTokenEstimate() { + ChatCompletionMessageParam assistantWithToolCall = ChatCompletionMessageParam.ofAssistant( + ChatCompletionAssistantMessageParam.builder() + .toolCalls(List.of( + ChatCompletionMessageToolCall.ofFunction( + ChatCompletionMessageFunctionToolCall.builder() + .id("call-1") + .function(ChatCompletionMessageFunctionToolCall.Function.builder() + .name("lookup") + .arguments("{\"query\":\"" + repeat('z', 80) + "\"}") + .build()) + .build()))) + .build()); + + List history = List.of( + userMessage("short"), + assistantWithToolCall, + toolMessage("tool-result")); + + List trimmed = OpenAIConversationHistoryTrimmer.trim(history, config(0, 10)); + + assertThat(trimmed).hasSize(2); + assertThat(trimmed.get(0).assistant()).isPresent(); + assertThat(trimmed.get(1).tool()).isPresent(); + } + + @Test + void trimShouldReturnMutableListThatSupportsSubsequentAdds() { + List history = List.of( + userMessage(repeat('a', 40)), + assistantMessage(repeat('b', 40)), + userMessage(repeat('c', 40))); + + List trimmed = OpenAIConversationHistoryTrimmer.trim(history, config(0, 15)); + + trimmed.add(userMessage("next")); + + assertThat(trimmed).hasSize(2); + assertThat(extractText(trimmed.get(0))).isEqualTo(repeat('c', 40)); + assertThat(extractText(trimmed.get(1))).isEqualTo("next"); + } + + @Test + void trimShouldReturnMutableEmptyListForEmptyHistory() { + List trimmed + = OpenAIConversationHistoryTrimmer.trim(new ArrayList<>(), config(0, 10)); + + trimmed.add(userMessage("one")); + + assertThat(trimmed).hasSize(1); + } + + @Test + void trimShouldRetainLastSegmentWhenItAloneExceedsTokenLimit() { + List history = List.of( + userMessage("older"), + assistantMessage("older-reply"), + userMessage(repeat('x', 80))); + + List trimmed = OpenAIConversationHistoryTrimmer.trim(history, config(0, 10)); + + assertThat(trimmed).hasSize(1); + assertThat(extractText(trimmed.get(0))).isEqualTo(repeat('x', 80)); + } + + @Test + void trimShouldKeepAssistantToolCallBlockIntactWhenMessageLimitWouldSplitIt() { + List history = List.of( + userMessage("older"), + assistantMessage("older-reply"), + userMessage("latest"), + assistantWithToolCall("call-1"), + toolMessage("tool-result"), + assistantMessage("final-reply")); + + List trimmed = OpenAIConversationHistoryTrimmer.trim(history, config(2, 0)); + + assertThat(trimmed).hasSize(1); + assertThat(trimmed.get(0).assistant()).isPresent(); + assertThat(extractText(trimmed.get(0))).isEqualTo("final-reply"); + assertThat(trimmed.stream().noneMatch(message -> message.tool().isPresent())).isTrue(); + } + + @Test + void trimShouldRetainAssistantAndToolResultsAsOneSegment() { + List history = List.of( + assistantWithToolCall("call-1"), + toolMessage("tool-result")); + + List trimmed = OpenAIConversationHistoryTrimmer.trim(history, config(2, 0)); + + assertThat(trimmed).hasSize(2); + assertThat(trimmed.get(0).assistant()).isPresent(); + assertThat(trimmed.get(1).tool()).isPresent(); + } + + @Test + void estimateTokensShouldIncludeImagePayloadSize() { + String imageUrl = "data:image/png;base64," + repeat('x', 400); + ChatCompletionMessageParam imageMessage = ChatCompletionMessageParam.ofUser( + ChatCompletionUserMessageParam.builder() + .content(ChatCompletionUserMessageParam.Content.ofArrayOfContentParts(List.of( + ChatCompletionContentPart.ofText( + ChatCompletionContentPartText.builder().text("describe").build()), + ChatCompletionContentPart.ofImageUrl( + ChatCompletionContentPartImage.builder() + .imageUrl(ChatCompletionContentPartImage.ImageUrl.builder() + .url(imageUrl) + .build()) + .build())))) + .build()); + + int tokens = OpenAIConversationHistoryTrimmer.estimateTokens(List.of(imageMessage)); + + assertThat(tokens).isGreaterThan(100); + } + + private static OpenAIConfiguration config(int maxHistoryMessages, int maxHistoryTokens) { + OpenAIConfiguration configuration = new OpenAIConfiguration(); + configuration.setMaxHistoryMessages(maxHistoryMessages); + configuration.setMaxHistoryTokens(maxHistoryTokens); + return configuration; + } + + private static ChatCompletionMessageParam userMessage(String text) { + return ChatCompletionMessageParam.ofUser( + ChatCompletionUserMessageParam.builder() + .content(ChatCompletionUserMessageParam.Content.ofText(text)) + .build()); + } + + private static ChatCompletionMessageParam assistantMessage(String text) { + return ChatCompletionMessageParam.ofAssistant( + ChatCompletionAssistantMessageParam.builder() + .content(ChatCompletionAssistantMessageParam.Content.ofText(text)) + .build()); + } + + private static ChatCompletionMessageParam assistantWithToolCall(String toolCallId) { + return ChatCompletionMessageParam.ofAssistant( + ChatCompletionAssistantMessageParam.builder() + .toolCalls(List.of( + ChatCompletionMessageToolCall.ofFunction( + ChatCompletionMessageFunctionToolCall.builder() + .id(toolCallId) + .function(ChatCompletionMessageFunctionToolCall.Function.builder() + .name("lookup") + .arguments("{}") + .build()) + .build()))) + .build()); + } + + private static ChatCompletionMessageParam toolMessage(String text) { + return ChatCompletionMessageParam.ofTool( + ChatCompletionToolMessageParam.builder() + .toolCallId("call-1") + .content(text) + .build()); + } + + private static String extractText(ChatCompletionMessageParam message) { + return message.user() + .map(user -> user.content().isText() ? user.content().asText() : "") + .orElseGet(() -> message.assistant() + .flatMap(assistant -> assistant.content() + .filter(ChatCompletionAssistantMessageParam.Content::isText) + .map(ChatCompletionAssistantMessageParam.Content::asText)) + .orElseGet(() -> message.tool() + .map(tool -> tool.content().isText() ? tool.content().asText() : "") + .orElse(""))); + } + + private static String repeat(char value, int count) { + return String.valueOf(value).repeat(count); + } +} 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 fc175c550c1b5..901192e02a690 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,6 +480,88 @@ default OpenAIEndpointBuilder jsonSchema(String jsonSchema) { doSetProperty("jsonSchema", jsonSchema); return this; } + /** + * When conversationMemory is enabled, retain at most this many messages + * in the exchange conversation history. System and developer messages + * are prepended separately and are not stored in history. Assistant + * tool-call blocks are kept intact and may retain slightly more than + * this limit to preserve tool result pairing. When 0, no message limit + * is applied. + * + * The option is a: int type. + * + * Default: 0 + * Group: producer + * + * @param maxHistoryMessages the value to set + * @return the dsl builder + */ + default OpenAIEndpointBuilder maxHistoryMessages(int maxHistoryMessages) { + doSetProperty("maxHistoryMessages", maxHistoryMessages); + return this; + } + /** + * When conversationMemory is enabled, retain at most this many messages + * in the exchange conversation history. System and developer messages + * are prepended separately and are not stored in history. Assistant + * tool-call blocks are kept intact and may retain slightly more than + * this limit to preserve tool result pairing. When 0, no message limit + * is applied. + * + * The option will be converted to a int type. + * + * Default: 0 + * Group: producer + * + * @param maxHistoryMessages the value to set + * @return the dsl builder + */ + default OpenAIEndpointBuilder maxHistoryMessages(String maxHistoryMessages) { + doSetProperty("maxHistoryMessages", maxHistoryMessages); + return this; + } + /** + * When conversationMemory is enabled, trim conversation history using a + * token estimate (character count / 4, including image payload size for + * multi-modal user messages). Oldest segments are dropped first until + * the estimated tokens are within this limit. Assistant tool-call + * blocks are removed as a unit with their tool results. The most recent + * segment is always retained, even when it alone exceeds this limit. + * When 0, no token limit is applied. + * + * The option is a: int type. + * + * Default: 0 + * Group: producer + * + * @param maxHistoryTokens the value to set + * @return the dsl builder + */ + default OpenAIEndpointBuilder maxHistoryTokens(int maxHistoryTokens) { + doSetProperty("maxHistoryTokens", maxHistoryTokens); + return this; + } + /** + * When conversationMemory is enabled, trim conversation history using a + * token estimate (character count / 4, including image payload size for + * multi-modal user messages). Oldest segments are dropped first until + * the estimated tokens are within this limit. Assistant tool-call + * blocks are removed as a unit with their tool results. The most recent + * segment is always retained, even when it alone exceeds this limit. + * When 0, no token limit is applied. + * + * The option will be converted to a int type. + * + * Default: 0 + * Group: producer + * + * @param maxHistoryTokens the value to set + * @return the dsl builder + */ + default OpenAIEndpointBuilder maxHistoryTokens(String maxHistoryTokens) { + doSetProperty("maxHistoryTokens", maxHistoryTokens); + return this; + } /** * Maximum number of times the OpenAI SDK client retries failed * requests. The SDK retry is rate-limit aware (honors Retry-After on