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 3ded34a943e71..e636baf460ac6 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 @@ -63,30 +63,37 @@ "CamelOpenAIAgenticTotalTokens": { "index": 26, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Cumulative total tokens consumed across all agentic loop iterations", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AGENTIC_TOTAL_TOKENS" }, "CamelOpenAIResponse": { "index": 27, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "com.openai.models.chat.completions.ChatCompletion", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The complete OpenAI chat completion response object", "constantName": "org.apache.camel.component.openai.OpenAIConstants#RESPONSE" }, "CamelOpenAIResponsesResponse": { "index": 28, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "com.openai.models.responses.Response", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The complete OpenAI Responses API response object", "constantName": "org.apache.camel.component.openai.OpenAIConstants#RESPONSES_RESPONSE" }, - "CamelOpenAIEmbeddingModel": { "index": 29, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The model to use for embeddings", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_MODEL" }, - "CamelOpenAIEmbeddingDimensions": { "index": 30, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Number of output dimensions", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_DIMENSIONS" }, - "CamelOpenAIEmbeddingResponseModel": { "index": 31, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The embedding model used in the response", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_RESPONSE_MODEL" }, - "CamelOpenAIEmbeddingCount": { "index": 32, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Number of embeddings returned", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_COUNT" }, - "CamelOpenAIEmbeddingVectorSize": { "index": 33, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Vector dimensions of the embeddings", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_VECTOR_SIZE" }, - "CamelOpenAIReferenceEmbedding": { "index": 34, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "List", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Reference embedding vector for similarity comparison", "constantName": "org.apache.camel.component.openai.OpenAIConstants#REFERENCE_EMBEDDING" }, - "CamelOpenAISimilarityScore": { "index": 35, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Calculated cosine similarity score (0.0 to 1.0)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SIMILARITY_SCORE" }, - "CamelOpenAIOriginalText": { "index": 36, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String or List", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Original text content when embeddings operation is used", "constantName": "org.apache.camel.component.openai.OpenAIConstants#ORIGINAL_TEXT" }, - "CamelOpenAIAudioModel": { "index": 37, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The model to use for audio transcription", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_MODEL" }, - "CamelOpenAIAudioLanguage": { "index": 38, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The language of the input audio (ISO-639-1)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_LANGUAGE" }, - "CamelOpenAIAudioResponseFormat": { "index": 39, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The response format for audio transcription (json, text, srt, verbose_json, vtt)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_RESPONSE_FORMAT" }, - "CamelOpenAIAudioTemperature": { "index": 40, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sampling temperature for audio transcription (0.0 to 1.0)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_TEMPERATURE" }, - "CamelOpenAIAudioPrompt": { "index": 41, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Optional text to guide the model's style or continue a previous audio segment", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_PROMPT" }, - "CamelOpenAIAudioTimestampGranularities": { "index": 42, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Comma-separated timestamp granularities: word, segment, or word,segment (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_TIMESTAMP_GRANULARITIES" }, - "CamelOpenAIAudioDuration": { "index": 43, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Duration of the audio in seconds (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_DURATION" }, - "CamelOpenAIAudioDetectedLanguage": { "index": 44, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Language detected in the audio (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_DETECTED_LANGUAGE" }, - "CamelOpenAISpeechModel": { "index": 45, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The model to use for text-to-speech (e.g., gpt-4o-mini-tts, tts-1, tts-1-hd)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SPEECH_MODEL" }, - "CamelOpenAISpeechVoice": { "index": 46, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The voice to use for the generated audio (e.g., alloy, echo, fable, onyx, nova, shimmer)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SPEECH_VOICE" }, - "CamelOpenAISpeechResponseFormat": { "index": 47, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The audio format for text-to-speech output (mp3, opus, aac, flac, wav, pcm)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SPEECH_RESPONSE_FORMAT" }, - "CamelOpenAISpeechSpeed": { "index": 48, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The speed of the generated audio (0.25 to 4.0, where 1.0 is normal speed)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SPEECH_SPEED" }, - "CamelOpenAISpeechInstructions": { "index": 49, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Optional instructions to control the voice of the generated audio (does not work with tts-1 or tts-1-hd)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SPEECH_INSTRUCTIONS" } + "CamelOpenAIModerationResponse": { "index": 29, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "com.openai.models.moderations.ModerationCreateResponse", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The complete OpenAI moderation response object", "constantName": "org.apache.camel.component.openai.OpenAIConstants#MODERATION_RESPONSE" }, + "CamelOpenAIEmbeddingModel": { "index": 30, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The model to use for embeddings", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_MODEL" }, + "CamelOpenAIEmbeddingDimensions": { "index": 31, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Number of output dimensions", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_DIMENSIONS" }, + "CamelOpenAIEmbeddingResponseModel": { "index": 32, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The embedding model used in the response", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_RESPONSE_MODEL" }, + "CamelOpenAIEmbeddingCount": { "index": 33, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Number of embeddings returned", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_COUNT" }, + "CamelOpenAIEmbeddingVectorSize": { "index": 34, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Vector dimensions of the embeddings", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_VECTOR_SIZE" }, + "CamelOpenAIReferenceEmbedding": { "index": 35, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "List", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Reference embedding vector for similarity comparison", "constantName": "org.apache.camel.component.openai.OpenAIConstants#REFERENCE_EMBEDDING" }, + "CamelOpenAISimilarityScore": { "index": 36, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Calculated cosine similarity score (0.0 to 1.0)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SIMILARITY_SCORE" }, + "CamelOpenAIOriginalText": { "index": 37, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String or List", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Original text content when embeddings operation is used", "constantName": "org.apache.camel.component.openai.OpenAIConstants#ORIGINAL_TEXT" }, + "CamelOpenAIModerationModel": { "index": 38, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The model to use for moderation (e.g., omni-moderation-latest)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#MODERATION_MODEL" }, + "CamelOpenAIModerationFlagged": { "index": 39, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Whether the moderation API flagged the input as violating the usage policies. For a batch of inputs this is true when at least one input was flagged", "constantName": "org.apache.camel.component.openai.OpenAIConstants#MODERATION_FLAGGED" }, + "CamelOpenAIModerationResults": { "index": 40, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "java.util.List>", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "One verdict per moderated input, in the order of the inputs. Each entry holds the keys 'input', 'flagged', 'categories' and 'categoryScores', so a batch can be split and routed per item", "constantName": "org.apache.camel.component.openai.OpenAIConstants#MODERATION_RESULTS" }, + "CamelOpenAIModerationCategories": { "index": 41, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "java.util.Map", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The moderation categories and whether each one was violated, for a single input. Not set for a list body, where 'CamelOpenAIModerationResults' carries the verdicts", "constantName": "org.apache.camel.component.openai.OpenAIConstants#MODERATION_CATEGORIES" }, + "CamelOpenAIModerationCategoryScores": { "index": 42, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "java.util.Map", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The moderation confidence score per category, for a single input. Not set for a list body, where 'CamelOpenAIModerationResults' carries the verdicts", "constantName": "org.apache.camel.component.openai.OpenAIConstants#MODERATION_CATEGORY_SCORES" }, + "CamelOpenAIModerationResponseModel": { "index": 43, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The moderation model used in the response", "constantName": "org.apache.camel.component.openai.OpenAIConstants#MODERATION_RESPONSE_MODEL" }, + "CamelOpenAIAudioModel": { "index": 44, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The model to use for audio transcription", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_MODEL" }, + "CamelOpenAIAudioLanguage": { "index": 45, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The language of the input audio (ISO-639-1)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_LANGUAGE" }, + "CamelOpenAIAudioResponseFormat": { "index": 46, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The response format for audio transcription (json, text, srt, verbose_json, vtt)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_RESPONSE_FORMAT" }, + "CamelOpenAIAudioTemperature": { "index": 47, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sampling temperature for audio transcription (0.0 to 1.0)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_TEMPERATURE" }, + "CamelOpenAIAudioPrompt": { "index": 48, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Optional text to guide the model's style or continue a previous audio segment", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_PROMPT" }, + "CamelOpenAIAudioTimestampGranularities": { "index": 49, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Comma-separated timestamp granularities: word, segment, or word,segment (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_TIMESTAMP_GRANULARITIES" }, + "CamelOpenAIAudioDuration": { "index": 50, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Duration of the audio in seconds (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_DURATION" }, + "CamelOpenAIAudioDetectedLanguage": { "index": 51, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Language detected in the audio (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_DETECTED_LANGUAGE" }, + "CamelOpenAISpeechModel": { "index": 52, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The model to use for text-to-speech (e.g., gpt-4o-mini-tts, tts-1, tts-1-hd)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SPEECH_MODEL" }, + "CamelOpenAISpeechVoice": { "index": 53, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The voice to use for the generated audio (e.g., alloy, echo, fable, onyx, nova, shimmer)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SPEECH_VOICE" }, + "CamelOpenAISpeechResponseFormat": { "index": 54, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The audio format for text-to-speech output (mp3, opus, aac, flac, wav, pcm)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SPEECH_RESPONSE_FORMAT" }, + "CamelOpenAISpeechSpeed": { "index": 55, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The speed of the generated audio (0.25 to 4.0, where 1.0 is normal speed)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SPEECH_SPEED" }, + "CamelOpenAISpeechInstructions": { "index": 56, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Optional instructions to control the voice of the generated audio (does not work with tts-1 or tts-1-hd)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SPEECH_INSTRUCTIONS" } }, "properties": { - "operation": { "index": 0, "kind": "path", "displayName": "Operation", "group": "producer", "label": "", "required": true, "type": "enum", "javaType": "org.apache.camel.component.openai.OpenAIOperations", "enum": [ "chat-completion", "responses", "embeddings", "tool-execution", "audio-transcription", "audio-translation", "audio-speech" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The operation to perform: 'chat-completion', 'responses', 'embeddings', 'tool-execution', 'audio-transcription', 'audio-translation', or 'audio-speech'" }, + "operation": { "index": 0, "kind": "path", "displayName": "Operation", "group": "producer", "label": "", "required": true, "type": "enum", "javaType": "org.apache.camel.component.openai.OpenAIOperations", "enum": [ "chat-completion", "responses", "embeddings", "tool-execution", "audio-transcription", "audio-translation", "audio-speech", "moderation" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The operation to perform: 'chat-completion', 'responses', 'embeddings', 'tool-execution', 'audio-transcription', 'audio-translation', 'audio-speech', or 'moderation'" }, "additionalBodyProperty": { "index": 1, "kind": "parameter", "displayName": "Additional Body Property", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "java.util.Map", "prefix": "additionalBodyProperty.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Additional JSON properties to include in the request body (e.g. additionalBodyProperty.traceId=123). This is a multi-value option with prefix: additionalBodyProperty." }, "additionalHeader": { "index": 2, "kind": "parameter", "displayName": "Additional Header", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "java.util.Map", "prefix": "additionalHeader.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Additional HTTP request headers to send with every API call (e.g. additionalHeader.OpenAI-Organization=my-org or additionalHeader.api-key=secret). Values may contain secrets. This is a multi-value option with prefix: additionalHeader." }, "additionalResponseHeader": { "index": 3, "kind": "parameter", "displayName": "Additional Response Header", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "java.util.Map", "prefix": "additionalResponseHeader.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Map additional fields from the response message to Camel headers. The key is the field name in the API response, the value is the Camel header name (e.g. additionalResponseHeader.reasoning_content=CamelMyReasoningHeader). This is a multi-value option with prefix: additionalResponseHeader." }, @@ -122,37 +129,38 @@ "mcpTimeout": { "index": 33, "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." }, "mcpToolRefresh": { "index": 34, "kind": "parameter", "displayName": "Mcp Tool Refresh", "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": "Refresh the advertised tool list when an MCP server notifies that its tools changed. Set to false to keep the tool list fixed to what was listed when the endpoint started, for deployments that require a deterministic set of tools." }, "model": { "index": 35, "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": 36, "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" }, - "parallelToolExecution": { "index": 37, "kind": "parameter", "displayName": "Parallel Tool Execution", "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": "Execute the tool calls returned by the model in a single response concurrently instead of sequentially. Tool calls in the same batch are independent by design, so this reduces the latency of a batch to that of its slowest tool. Results are always fed back to the model in the original tool call order. Note that with toolExecutionErrorStrategy=failExchange the sibling tool calls already dispatched complete before the exchange fails." }, - "parallelToolTimeout": { "index": 38, "kind": "parameter", "displayName": "Parallel Tool 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": "Timeout in milliseconds for a batch of parallel tool calls, so that one slow tool cannot block the whole batch. The timeout applies to the batch as a whole, not per tool call. A tool call that exceeds it is cancelled and handled according to toolExecutionErrorStrategy. The default of 0 disables the batch timeout and relies on mcpTimeout, which already bounds each individual MCP request. Only used when parallelToolExecution=true." }, - "previousResponseId": { "index": 39, "kind": "parameter", "displayName": "Previous Response Id", "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": "Previous response id for OpenAI server-side conversation state (Responses API only)" }, - "requestTimeout": { "index": 40, "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." }, - "speechInstructions": { "index": 41, "kind": "parameter", "displayName": "Speech Instructions", "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": "Optional instructions to control the voice of the generated audio. Does not work with tts-1 or tts-1-hd." }, - "speechModel": { "index": 42, "kind": "parameter", "displayName": "Speech 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 text-to-speech (e.g., gpt-4o-mini-tts, tts-1, tts-1-hd)" }, - "speechResponseFormat": { "index": 43, "kind": "parameter", "displayName": "Speech Response Format", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "mp3", "opus", "aac", "flac", "wav", "pcm" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "mp3", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The audio format for text-to-speech output" }, - "speechSpeed": { "index": 44, "kind": "parameter", "displayName": "Speech Speed", "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": "The speed of the generated audio, from 0.25 to 4.0 where 1.0 is normal speed" }, - "speechVoice": { "index": 45, "kind": "parameter", "displayName": "Speech Voice", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "alloy", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The voice to use for text-to-speech (e.g., alloy, echo, fable, onyx, nova, shimmer). See the OpenAI documentation for the full list of supported voices." }, - "storeFullResponse": { "index": 46, "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 SDK response in non-streaming mode: chat-completion uses exchange property 'CamelOpenAIResponse'; responses uses 'CamelOpenAIResponsesResponse'" }, - "streaming": { "index": 47, "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": 48, "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": 49, "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": 50, "kind": "parameter", "displayName": "Temperature", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Temperature for response generation (0.0 to 2.0)" }, - "toolExecutionErrorStrategy": { "index": 51, "kind": "parameter", "displayName": "Tool Execution Error Strategy", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "org.apache.camel.component.openai.ToolExecutionErrorStrategy", "enum": [ "failExchange", "repromptModel" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "failExchange", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Strategy for handling exceptions thrown during MCP tool execution. 'failExchange' (default) propagates the exception to the Camel exchange so that standard Camel error handling (onException, dead-letter channel) can process it. This is the safer default because 'repromptModel' sends raw exception messages (which may contain connection strings, hostnames, or internal paths) to a third-party LLM provider. 'repromptModel' catches the error and sends it back to the model as a tool result so the model can attempt to recover." }, - "topP": { "index": 52, "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": 53, "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": 54, "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": 55, "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": 56, "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": 57, "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": 58, "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": 59, "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": 60, "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": 61, "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": 62, "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": 63, "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": 64, "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": 65, "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": 66, "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": 67, "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" } + "moderationModel": { "index": 36, "kind": "parameter", "displayName": "Moderation Model", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "omni-moderation-latest", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The model to use for moderation" }, + "outputClass": { "index": 37, "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" }, + "parallelToolExecution": { "index": 38, "kind": "parameter", "displayName": "Parallel Tool Execution", "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": "Execute the tool calls returned by the model in a single response concurrently instead of sequentially. Tool calls in the same batch are independent by design, so this reduces the latency of a batch to that of its slowest tool. Results are always fed back to the model in the original tool call order. Note that with toolExecutionErrorStrategy=failExchange the sibling tool calls already dispatched complete before the exchange fails." }, + "parallelToolTimeout": { "index": 39, "kind": "parameter", "displayName": "Parallel Tool 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": "Timeout in milliseconds for a batch of parallel tool calls, so that one slow tool cannot block the whole batch. The timeout applies to the batch as a whole, not per tool call. A tool call that exceeds it is cancelled and handled according to toolExecutionErrorStrategy. The default of 0 disables the batch timeout and relies on mcpTimeout, which already bounds each individual MCP request. Only used when parallelToolExecution=true." }, + "previousResponseId": { "index": 40, "kind": "parameter", "displayName": "Previous Response Id", "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": "Previous response id for OpenAI server-side conversation state (Responses API only)" }, + "requestTimeout": { "index": 41, "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." }, + "speechInstructions": { "index": 42, "kind": "parameter", "displayName": "Speech Instructions", "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": "Optional instructions to control the voice of the generated audio. Does not work with tts-1 or tts-1-hd." }, + "speechModel": { "index": 43, "kind": "parameter", "displayName": "Speech 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 text-to-speech (e.g., gpt-4o-mini-tts, tts-1, tts-1-hd)" }, + "speechResponseFormat": { "index": 44, "kind": "parameter", "displayName": "Speech Response Format", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "mp3", "opus", "aac", "flac", "wav", "pcm" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "mp3", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The audio format for text-to-speech output" }, + "speechSpeed": { "index": 45, "kind": "parameter", "displayName": "Speech Speed", "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": "The speed of the generated audio, from 0.25 to 4.0 where 1.0 is normal speed" }, + "speechVoice": { "index": 46, "kind": "parameter", "displayName": "Speech Voice", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "alloy", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The voice to use for text-to-speech (e.g., alloy, echo, fable, onyx, nova, shimmer). See the OpenAI documentation for the full list of supported voices." }, + "storeFullResponse": { "index": 47, "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 SDK response in non-streaming mode: chat-completion uses exchange property 'CamelOpenAIResponse'; responses uses 'CamelOpenAIResponsesResponse'; moderation uses 'CamelOpenAIModerationResponse'" }, + "streaming": { "index": 48, "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": 49, "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": 50, "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": 51, "kind": "parameter", "displayName": "Temperature", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Temperature for response generation (0.0 to 2.0)" }, + "toolExecutionErrorStrategy": { "index": 52, "kind": "parameter", "displayName": "Tool Execution Error Strategy", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "org.apache.camel.component.openai.ToolExecutionErrorStrategy", "enum": [ "failExchange", "repromptModel" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "failExchange", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Strategy for handling exceptions thrown during MCP tool execution. 'failExchange' (default) propagates the exception to the Camel exchange so that standard Camel error handling (onException, dead-letter channel) can process it. This is the safer default because 'repromptModel' sends raw exception messages (which may contain connection strings, hostnames, or internal paths) to a third-party LLM provider. 'repromptModel' catches the error and sends it back to the model as a tool result so the model can attempt to recover." }, + "topP": { "index": 53, "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": 54, "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": 55, "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": 56, "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": 57, "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": 58, "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": 59, "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": 60, "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": 61, "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": 62, "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": 63, "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": 64, "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": 65, "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": 66, "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": 67, "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": 68, "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/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-component.adoc index d2961f939c958..6d5637f0ddde4 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-component.adoc @@ -14,7 +14,7 @@ *{component-header}* -The OpenAI component provides integration with OpenAI and OpenAI-compatible APIs for chat completion, text embeddings, audio transcription, audio translation, and text-to-speech using the official openai-java SDK. +The OpenAI component provides integration with OpenAI and OpenAI-compatible APIs for chat completion, text embeddings, content moderation, audio transcription, audio translation, and text-to-speech using the official openai-java SDK. Maven users will need to add the following dependency to their `pom.xml` for this component: @@ -47,6 +47,7 @@ See xref:others:openai-responses.adoc[Responses API operation] for usage (`previ * `audio-transcription` - Transcribe audio files to text using speech-to-text models (e.g., Whisper, GPT-4o Transcribe) * `audio-translation` - Transcribe and translate audio files into English text (e.g., Whisper) * `audio-speech` - Synthesize spoken audio from text using text-to-speech models (e.g., gpt-4o-mini-tts, tts-1) +* `moderation` - Check text against the OpenAI usage policies before it reaches a model // component options: START include::partial$component-configure-options.adoc[] @@ -1254,17 +1255,20 @@ For more details on specific features, see: * xref:others:openai-responses.adoc[Responses API operation] - OpenAI Responses API, hosted tools, and server-side conversation state * xref:others:openai-mcp.adoc[MCP Tool Calling] - Model Context Protocol server configuration, agentic loop, streaming, and connection recovery * xref:others:openai-providers.adoc[OpenAI-Compatible Providers] - Using Ollama, LM Studio, vLLM, and OpenRouter as alternative backends -* xref:others:openai-operations.adoc[Embeddings and Audio Operations] - Text embeddings, vector database integration, and audio transcription +* xref:others:openai-operations.adoc[Embeddings, Moderation and Audio Operations] - Text embeddings, vector database integration, content moderation, and audio transcription == Error Handling The component may throw the following exceptions: * `IllegalArgumentException`: - ** When an invalid operation is specified (supported: `chat-completion`, `embeddings`, `tool-execution`, `audio-transcription`, `audio-translation`, `audio-speech`) + ** When an invalid operation is specified (supported: `chat-completion`, `responses`, `embeddings`, `tool-execution`, `audio-transcription`, `audio-translation`, `audio-speech`, `moderation`) ** When message body or user message is missing ** When the audio model is missing (audio-transcription, audio-translation) or the speech model is missing (audio-speech) ** When image file is provided without userMessage (chat-completion) ** When unsupported file type is provided (only text and image files are supported) ** When invalid JSON schema string is provided + ** When the moderation input list is empty or contains null elements (moderation) +* `CamelExchangeException`: + ** When moderation returns a number of results that does not match the number of inputs (moderation) * API-specific exceptions from the OpenAI SDK for network errors, authentication failures, rate limiting, etc. diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-operations.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-operations.adoc index f1eaa1905b127..99590c19cc364 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-operations.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-operations.adoc @@ -1,4 +1,4 @@ -= OpenAI - Embeddings and Audio Operations += OpenAI - Embeddings, Moderation and Audio Operations :tabs-sync-option: xref:ROOT:openai-component.adoc[Back to OpenAI Component] @@ -168,6 +168,131 @@ The following headers are set after an embeddings request: | `CamelOpenAISimilarityScore` | Double | Cosine similarity (if reference embedding provided) |=== +== Moderation Operation + +The `moderation` operation checks text against the OpenAI usage policies. It is the canonical pre-filter for untrusted +input on a public-facing route: rejecting policy-violating content before spending chat tokens or triggering tool calls. + +The message body is passed through unchanged and the verdict is exposed as headers, so the result can be used for +content-based routing while the original content stays available to the rest of the route. + +IMPORTANT: Moderation is a policy filter, not a trust boundary. The verdict is probabilistic and its categories are +defined by the provider, so it is not a substitute for authentication, authorization, schema validation or defences +against prompt injection. The operation only reports a verdict — a flagged body keeps flowing unless the route stops +or replaces it, as in the example below. + +NOTE: The operation moderates text only. The body, or each element of a list body, is converted to a `String` and sent +as text input; the multi-modal inputs of the moderation API are not exposed. + +=== Guarding a Route + +[tabs] +==== +Java:: ++ +[source,java] +---- +from("platform-http:/chat") + .to("openai:moderation?moderationModel=omni-moderation-latest") + .choice() + .when(header(OpenAIConstants.MODERATION_FLAGGED).isEqualTo(true)) + .setBody(constant("Your message violates our usage policy.")) + .otherwise() + .to("openai:chat-completion?model=gpt-5") + .end(); +---- + +YAML:: ++ +[source,yaml] +---- +- from: + uri: platform-http:/chat + steps: + - to: openai:moderation?moderationModel=omni-moderation-latest + - choice: + when: + - simple: "${header.CamelOpenAIModerationFlagged} == true" + steps: + - setBody: + constant: "Your message violates our usage policy." + otherwise: + steps: + - to: openai:chat-completion?model=gpt-5 +---- +==== + +=== Verdicts per Input + +`CamelOpenAIModerationResults` always holds one verdict per moderated input, in the order of the inputs. Each entry is +a map with the keys `input`, `flagged`, `categories` and `categoryScores`, which makes a batch straightforward to split +and route per item: + +[source,java] +---- +from("direct:moderate-batch") + .to("openai:moderation") + .split(header(OpenAIConstants.MODERATION_RESULTS)) + .choice() + .when(simple("${body[flagged]}")) + .to("direct:quarantine") + .otherwise() + .to("direct:downstream") + .end(); +---- + +A `List` body moderates every element in a single API call, and `CamelOpenAIModerationFlagged` is then `true` when at +least one element was flagged. + +For a single input, the same categories are also exposed as plain maps in `CamelOpenAIModerationCategories` and +`CamelOpenAIModerationCategoryScores`, which is convenient for acting on one category directly — for example routing +anything the model is fairly confident about to human review: + +[source,java] +---- +from("direct:moderate") + .to("openai:moderation") + .choice() + .when(simple("${header.CamelOpenAIModerationCategoryScores[hate]} > 0.85")) + .to("direct:human-review") + .otherwise() + .to("direct:downstream") + .end(); +---- + +Those two headers are not set for a list body, where `CamelOpenAIModerationResults` carries the verdicts. + +=== Failure Modes + +The operation is meant to gate untrusted content, so it fails the exchange rather than letting a message through +without a verdict: + +* the API returning a number of results that does not match the number of inputs raises a `CamelExchangeException`, + instead of leaving `CamelOpenAIModerationFlagged` as `false`; +* a missing body, an empty list, or a list containing `null` elements raises an `IllegalArgumentException`. + +=== Moderation Output Headers + +The following headers are set after a moderation request: + +[cols="1,1,3"] +|=== +| Header | Type | Description + +| `CamelOpenAIModerationFlagged` | Boolean | Whether the input violates the usage policies. For a batch, `true` when at least one input was flagged +| `CamelOpenAIModerationResults` | List | One verdict per input, in input order. Each entry holds `input`, `flagged`, `categories` and `categoryScores` +| `CamelOpenAIModerationCategories` | Map | Category name to violation flag, for a single input. Not set for a list body +| `CamelOpenAIModerationCategoryScores` | Map | Category name to confidence score, for a single input. Not set for a list body +| `CamelOpenAIModerationResponseModel` | String | The model used for moderation +|=== + +The category names are the ones returned by the API, for example `hate`, `hate/threatening`, `self-harm/intent`, +`sexual/minors` and `violence/graphic`. + +NOTE: The `illicit` and `illicit/violent` categories are optional in the API model. OpenAI returns them, but an +xref:others:openai-providers.adoc[OpenAI-compatible provider] may not, in which case they are absent from the +category map. The score map always contains every category. + == Audio Transcription Operation The `audio-transcription` operation transcribes audio files to text using OpenAI's speech-to-text models (Whisper, GPT-4o Transcribe). 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 41665230d82d0..e37e21cabaa2f 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 @@ -93,6 +93,8 @@ public boolean configure(CamelContext camelContext, Object obj, String name, Obj case "mcptoolrefresh": case "mcpToolRefresh": target.getConfiguration().setMcpToolRefresh(property(camelContext, boolean.class, value)); return true; case "model": target.getConfiguration().setModel(property(camelContext, java.lang.String.class, value)); return true; + case "moderationmodel": + case "moderationModel": target.getConfiguration().setModerationModel(property(camelContext, java.lang.String.class, value)); return true; case "oauthprofile": case "oauthProfile": target.getConfiguration().setOauthProfile(property(camelContext, java.lang.String.class, value)); return true; case "outputclass": @@ -230,6 +232,8 @@ public Class getOptionType(String name, boolean ignoreCase) { case "mcptoolrefresh": case "mcpToolRefresh": return boolean.class; case "model": return java.lang.String.class; + case "moderationmodel": + case "moderationModel": return java.lang.String.class; case "oauthprofile": case "oauthProfile": return java.lang.String.class; case "outputclass": @@ -368,6 +372,8 @@ public Object getOptionValue(Object obj, String name, boolean ignoreCase) { case "mcptoolrefresh": case "mcpToolRefresh": return target.getConfiguration().isMcpToolRefresh(); case "model": return target.getConfiguration().getModel(); + case "moderationmodel": + case "moderationModel": return target.getConfiguration().getModerationModel(); case "oauthprofile": case "oauthProfile": return target.getConfiguration().getOauthProfile(); case "outputclass": 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 f09adda487803..c8c8445903be3 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<>(68); + Set props = new HashSet<>(69); props.add("additionalBodyProperty"); props.add("additionalHeader"); props.add("additionalResponseHeader"); @@ -61,6 +61,7 @@ public class OpenAIEndpointUriFactory extends org.apache.camel.support.component props.add("mcpTimeout"); props.add("mcpToolRefresh"); props.add("model"); + props.add("moderationModel"); props.add("oauthProfile"); props.add("operation"); props.add("outputClass"); 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 3ded34a943e71..e636baf460ac6 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 @@ -63,30 +63,37 @@ "CamelOpenAIAgenticTotalTokens": { "index": 26, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Cumulative total tokens consumed across all agentic loop iterations", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AGENTIC_TOTAL_TOKENS" }, "CamelOpenAIResponse": { "index": 27, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "com.openai.models.chat.completions.ChatCompletion", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The complete OpenAI chat completion response object", "constantName": "org.apache.camel.component.openai.OpenAIConstants#RESPONSE" }, "CamelOpenAIResponsesResponse": { "index": 28, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "com.openai.models.responses.Response", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The complete OpenAI Responses API response object", "constantName": "org.apache.camel.component.openai.OpenAIConstants#RESPONSES_RESPONSE" }, - "CamelOpenAIEmbeddingModel": { "index": 29, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The model to use for embeddings", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_MODEL" }, - "CamelOpenAIEmbeddingDimensions": { "index": 30, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Number of output dimensions", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_DIMENSIONS" }, - "CamelOpenAIEmbeddingResponseModel": { "index": 31, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The embedding model used in the response", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_RESPONSE_MODEL" }, - "CamelOpenAIEmbeddingCount": { "index": 32, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Number of embeddings returned", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_COUNT" }, - "CamelOpenAIEmbeddingVectorSize": { "index": 33, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Vector dimensions of the embeddings", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_VECTOR_SIZE" }, - "CamelOpenAIReferenceEmbedding": { "index": 34, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "List", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Reference embedding vector for similarity comparison", "constantName": "org.apache.camel.component.openai.OpenAIConstants#REFERENCE_EMBEDDING" }, - "CamelOpenAISimilarityScore": { "index": 35, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Calculated cosine similarity score (0.0 to 1.0)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SIMILARITY_SCORE" }, - "CamelOpenAIOriginalText": { "index": 36, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String or List", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Original text content when embeddings operation is used", "constantName": "org.apache.camel.component.openai.OpenAIConstants#ORIGINAL_TEXT" }, - "CamelOpenAIAudioModel": { "index": 37, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The model to use for audio transcription", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_MODEL" }, - "CamelOpenAIAudioLanguage": { "index": 38, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The language of the input audio (ISO-639-1)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_LANGUAGE" }, - "CamelOpenAIAudioResponseFormat": { "index": 39, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The response format for audio transcription (json, text, srt, verbose_json, vtt)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_RESPONSE_FORMAT" }, - "CamelOpenAIAudioTemperature": { "index": 40, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sampling temperature for audio transcription (0.0 to 1.0)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_TEMPERATURE" }, - "CamelOpenAIAudioPrompt": { "index": 41, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Optional text to guide the model's style or continue a previous audio segment", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_PROMPT" }, - "CamelOpenAIAudioTimestampGranularities": { "index": 42, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Comma-separated timestamp granularities: word, segment, or word,segment (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_TIMESTAMP_GRANULARITIES" }, - "CamelOpenAIAudioDuration": { "index": 43, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Duration of the audio in seconds (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_DURATION" }, - "CamelOpenAIAudioDetectedLanguage": { "index": 44, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Language detected in the audio (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_DETECTED_LANGUAGE" }, - "CamelOpenAISpeechModel": { "index": 45, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The model to use for text-to-speech (e.g., gpt-4o-mini-tts, tts-1, tts-1-hd)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SPEECH_MODEL" }, - "CamelOpenAISpeechVoice": { "index": 46, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The voice to use for the generated audio (e.g., alloy, echo, fable, onyx, nova, shimmer)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SPEECH_VOICE" }, - "CamelOpenAISpeechResponseFormat": { "index": 47, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The audio format for text-to-speech output (mp3, opus, aac, flac, wav, pcm)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SPEECH_RESPONSE_FORMAT" }, - "CamelOpenAISpeechSpeed": { "index": 48, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The speed of the generated audio (0.25 to 4.0, where 1.0 is normal speed)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SPEECH_SPEED" }, - "CamelOpenAISpeechInstructions": { "index": 49, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Optional instructions to control the voice of the generated audio (does not work with tts-1 or tts-1-hd)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SPEECH_INSTRUCTIONS" } + "CamelOpenAIModerationResponse": { "index": 29, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "com.openai.models.moderations.ModerationCreateResponse", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The complete OpenAI moderation response object", "constantName": "org.apache.camel.component.openai.OpenAIConstants#MODERATION_RESPONSE" }, + "CamelOpenAIEmbeddingModel": { "index": 30, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The model to use for embeddings", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_MODEL" }, + "CamelOpenAIEmbeddingDimensions": { "index": 31, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Number of output dimensions", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_DIMENSIONS" }, + "CamelOpenAIEmbeddingResponseModel": { "index": 32, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The embedding model used in the response", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_RESPONSE_MODEL" }, + "CamelOpenAIEmbeddingCount": { "index": 33, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Number of embeddings returned", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_COUNT" }, + "CamelOpenAIEmbeddingVectorSize": { "index": 34, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Vector dimensions of the embeddings", "constantName": "org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_VECTOR_SIZE" }, + "CamelOpenAIReferenceEmbedding": { "index": 35, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "List", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Reference embedding vector for similarity comparison", "constantName": "org.apache.camel.component.openai.OpenAIConstants#REFERENCE_EMBEDDING" }, + "CamelOpenAISimilarityScore": { "index": 36, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Calculated cosine similarity score (0.0 to 1.0)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SIMILARITY_SCORE" }, + "CamelOpenAIOriginalText": { "index": 37, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String or List", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Original text content when embeddings operation is used", "constantName": "org.apache.camel.component.openai.OpenAIConstants#ORIGINAL_TEXT" }, + "CamelOpenAIModerationModel": { "index": 38, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The model to use for moderation (e.g., omni-moderation-latest)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#MODERATION_MODEL" }, + "CamelOpenAIModerationFlagged": { "index": 39, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Whether the moderation API flagged the input as violating the usage policies. For a batch of inputs this is true when at least one input was flagged", "constantName": "org.apache.camel.component.openai.OpenAIConstants#MODERATION_FLAGGED" }, + "CamelOpenAIModerationResults": { "index": 40, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "java.util.List>", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "One verdict per moderated input, in the order of the inputs. Each entry holds the keys 'input', 'flagged', 'categories' and 'categoryScores', so a batch can be split and routed per item", "constantName": "org.apache.camel.component.openai.OpenAIConstants#MODERATION_RESULTS" }, + "CamelOpenAIModerationCategories": { "index": 41, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "java.util.Map", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The moderation categories and whether each one was violated, for a single input. Not set for a list body, where 'CamelOpenAIModerationResults' carries the verdicts", "constantName": "org.apache.camel.component.openai.OpenAIConstants#MODERATION_CATEGORIES" }, + "CamelOpenAIModerationCategoryScores": { "index": 42, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "java.util.Map", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The moderation confidence score per category, for a single input. Not set for a list body, where 'CamelOpenAIModerationResults' carries the verdicts", "constantName": "org.apache.camel.component.openai.OpenAIConstants#MODERATION_CATEGORY_SCORES" }, + "CamelOpenAIModerationResponseModel": { "index": 43, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The moderation model used in the response", "constantName": "org.apache.camel.component.openai.OpenAIConstants#MODERATION_RESPONSE_MODEL" }, + "CamelOpenAIAudioModel": { "index": 44, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The model to use for audio transcription", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_MODEL" }, + "CamelOpenAIAudioLanguage": { "index": 45, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The language of the input audio (ISO-639-1)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_LANGUAGE" }, + "CamelOpenAIAudioResponseFormat": { "index": 46, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The response format for audio transcription (json, text, srt, verbose_json, vtt)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_RESPONSE_FORMAT" }, + "CamelOpenAIAudioTemperature": { "index": 47, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sampling temperature for audio transcription (0.0 to 1.0)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_TEMPERATURE" }, + "CamelOpenAIAudioPrompt": { "index": 48, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Optional text to guide the model's style or continue a previous audio segment", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_PROMPT" }, + "CamelOpenAIAudioTimestampGranularities": { "index": 49, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Comma-separated timestamp granularities: word, segment, or word,segment (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_TIMESTAMP_GRANULARITIES" }, + "CamelOpenAIAudioDuration": { "index": 50, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Duration of the audio in seconds (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_DURATION" }, + "CamelOpenAIAudioDetectedLanguage": { "index": 51, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Language detected in the audio (verbose_json only)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#AUDIO_DETECTED_LANGUAGE" }, + "CamelOpenAISpeechModel": { "index": 52, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The model to use for text-to-speech (e.g., gpt-4o-mini-tts, tts-1, tts-1-hd)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SPEECH_MODEL" }, + "CamelOpenAISpeechVoice": { "index": 53, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The voice to use for the generated audio (e.g., alloy, echo, fable, onyx, nova, shimmer)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SPEECH_VOICE" }, + "CamelOpenAISpeechResponseFormat": { "index": 54, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The audio format for text-to-speech output (mp3, opus, aac, flac, wav, pcm)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SPEECH_RESPONSE_FORMAT" }, + "CamelOpenAISpeechSpeed": { "index": 55, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The speed of the generated audio (0.25 to 4.0, where 1.0 is normal speed)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SPEECH_SPEED" }, + "CamelOpenAISpeechInstructions": { "index": 56, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Optional instructions to control the voice of the generated audio (does not work with tts-1 or tts-1-hd)", "constantName": "org.apache.camel.component.openai.OpenAIConstants#SPEECH_INSTRUCTIONS" } }, "properties": { - "operation": { "index": 0, "kind": "path", "displayName": "Operation", "group": "producer", "label": "", "required": true, "type": "enum", "javaType": "org.apache.camel.component.openai.OpenAIOperations", "enum": [ "chat-completion", "responses", "embeddings", "tool-execution", "audio-transcription", "audio-translation", "audio-speech" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The operation to perform: 'chat-completion', 'responses', 'embeddings', 'tool-execution', 'audio-transcription', 'audio-translation', or 'audio-speech'" }, + "operation": { "index": 0, "kind": "path", "displayName": "Operation", "group": "producer", "label": "", "required": true, "type": "enum", "javaType": "org.apache.camel.component.openai.OpenAIOperations", "enum": [ "chat-completion", "responses", "embeddings", "tool-execution", "audio-transcription", "audio-translation", "audio-speech", "moderation" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The operation to perform: 'chat-completion', 'responses', 'embeddings', 'tool-execution', 'audio-transcription', 'audio-translation', 'audio-speech', or 'moderation'" }, "additionalBodyProperty": { "index": 1, "kind": "parameter", "displayName": "Additional Body Property", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "java.util.Map", "prefix": "additionalBodyProperty.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Additional JSON properties to include in the request body (e.g. additionalBodyProperty.traceId=123). This is a multi-value option with prefix: additionalBodyProperty." }, "additionalHeader": { "index": 2, "kind": "parameter", "displayName": "Additional Header", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "java.util.Map", "prefix": "additionalHeader.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Additional HTTP request headers to send with every API call (e.g. additionalHeader.OpenAI-Organization=my-org or additionalHeader.api-key=secret). Values may contain secrets. This is a multi-value option with prefix: additionalHeader." }, "additionalResponseHeader": { "index": 3, "kind": "parameter", "displayName": "Additional Response Header", "group": "producer", "label": "", "required": false, "type": "object", "javaType": "java.util.Map", "prefix": "additionalResponseHeader.", "multiValue": true, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Map additional fields from the response message to Camel headers. The key is the field name in the API response, the value is the Camel header name (e.g. additionalResponseHeader.reasoning_content=CamelMyReasoningHeader). This is a multi-value option with prefix: additionalResponseHeader." }, @@ -122,37 +129,38 @@ "mcpTimeout": { "index": 33, "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." }, "mcpToolRefresh": { "index": 34, "kind": "parameter", "displayName": "Mcp Tool Refresh", "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": "Refresh the advertised tool list when an MCP server notifies that its tools changed. Set to false to keep the tool list fixed to what was listed when the endpoint started, for deployments that require a deterministic set of tools." }, "model": { "index": 35, "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": 36, "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" }, - "parallelToolExecution": { "index": 37, "kind": "parameter", "displayName": "Parallel Tool Execution", "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": "Execute the tool calls returned by the model in a single response concurrently instead of sequentially. Tool calls in the same batch are independent by design, so this reduces the latency of a batch to that of its slowest tool. Results are always fed back to the model in the original tool call order. Note that with toolExecutionErrorStrategy=failExchange the sibling tool calls already dispatched complete before the exchange fails." }, - "parallelToolTimeout": { "index": 38, "kind": "parameter", "displayName": "Parallel Tool 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": "Timeout in milliseconds for a batch of parallel tool calls, so that one slow tool cannot block the whole batch. The timeout applies to the batch as a whole, not per tool call. A tool call that exceeds it is cancelled and handled according to toolExecutionErrorStrategy. The default of 0 disables the batch timeout and relies on mcpTimeout, which already bounds each individual MCP request. Only used when parallelToolExecution=true." }, - "previousResponseId": { "index": 39, "kind": "parameter", "displayName": "Previous Response Id", "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": "Previous response id for OpenAI server-side conversation state (Responses API only)" }, - "requestTimeout": { "index": 40, "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." }, - "speechInstructions": { "index": 41, "kind": "parameter", "displayName": "Speech Instructions", "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": "Optional instructions to control the voice of the generated audio. Does not work with tts-1 or tts-1-hd." }, - "speechModel": { "index": 42, "kind": "parameter", "displayName": "Speech 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 text-to-speech (e.g., gpt-4o-mini-tts, tts-1, tts-1-hd)" }, - "speechResponseFormat": { "index": 43, "kind": "parameter", "displayName": "Speech Response Format", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "mp3", "opus", "aac", "flac", "wav", "pcm" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "mp3", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The audio format for text-to-speech output" }, - "speechSpeed": { "index": 44, "kind": "parameter", "displayName": "Speech Speed", "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": "The speed of the generated audio, from 0.25 to 4.0 where 1.0 is normal speed" }, - "speechVoice": { "index": 45, "kind": "parameter", "displayName": "Speech Voice", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "alloy", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The voice to use for text-to-speech (e.g., alloy, echo, fable, onyx, nova, shimmer). See the OpenAI documentation for the full list of supported voices." }, - "storeFullResponse": { "index": 46, "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 SDK response in non-streaming mode: chat-completion uses exchange property 'CamelOpenAIResponse'; responses uses 'CamelOpenAIResponsesResponse'" }, - "streaming": { "index": 47, "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": 48, "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": 49, "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": 50, "kind": "parameter", "displayName": "Temperature", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Temperature for response generation (0.0 to 2.0)" }, - "toolExecutionErrorStrategy": { "index": 51, "kind": "parameter", "displayName": "Tool Execution Error Strategy", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "org.apache.camel.component.openai.ToolExecutionErrorStrategy", "enum": [ "failExchange", "repromptModel" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "failExchange", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Strategy for handling exceptions thrown during MCP tool execution. 'failExchange' (default) propagates the exception to the Camel exchange so that standard Camel error handling (onException, dead-letter channel) can process it. This is the safer default because 'repromptModel' sends raw exception messages (which may contain connection strings, hostnames, or internal paths) to a third-party LLM provider. 'repromptModel' catches the error and sends it back to the model as a tool result so the model can attempt to recover." }, - "topP": { "index": 52, "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": 53, "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": 54, "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": 55, "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": 56, "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": 57, "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": 58, "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": 59, "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": 60, "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": 61, "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": 62, "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": 63, "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": 64, "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": 65, "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": 66, "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": 67, "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" } + "moderationModel": { "index": 36, "kind": "parameter", "displayName": "Moderation Model", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "omni-moderation-latest", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The model to use for moderation" }, + "outputClass": { "index": 37, "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" }, + "parallelToolExecution": { "index": 38, "kind": "parameter", "displayName": "Parallel Tool Execution", "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": "Execute the tool calls returned by the model in a single response concurrently instead of sequentially. Tool calls in the same batch are independent by design, so this reduces the latency of a batch to that of its slowest tool. Results are always fed back to the model in the original tool call order. Note that with toolExecutionErrorStrategy=failExchange the sibling tool calls already dispatched complete before the exchange fails." }, + "parallelToolTimeout": { "index": 39, "kind": "parameter", "displayName": "Parallel Tool 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": "Timeout in milliseconds for a batch of parallel tool calls, so that one slow tool cannot block the whole batch. The timeout applies to the batch as a whole, not per tool call. A tool call that exceeds it is cancelled and handled according to toolExecutionErrorStrategy. The default of 0 disables the batch timeout and relies on mcpTimeout, which already bounds each individual MCP request. Only used when parallelToolExecution=true." }, + "previousResponseId": { "index": 40, "kind": "parameter", "displayName": "Previous Response Id", "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": "Previous response id for OpenAI server-side conversation state (Responses API only)" }, + "requestTimeout": { "index": 41, "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." }, + "speechInstructions": { "index": 42, "kind": "parameter", "displayName": "Speech Instructions", "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": "Optional instructions to control the voice of the generated audio. Does not work with tts-1 or tts-1-hd." }, + "speechModel": { "index": 43, "kind": "parameter", "displayName": "Speech 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 text-to-speech (e.g., gpt-4o-mini-tts, tts-1, tts-1-hd)" }, + "speechResponseFormat": { "index": 44, "kind": "parameter", "displayName": "Speech Response Format", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "mp3", "opus", "aac", "flac", "wav", "pcm" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "mp3", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The audio format for text-to-speech output" }, + "speechSpeed": { "index": 45, "kind": "parameter", "displayName": "Speech Speed", "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": "The speed of the generated audio, from 0.25 to 4.0 where 1.0 is normal speed" }, + "speechVoice": { "index": 46, "kind": "parameter", "displayName": "Speech Voice", "group": "producer", "label": "", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "alloy", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "The voice to use for text-to-speech (e.g., alloy, echo, fable, onyx, nova, shimmer). See the OpenAI documentation for the full list of supported voices." }, + "storeFullResponse": { "index": 47, "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 SDK response in non-streaming mode: chat-completion uses exchange property 'CamelOpenAIResponse'; responses uses 'CamelOpenAIResponsesResponse'; moderation uses 'CamelOpenAIModerationResponse'" }, + "streaming": { "index": 48, "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": 49, "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": 50, "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": 51, "kind": "parameter", "displayName": "Temperature", "group": "producer", "label": "", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Temperature for response generation (0.0 to 2.0)" }, + "toolExecutionErrorStrategy": { "index": 52, "kind": "parameter", "displayName": "Tool Execution Error Strategy", "group": "producer", "label": "", "required": false, "type": "enum", "javaType": "org.apache.camel.component.openai.ToolExecutionErrorStrategy", "enum": [ "failExchange", "repromptModel" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "failExchange", "configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": "configuration", "description": "Strategy for handling exceptions thrown during MCP tool execution. 'failExchange' (default) propagates the exception to the Camel exchange so that standard Camel error handling (onException, dead-letter channel) can process it. This is the safer default because 'repromptModel' sends raw exception messages (which may contain connection strings, hostnames, or internal paths) to a third-party LLM provider. 'repromptModel' catches the error and sends it back to the model as a tool result so the model can attempt to recover." }, + "topP": { "index": 53, "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": 54, "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": 55, "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": 56, "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": 57, "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": 58, "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": 59, "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": 60, "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": 61, "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": 62, "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": 63, "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": 64, "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": 65, "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": 66, "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": 67, "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": 68, "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 d2961f939c958..6d5637f0ddde4 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 @@ -14,7 +14,7 @@ *{component-header}* -The OpenAI component provides integration with OpenAI and OpenAI-compatible APIs for chat completion, text embeddings, audio transcription, audio translation, and text-to-speech using the official openai-java SDK. +The OpenAI component provides integration with OpenAI and OpenAI-compatible APIs for chat completion, text embeddings, content moderation, audio transcription, audio translation, and text-to-speech using the official openai-java SDK. Maven users will need to add the following dependency to their `pom.xml` for this component: @@ -47,6 +47,7 @@ See xref:others:openai-responses.adoc[Responses API operation] for usage (`previ * `audio-transcription` - Transcribe audio files to text using speech-to-text models (e.g., Whisper, GPT-4o Transcribe) * `audio-translation` - Transcribe and translate audio files into English text (e.g., Whisper) * `audio-speech` - Synthesize spoken audio from text using text-to-speech models (e.g., gpt-4o-mini-tts, tts-1) +* `moderation` - Check text against the OpenAI usage policies before it reaches a model // component options: START include::partial$component-configure-options.adoc[] @@ -1254,17 +1255,20 @@ For more details on specific features, see: * xref:others:openai-responses.adoc[Responses API operation] - OpenAI Responses API, hosted tools, and server-side conversation state * xref:others:openai-mcp.adoc[MCP Tool Calling] - Model Context Protocol server configuration, agentic loop, streaming, and connection recovery * xref:others:openai-providers.adoc[OpenAI-Compatible Providers] - Using Ollama, LM Studio, vLLM, and OpenRouter as alternative backends -* xref:others:openai-operations.adoc[Embeddings and Audio Operations] - Text embeddings, vector database integration, and audio transcription +* xref:others:openai-operations.adoc[Embeddings, Moderation and Audio Operations] - Text embeddings, vector database integration, content moderation, and audio transcription == Error Handling The component may throw the following exceptions: * `IllegalArgumentException`: - ** When an invalid operation is specified (supported: `chat-completion`, `embeddings`, `tool-execution`, `audio-transcription`, `audio-translation`, `audio-speech`) + ** When an invalid operation is specified (supported: `chat-completion`, `responses`, `embeddings`, `tool-execution`, `audio-transcription`, `audio-translation`, `audio-speech`, `moderation`) ** When message body or user message is missing ** When the audio model is missing (audio-transcription, audio-translation) or the speech model is missing (audio-speech) ** When image file is provided without userMessage (chat-completion) ** When unsupported file type is provided (only text and image files are supported) ** When invalid JSON schema string is provided + ** When the moderation input list is empty or contains null elements (moderation) +* `CamelExchangeException`: + ** When moderation returns a number of results that does not match the number of inputs (moderation) * API-specific exceptions from the OpenAI SDK for network errors, authentication failures, rate limiting, etc. diff --git a/components/camel-ai/camel-openai/src/main/docs/openai-operations.adoc b/components/camel-ai/camel-openai/src/main/docs/openai-operations.adoc index f1eaa1905b127..99590c19cc364 100644 --- a/components/camel-ai/camel-openai/src/main/docs/openai-operations.adoc +++ b/components/camel-ai/camel-openai/src/main/docs/openai-operations.adoc @@ -1,4 +1,4 @@ -= OpenAI - Embeddings and Audio Operations += OpenAI - Embeddings, Moderation and Audio Operations :tabs-sync-option: xref:ROOT:openai-component.adoc[Back to OpenAI Component] @@ -168,6 +168,131 @@ The following headers are set after an embeddings request: | `CamelOpenAISimilarityScore` | Double | Cosine similarity (if reference embedding provided) |=== +== Moderation Operation + +The `moderation` operation checks text against the OpenAI usage policies. It is the canonical pre-filter for untrusted +input on a public-facing route: rejecting policy-violating content before spending chat tokens or triggering tool calls. + +The message body is passed through unchanged and the verdict is exposed as headers, so the result can be used for +content-based routing while the original content stays available to the rest of the route. + +IMPORTANT: Moderation is a policy filter, not a trust boundary. The verdict is probabilistic and its categories are +defined by the provider, so it is not a substitute for authentication, authorization, schema validation or defences +against prompt injection. The operation only reports a verdict — a flagged body keeps flowing unless the route stops +or replaces it, as in the example below. + +NOTE: The operation moderates text only. The body, or each element of a list body, is converted to a `String` and sent +as text input; the multi-modal inputs of the moderation API are not exposed. + +=== Guarding a Route + +[tabs] +==== +Java:: ++ +[source,java] +---- +from("platform-http:/chat") + .to("openai:moderation?moderationModel=omni-moderation-latest") + .choice() + .when(header(OpenAIConstants.MODERATION_FLAGGED).isEqualTo(true)) + .setBody(constant("Your message violates our usage policy.")) + .otherwise() + .to("openai:chat-completion?model=gpt-5") + .end(); +---- + +YAML:: ++ +[source,yaml] +---- +- from: + uri: platform-http:/chat + steps: + - to: openai:moderation?moderationModel=omni-moderation-latest + - choice: + when: + - simple: "${header.CamelOpenAIModerationFlagged} == true" + steps: + - setBody: + constant: "Your message violates our usage policy." + otherwise: + steps: + - to: openai:chat-completion?model=gpt-5 +---- +==== + +=== Verdicts per Input + +`CamelOpenAIModerationResults` always holds one verdict per moderated input, in the order of the inputs. Each entry is +a map with the keys `input`, `flagged`, `categories` and `categoryScores`, which makes a batch straightforward to split +and route per item: + +[source,java] +---- +from("direct:moderate-batch") + .to("openai:moderation") + .split(header(OpenAIConstants.MODERATION_RESULTS)) + .choice() + .when(simple("${body[flagged]}")) + .to("direct:quarantine") + .otherwise() + .to("direct:downstream") + .end(); +---- + +A `List` body moderates every element in a single API call, and `CamelOpenAIModerationFlagged` is then `true` when at +least one element was flagged. + +For a single input, the same categories are also exposed as plain maps in `CamelOpenAIModerationCategories` and +`CamelOpenAIModerationCategoryScores`, which is convenient for acting on one category directly — for example routing +anything the model is fairly confident about to human review: + +[source,java] +---- +from("direct:moderate") + .to("openai:moderation") + .choice() + .when(simple("${header.CamelOpenAIModerationCategoryScores[hate]} > 0.85")) + .to("direct:human-review") + .otherwise() + .to("direct:downstream") + .end(); +---- + +Those two headers are not set for a list body, where `CamelOpenAIModerationResults` carries the verdicts. + +=== Failure Modes + +The operation is meant to gate untrusted content, so it fails the exchange rather than letting a message through +without a verdict: + +* the API returning a number of results that does not match the number of inputs raises a `CamelExchangeException`, + instead of leaving `CamelOpenAIModerationFlagged` as `false`; +* a missing body, an empty list, or a list containing `null` elements raises an `IllegalArgumentException`. + +=== Moderation Output Headers + +The following headers are set after a moderation request: + +[cols="1,1,3"] +|=== +| Header | Type | Description + +| `CamelOpenAIModerationFlagged` | Boolean | Whether the input violates the usage policies. For a batch, `true` when at least one input was flagged +| `CamelOpenAIModerationResults` | List | One verdict per input, in input order. Each entry holds `input`, `flagged`, `categories` and `categoryScores` +| `CamelOpenAIModerationCategories` | Map | Category name to violation flag, for a single input. Not set for a list body +| `CamelOpenAIModerationCategoryScores` | Map | Category name to confidence score, for a single input. Not set for a list body +| `CamelOpenAIModerationResponseModel` | String | The model used for moderation +|=== + +The category names are the ones returned by the API, for example `hate`, `hate/threatening`, `self-harm/intent`, +`sexual/minors` and `violence/graphic`. + +NOTE: The `illicit` and `illicit/violent` categories are optional in the API model. OpenAI returns them, but an +xref:others:openai-providers.adoc[OpenAI-compatible provider] may not, in which case they are absent from the +category map. The score map always contains every category. + == Audio Transcription Operation The `audio-transcription` operation transcribes audio files to text using OpenAI's speech-to-text models (Whisper, GPT-4o Transcribe). 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 4afd1b77879c9..ad171fb7d24e0 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 @@ -149,7 +149,8 @@ public class OpenAIConfiguration implements Cloneable { @UriParam(defaultValue = "false") @Metadata(description = "Store the full SDK response in non-streaming mode: chat-completion uses exchange property " - + "'CamelOpenAIResponse'; responses uses 'CamelOpenAIResponsesResponse'") + + "'CamelOpenAIResponse'; responses uses 'CamelOpenAIResponsesResponse'; " + + "moderation uses 'CamelOpenAIModerationResponse'") private boolean storeFullResponse = false; @UriParam(defaultValue = "false") @@ -290,6 +291,12 @@ public class OpenAIConfiguration implements Cloneable { + "Only applicable with verbose_json response format.") private String audioTimestampGranularities; + // ========== MODERATION CONFIGURATION ========== + + @UriParam(defaultValue = "omni-moderation-latest") + @Metadata(description = "The model to use for moderation") + private String moderationModel = "omni-moderation-latest"; + // ========== AUDIO SPEECH (TEXT-TO-SPEECH) CONFIGURATION ========== @UriParam @@ -664,6 +671,14 @@ public void setAudioTimestampGranularities(String audioTimestampGranularities) { this.audioTimestampGranularities = audioTimestampGranularities; } + public String getModerationModel() { + return moderationModel; + } + + public void setModerationModel(String moderationModel) { + this.moderationModel = moderationModel; + } + public String getSpeechModel() { return speechModel; } diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConstants.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConstants.java index 414a8ce4def0b..3315c65be9e63 100644 --- a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConstants.java +++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConstants.java @@ -101,6 +101,9 @@ public final class OpenAIConstants { @Metadata(description = "The complete OpenAI Responses API response object", javaType = "com.openai.models.responses.Response") public static final String RESPONSES_RESPONSE = "CamelOpenAIResponsesResponse"; + @Metadata(description = "The complete OpenAI moderation response object", + javaType = "com.openai.models.moderations.ModerationCreateResponse") + public static final String MODERATION_RESPONSE = "CamelOpenAIModerationResponse"; // Embeddings Input Headers @Metadata(description = "The model to use for embeddings", javaType = "String") @@ -124,6 +127,37 @@ public final class OpenAIConstants { @Metadata(description = "Original text content when embeddings operation is used", javaType = "String or List") public static final String ORIGINAL_TEXT = "CamelOpenAIOriginalText"; + // Moderation Input Headers + @Metadata(description = "The model to use for moderation (e.g., omni-moderation-latest)", javaType = "String") + public static final String MODERATION_MODEL = "CamelOpenAIModerationModel"; + + // Moderation Output Headers + @Metadata(description = "Whether the moderation API flagged the input as violating the usage policies. " + + "For a batch of inputs this is true when at least one input was flagged", + javaType = "Boolean") + public static final String MODERATION_FLAGGED = "CamelOpenAIModerationFlagged"; + @Metadata(description = "One verdict per moderated input, in the order of the inputs. Each entry holds the keys " + + "'input', 'flagged', 'categories' and 'categoryScores', so a batch can be split and " + + "routed per item", + javaType = "java.util.List>") + public static final String MODERATION_RESULTS = "CamelOpenAIModerationResults"; + @Metadata(description = "The moderation categories and whether each one was violated, for a single input. " + + "Not set for a list body, where 'CamelOpenAIModerationResults' carries the verdicts", + javaType = "java.util.Map") + public static final String MODERATION_CATEGORIES = "CamelOpenAIModerationCategories"; + @Metadata(description = "The moderation confidence score per category, for a single input. Not set for a list " + + "body, where 'CamelOpenAIModerationResults' carries the verdicts", + javaType = "java.util.Map") + public static final String MODERATION_CATEGORY_SCORES = "CamelOpenAIModerationCategoryScores"; + + // Keys of a single entry of the CamelOpenAIModerationResults header + public static final String MODERATION_RESULT_INPUT = "input"; + public static final String MODERATION_RESULT_FLAGGED = "flagged"; + public static final String MODERATION_RESULT_CATEGORIES = "categories"; + public static final String MODERATION_RESULT_CATEGORY_SCORES = "categoryScores"; + @Metadata(description = "The moderation model used in the response", javaType = "String") + public static final String MODERATION_RESPONSE_MODEL = "CamelOpenAIModerationResponseModel"; + // Audio Transcription Input Headers @Metadata(description = "The model to use for audio transcription", javaType = "String") public static final String AUDIO_MODEL = "CamelOpenAIAudioModel"; diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIEndpoint.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIEndpoint.java index 63f872b3745af..8210107c16329 100644 --- a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIEndpoint.java +++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIEndpoint.java @@ -85,7 +85,7 @@ public class OpenAIEndpoint extends DefaultEndpoint { @UriPath @Metadata(required = true, description = "The operation to perform: 'chat-completion', 'responses', 'embeddings', 'tool-execution', " - + "'audio-transcription', 'audio-translation', or 'audio-speech'") + + "'audio-transcription', 'audio-translation', 'audio-speech', or 'moderation'") private OpenAIOperations operation; @UriParam @@ -125,6 +125,7 @@ public Producer createProducer() throws Exception { case audioTranscription -> new OpenAIAudioTranscriptionProducer(this); case audioTranslation -> new OpenAIAudioTranslationProducer(this); case audioSpeech -> new OpenAIAudioSpeechProducer(this); + case moderation -> new OpenAIModerationProducer(this); }; } diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIModerationProducer.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIModerationProducer.java new file mode 100644 index 0000000000000..8f53713facc8e --- /dev/null +++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIModerationProducer.java @@ -0,0 +1,212 @@ +/* + * 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.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.openai.models.moderations.Moderation; +import com.openai.models.moderations.ModerationCreateParams; +import com.openai.models.moderations.ModerationCreateResponse; +import org.apache.camel.AsyncCallback; +import org.apache.camel.CamelExchangeException; +import org.apache.camel.Exchange; +import org.apache.camel.Message; +import org.apache.camel.support.DefaultAsyncProducer; +import org.apache.camel.util.ObjectHelper; + +/** + * OpenAI producer for moderating text against the usage policies. + *

+ * The message body is passed through unchanged so the verdict can be used for content-based routing while the original + * content is still available to the rest of the route. + */ +public class OpenAIModerationProducer extends DefaultAsyncProducer { + + public OpenAIModerationProducer(OpenAIEndpoint endpoint) { + super(endpoint); + } + + @Override + public OpenAIEndpoint getEndpoint() { + return (OpenAIEndpoint) super.getEndpoint(); + } + + @Override + public boolean process(Exchange exchange, AsyncCallback callback) { + try { + processInternal(exchange); + callback.done(true); + return true; + } catch (Exception e) { + exchange.setException(e); + callback.done(true); + return true; + } + } + + private void processInternal(Exchange exchange) throws Exception { + OpenAIConfiguration config = getEndpoint().getConfiguration(); + Message in = exchange.getIn(); + + String model = resolveParameter(in, OpenAIConstants.MODERATION_MODEL, + config.getModerationModel(), String.class); + + if (ObjectHelper.isEmpty(model)) { + throw new IllegalArgumentException("Moderation model must be specified via moderationModel parameter"); + } + + boolean batch = in.getBody() instanceof List; + List inputs = extractInputs(exchange); + if (inputs.isEmpty()) { + throw new IllegalArgumentException("No input text provided for moderation"); + } + + ModerationCreateParams.Builder paramsBuilder = ModerationCreateParams.builder() + .model(model); + + if (inputs.size() == 1) { + paramsBuilder.input(inputs.get(0)); + } else { + paramsBuilder.inputOfStrings(inputs); + } + + ModerationCreateResponse response = getEndpoint().getClient() + .moderations().create(paramsBuilder.build()); + + // this operation is used to gate untrusted content, so a missing verdict must fail the exchange + // instead of leaving CamelOpenAIModerationFlagged false and letting the message through + if (response.results().size() != inputs.size()) { + throw new CamelExchangeException( + "Moderation returned " + response.results().size() + " result(s) for " + inputs.size() + + " input(s)", + exchange); + } + + // stored only once the response is known to be complete, so a failed exchange carries no verdict + if (config.isStoreFullResponse()) { + exchange.setProperty(OpenAIConstants.MODERATION_RESPONSE, response); + } + + setResponseHeaders(exchange.getMessage(), response, inputs, batch); + } + + private List extractInputs(Exchange exchange) { + Message in = exchange.getIn(); + Object body = in.getBody(); + List inputs = new ArrayList<>(); + + if (body instanceof String text) { + inputs.add(text); + } else if (body instanceof List list) { + for (Object item : list) { + if (item == null) { + // moderating the literal "null" would silently report a verdict for content that was not sent + throw new IllegalArgumentException("The input list for moderation must not contain null elements"); + } + if (item instanceof String s) { + inputs.add(s); + } else { + // convert as a non-String body would be, so an unconvertible type fails instead of + // being moderated as its toString() + String converted = exchange.getContext().getTypeConverter().tryConvertTo(String.class, exchange, item); + if (converted == null) { + throw new IllegalArgumentException( + "Cannot convert the moderation input of type " + item.getClass().getName() + + " to String"); + } + inputs.add(converted); + } + } + } else if (body != null) { + inputs.add(in.getBody(String.class)); + } + + return inputs; + } + + private void setResponseHeaders( + Message message, ModerationCreateResponse response, List inputs, boolean batch) { + message.setHeader(OpenAIConstants.MODERATION_RESPONSE_MODEL, response.model()); + + List results = response.results(); + message.setHeader(OpenAIConstants.MODERATION_FLAGGED, results.stream().anyMatch(Moderation::flagged)); + + List> verdicts = new ArrayList<>(results.size()); + for (int i = 0; i < results.size(); i++) { + Moderation result = results.get(i); + Map verdict = new LinkedHashMap<>(); + verdict.put(OpenAIConstants.MODERATION_RESULT_INPUT, inputs.get(i)); + verdict.put(OpenAIConstants.MODERATION_RESULT_FLAGGED, result.flagged()); + verdict.put(OpenAIConstants.MODERATION_RESULT_CATEGORIES, toCategories(result.categories())); + verdict.put(OpenAIConstants.MODERATION_RESULT_CATEGORY_SCORES, toCategoryScores(result.categoryScores())); + verdicts.add(verdict); + } + message.setHeader(OpenAIConstants.MODERATION_RESULTS, verdicts); + + if (!batch && results.size() == 1) { + message.setHeader(OpenAIConstants.MODERATION_CATEGORIES, toCategories(results.get(0).categories())); + message.setHeader(OpenAIConstants.MODERATION_CATEGORY_SCORES, + toCategoryScores(results.get(0).categoryScores())); + } + } + + private Map toCategories(Moderation.Categories categories) { + Map map = new LinkedHashMap<>(); + map.put("harassment", categories.harassment()); + map.put("harassment/threatening", categories.harassmentThreatening()); + map.put("hate", categories.hate()); + map.put("hate/threatening", categories.hateThreatening()); + categories.illicit().ifPresent(value -> map.put("illicit", value)); + categories.illicitViolent().ifPresent(value -> map.put("illicit/violent", value)); + map.put("self-harm", categories.selfHarm()); + map.put("self-harm/instructions", categories.selfHarmInstructions()); + map.put("self-harm/intent", categories.selfHarmIntent()); + map.put("sexual", categories.sexual()); + map.put("sexual/minors", categories.sexualMinors()); + map.put("violence", categories.violence()); + map.put("violence/graphic", categories.violenceGraphic()); + return map; + } + + private Map toCategoryScores(Moderation.CategoryScores scores) { + Map map = new LinkedHashMap<>(); + map.put("harassment", scores.harassment()); + map.put("harassment/threatening", scores.harassmentThreatening()); + map.put("hate", scores.hate()); + map.put("hate/threatening", scores.hateThreatening()); + map.put("illicit", scores.illicit()); + map.put("illicit/violent", scores.illicitViolent()); + map.put("self-harm", scores.selfHarm()); + map.put("self-harm/instructions", scores.selfHarmInstructions()); + map.put("self-harm/intent", scores.selfHarmIntent()); + map.put("sexual", scores.sexual()); + map.put("sexual/minors", scores.sexualMinors()); + map.put("violence", scores.violence()); + map.put("violence/graphic", scores.violenceGraphic()); + return map; + } + + private T resolveParameter( + Message message, String headerName, + T defaultValue, Class type) { + T headerValue = message.getHeader(headerName, type); + return ObjectHelper.isNotEmpty(headerValue) ? headerValue : defaultValue; + } +} diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIOperations.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIOperations.java index 56963bac6a930..6679e355f6d1a 100644 --- a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIOperations.java +++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIOperations.java @@ -24,7 +24,8 @@ public enum OpenAIOperations { toolExecution("tool-execution"), audioTranscription("audio-transcription"), audioTranslation("audio-translation"), - audioSpeech("audio-speech"); + audioSpeech("audio-speech"), + moderation("moderation"); private final String value; @@ -41,7 +42,7 @@ public static OpenAIOperations fromValue(String value) { throw new IllegalArgumentException( "Unknown operation: " + value + ". Supported: chat-completion, responses, embeddings, tool-execution, " - + "audio-transcription, audio-translation, audio-speech"); + + "audio-transcription, audio-translation, audio-speech, moderation"); } public String getValue() { diff --git a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIModerationMockTest.java b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIModerationMockTest.java new file mode 100644 index 0000000000000..db5b7ae62602b --- /dev/null +++ b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIModerationMockTest.java @@ -0,0 +1,392 @@ +/* + * 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 java.util.Map; + +import com.openai.models.moderations.ModerationCreateResponse; +import org.apache.camel.CamelExchangeException; +import org.apache.camel.Exchange; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +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 OpenAIModerationMockTest extends CamelTestSupport { + + @RegisterExtension + public OpenAIMock openAIMock = new OpenAIMock().builder() + .whenModeration("Apache Camel is an integration framework") + .replyWithModerationAllowed() + .end() + .whenModeration("I hate everyone") + .replyWithModerationFlagged("hate", 0.92) + .end() + .whenModeration("Another harmless sentence") + .replyWithModerationScore("violence", 0.01) + .end() + .whenModeration("Legacy model input") + .replyWithoutIllicitCategories() + .end() + .whenModeration("42") + .replyWithModerationAllowed() + .end() + .whenModeration("Provider returns no verdict") + .replyWithoutModerationResult() + .build(); + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("direct:moderation") + .to("openai:moderation?apiKey=dummy&baseUrl=" + openAIMock.getBaseUrl() + "/v1"); + + from("direct:moderation-store-response") + .to("openai:moderation?apiKey=dummy&storeFullResponse=true&baseUrl=" + + openAIMock.getBaseUrl() + "/v1"); + + from("direct:moderation-explicit-model") + .to("openai:moderation?apiKey=dummy&moderationModel=omni-moderation-2024-09-26&baseUrl=" + + openAIMock.getBaseUrl() + "/v1"); + + from("direct:split-verdicts") + .to("openai:moderation?apiKey=dummy&baseUrl=" + openAIMock.getBaseUrl() + "/v1") + .split(header(OpenAIConstants.MODERATION_RESULTS)) + .choice() + .when(simple("${body[flagged]}")) + .to("mock:quarantine") + .otherwise() + .to("mock:downstream") + .end(); + + from("direct:score-threshold") + .to("openai:moderation?apiKey=dummy&baseUrl=" + openAIMock.getBaseUrl() + "/v1") + .choice() + .when(simple("${header.CamelOpenAIModerationCategoryScores[hate]} > 0.85")) + .to("mock:review") + .otherwise() + .to("mock:score-accepted") + .end(); + + from("direct:guard") + .to("openai:moderation?apiKey=dummy&baseUrl=" + openAIMock.getBaseUrl() + "/v1") + .choice() + .when(header(OpenAIConstants.MODERATION_FLAGGED).isEqualTo(true)) + .setBody(constant("Your message violates our usage policy.")) + .otherwise() + .setBody(constant("accepted")) + .end(); + } + }; + } + + @Test + void testAllowedInput() { + Exchange result = template.request("direct:moderation", + e -> e.getIn().setBody("Apache Camel is an integration framework")); + + // the body is passed through unchanged + assertThat(result.getMessage().getBody()).isEqualTo("Apache Camel is an integration framework"); + assertThat(result.getMessage().getHeader(OpenAIConstants.MODERATION_FLAGGED)).isEqualTo(false); + assertThat(result.getMessage().getHeader(OpenAIConstants.MODERATION_RESPONSE_MODEL)).isNotNull(); + + @SuppressWarnings("unchecked") + Map categories + = result.getMessage().getHeader(OpenAIConstants.MODERATION_CATEGORIES, Map.class); + assertThat(categories).containsEntry("hate", false).containsEntry("violence", false); + + @SuppressWarnings("unchecked") + Map scores + = result.getMessage().getHeader(OpenAIConstants.MODERATION_CATEGORY_SCORES, Map.class); + assertThat(scores).containsKeys("hate", "self-harm/intent", "violence/graphic"); + assertThat(scores.get("hate")).isEqualTo(0.0); + // the omni-moderation models do report the illicit categories + assertThat(categories).containsKeys("illicit", "illicit/violent"); + } + + @Test + void testFlaggedInput() { + Exchange result = template.request("direct:moderation", e -> e.getIn().setBody("I hate everyone")); + + assertThat(result.getMessage().getBody()).isEqualTo("I hate everyone"); + assertThat(result.getMessage().getHeader(OpenAIConstants.MODERATION_FLAGGED)).isEqualTo(true); + + @SuppressWarnings("unchecked") + Map categories + = result.getMessage().getHeader(OpenAIConstants.MODERATION_CATEGORIES, Map.class); + assertThat(categories).containsEntry("hate", true).containsEntry("violence", false); + + @SuppressWarnings("unchecked") + Map scores + = result.getMessage().getHeader(OpenAIConstants.MODERATION_CATEGORY_SCORES, Map.class); + assertThat(scores.get("hate")).isEqualTo(0.92); + } + + @Test + void testScoredButNotFlaggedInput() { + Exchange result = template.request("direct:moderation", e -> e.getIn().setBody("Another harmless sentence")); + + assertThat(result.getMessage().getHeader(OpenAIConstants.MODERATION_FLAGGED)).isEqualTo(false); + + @SuppressWarnings("unchecked") + Map categories + = result.getMessage().getHeader(OpenAIConstants.MODERATION_CATEGORIES, Map.class); + assertThat(categories).containsEntry("violence", false); + + @SuppressWarnings("unchecked") + Map scores + = result.getMessage().getHeader(OpenAIConstants.MODERATION_CATEGORY_SCORES, Map.class); + assertThat(scores.get("violence")).isEqualTo(0.01); + } + + @Test + void testBatchModeration() { + List inputs = List.of("Apache Camel is an integration framework", "I hate everyone"); + + Exchange result = template.request("direct:moderation", e -> e.getIn().setBody(inputs)); + + assertThat(result.getMessage().getBody()).isEqualTo(inputs); + // flagged is true when at least one input of the batch was flagged + assertThat(result.getMessage().getHeader(OpenAIConstants.MODERATION_FLAGGED)).isEqualTo(true); + + @SuppressWarnings("unchecked") + List> verdicts + = result.getMessage().getHeader(OpenAIConstants.MODERATION_RESULTS, List.class); + assertThat(verdicts).hasSize(2); + + assertThat(verdicts.get(0)) + .containsEntry(OpenAIConstants.MODERATION_RESULT_INPUT, "Apache Camel is an integration framework") + .containsEntry(OpenAIConstants.MODERATION_RESULT_FLAGGED, false); + assertThat(verdicts.get(1)) + .containsEntry(OpenAIConstants.MODERATION_RESULT_INPUT, "I hate everyone") + .containsEntry(OpenAIConstants.MODERATION_RESULT_FLAGGED, true); + + @SuppressWarnings("unchecked") + Map flaggedCategories + = (Map) verdicts.get(1).get(OpenAIConstants.MODERATION_RESULT_CATEGORIES); + assertThat(flaggedCategories).containsEntry("hate", true); + + @SuppressWarnings("unchecked") + Map flaggedScores + = (Map) verdicts.get(1).get(OpenAIConstants.MODERATION_RESULT_CATEGORY_SCORES); + assertThat(flaggedScores.get("hate")).isEqualTo(0.92); + + // the plain maps belong to the single-input shape only + assertThat(result.getMessage().getHeader(OpenAIConstants.MODERATION_CATEGORIES)).isNull(); + assertThat(result.getMessage().getHeader(OpenAIConstants.MODERATION_CATEGORY_SCORES)).isNull(); + } + + @Test + void testBatchVerdictsCanBeSplitAndRouted() throws Exception { + MockEndpoint quarantine = getMockEndpoint("mock:quarantine"); + MockEndpoint downstream = getMockEndpoint("mock:downstream"); + quarantine.expectedMessageCount(1); + downstream.expectedMessageCount(1); + + template.sendBody("direct:split-verdicts", + List.of("Apache Camel is an integration framework", "I hate everyone")); + + MockEndpoint.assertIsSatisfied(context); + assertThat(quarantine.getExchanges().get(0).getMessage().getBody(Map.class)) + .containsEntry(OpenAIConstants.MODERATION_RESULT_INPUT, "I hate everyone"); + assertThat(downstream.getExchanges().get(0).getMessage().getBody(Map.class)) + .containsEntry(OpenAIConstants.MODERATION_RESULT_INPUT, "Apache Camel is an integration framework"); + } + + @Test + void testCategoryScoreThresholdInSimpleExpression() throws Exception { + MockEndpoint review = getMockEndpoint("mock:review"); + MockEndpoint accepted = getMockEndpoint("mock:score-accepted"); + review.expectedMessageCount(1); + accepted.expectedMessageCount(1); + + // hate scores 0.92, above the 0.85 threshold + template.sendBody("direct:score-threshold", "I hate everyone"); + // hate scores 0.0 + template.sendBody("direct:score-threshold", "Apache Camel is an integration framework"); + + MockEndpoint.assertIsSatisfied(context); + } + + @Test + void testStoreFullResponse() { + Exchange result = template.request("direct:moderation-store-response", + e -> e.getIn().setBody("I hate everyone")); + + Object response = result.getProperty(OpenAIConstants.MODERATION_RESPONSE); + assertThat(response).isInstanceOf(ModerationCreateResponse.class); + + ModerationCreateResponse moderationResponse = (ModerationCreateResponse) response; + assertThat(moderationResponse.results()).hasSize(1); + // the stored response must stay usable, not just deserializable + assertThat(moderationResponse.results().get(0).flagged()).isTrue(); + assertThat(moderationResponse.results().get(0).categories().hate()).isTrue(); + // guards the mock against drifting from the payload the API actually returns + assertThat(moderationResponse.isValid()).isTrue(); + } + + @Test + void testModelFromEndpointOptionAndHeader() { + Exchange fromOption = template.request("direct:moderation-explicit-model", + e -> e.getIn().setBody("Apache Camel is an integration framework")); + assertThat(fromOption.getMessage().getHeader(OpenAIConstants.MODERATION_RESPONSE_MODEL)) + .isEqualTo("omni-moderation-2024-09-26"); + + // the header wins over the endpoint option + Exchange fromHeader = template.request("direct:moderation-explicit-model", e -> { + e.getIn().setBody("Apache Camel is an integration framework"); + e.getIn().setHeader(OpenAIConstants.MODERATION_MODEL, "omni-moderation-latest"); + }); + assertThat(fromHeader.getMessage().getHeader(OpenAIConstants.MODERATION_RESPONSE_MODEL)) + .isEqualTo("omni-moderation-latest"); + + // and the default applies when neither is set + Exchange fromDefault = template.request("direct:moderation", + e -> e.getIn().setBody("Apache Camel is an integration framework")); + assertThat(fromDefault.getMessage().getHeader(OpenAIConstants.MODERATION_RESPONSE_MODEL)) + .isEqualTo("omni-moderation-latest"); + } + + @Test + void testProviderWithoutIllicitCategories() { + Exchange result = template.request("direct:moderation", e -> e.getIn().setBody("Legacy model input")); + + assertThat(result.getException()).isNull(); + assertThat(result.getMessage().getHeader(OpenAIConstants.MODERATION_FLAGGED)).isEqualTo(false); + + @SuppressWarnings("unchecked") + Map categories + = result.getMessage().getHeader(OpenAIConstants.MODERATION_CATEGORIES, Map.class); + // illicit is optional in the API model, so a provider may omit it + assertThat(categories).doesNotContainKeys("illicit", "illicit/violent"); + assertThat(categories).containsEntry("hate", false); + + @SuppressWarnings("unchecked") + Map scores + = result.getMessage().getHeader(OpenAIConstants.MODERATION_CATEGORY_SCORES, Map.class); + assertThat(scores).containsKey("illicit"); + } + + @Test + void testNonStringBodyIsConverted() { + Exchange result = template.request("direct:moderation", e -> e.getIn().setBody(42)); + + assertThat(result.getException()).isNull(); + assertThat(result.getMessage().getHeader(OpenAIConstants.MODERATION_FLAGGED)).isEqualTo(false); + // the body is passed through untouched, not replaced by the converted input + assertThat(result.getMessage().getBody()).isEqualTo(42); + + @SuppressWarnings("unchecked") + Map categories + = result.getMessage().getHeader(OpenAIConstants.MODERATION_CATEGORIES, Map.class); + assertThat(categories).containsEntry("hate", false); + } + + @Test + void testSingleElementListKeepsTheBatchShape() { + Exchange result = template.request("direct:moderation", + e -> e.getIn().setBody(List.of("Apache Camel is an integration framework"))); + + assertThat(result.getException()).isNull(); + assertThat(result.getMessage().getHeader(OpenAIConstants.MODERATION_FLAGGED)).isEqualTo(false); + + // a List body is a batch even with one element, so batch processors need no special case + @SuppressWarnings("unchecked") + List> verdicts + = result.getMessage().getHeader(OpenAIConstants.MODERATION_RESULTS, List.class); + assertThat(verdicts).hasSize(1); + assertThat(verdicts.get(0)).containsEntry(OpenAIConstants.MODERATION_RESULT_FLAGGED, false); + assertThat(result.getMessage().getHeader(OpenAIConstants.MODERATION_CATEGORIES)).isNull(); + } + + @Test + void testStringBodyExposesPlainMaps() { + Exchange result = template.request("direct:moderation", + e -> e.getIn().setBody("Apache Camel is an integration framework")); + + assertThat(result.getMessage().getHeader(OpenAIConstants.MODERATION_CATEGORIES)).isInstanceOf(Map.class); + assertThat(result.getMessage().getHeader(OpenAIConstants.MODERATION_CATEGORY_SCORES)).isInstanceOf(Map.class); + + // the results header is always present, so a route can use one shape everywhere + @SuppressWarnings("unchecked") + List> verdicts + = result.getMessage().getHeader(OpenAIConstants.MODERATION_RESULTS, List.class); + assertThat(verdicts).hasSize(1); + } + + @Test + void testPartialBatchVerdictFailsClosed() { + List inputs = List.of("Apache Camel is an integration framework", "Provider returns no verdict"); + + Exchange result = template.request("direct:moderation", e -> e.getIn().setBody(inputs)); + + // one verdict short of the batch must fail as well, not just an empty response + assertThat(result.getException()) + .isInstanceOf(CamelExchangeException.class) + .hasMessageContaining("Moderation returned 1 result(s) for 2 input(s)"); + assertThat(result.getMessage().getHeader(OpenAIConstants.MODERATION_RESULTS)).isNull(); + } + + @Test + void testEmptyListFails() { + Exchange result = template.request("direct:moderation", e -> e.getIn().setBody(List.of())); + + assertThat(result.getException()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("No input text provided for moderation"); + } + + @Test + void testListWithNullElementFails() { + List inputs = new ArrayList<>(); + inputs.add("Apache Camel is an integration framework"); + inputs.add(null); + + Exchange result = template.request("direct:moderation", e -> e.getIn().setBody(inputs)); + + assertThat(result.getException()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("must not contain null elements"); + } + + @Test + void testMissingBodyFails() { + Exchange result = template.request("direct:moderation", e -> e.getIn().setBody(null)); + + assertThat(result.getException()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("No input text provided for moderation"); + } + + @Test + void testGuardRouteRejectsFlaggedInput() { + Exchange rejected = template.request("direct:guard", e -> e.getIn().setBody("I hate everyone")); + assertThat(rejected.getMessage().getBody(String.class)).isEqualTo("Your message violates our usage policy."); + + Exchange accepted = template.request("direct:guard", + e -> e.getIn().setBody("Apache Camel is an integration framework")); + assertThat(accepted.getMessage().getBody(String.class)).isEqualTo("accepted"); + } +} diff --git a/docs/components/modules/others/nav.adoc b/docs/components/modules/others/nav.adoc index 093949c595f5e..bbbfe1b6bf5b7 100644 --- a/docs/components/modules/others/nav.adoc +++ b/docs/components/modules/others/nav.adoc @@ -64,7 +64,7 @@ ** xref:oauth.adoc[Oauth] ** xref:observability-services.adoc[Observability Services] ** xref:openai-providers.adoc[OpenAI - Compatible Providers] -** xref:openai-operations.adoc[OpenAI - Embeddings and Audio Operations] +** xref:openai-operations.adoc[OpenAI - Embeddings, Moderation and Audio Operations] ** xref:openai-mcp.adoc[OpenAI - MCP Tool Calling] ** xref:openai-responses.adoc[OpenAI - Responses API Operation] ** xref:openapi-java.adoc[Openapi Java] diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java index 52460b2cf8071..2507a07a13b9c 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/StaticEndpointBuilders.java @@ -12599,11 +12599,11 @@ public static OnceEndpointBuilderFactory.OnceEndpointBuilder once(String compone * * Path parameter: operation (required) * The operation to perform: 'chat-completion', 'responses', 'embeddings', - * 'tool-execution', 'audio-transcription', 'audio-translation', or - * 'audio-speech' - * There are 7 enums and the value can be one of: chat-completion, + * 'tool-execution', 'audio-transcription', 'audio-translation', + * 'audio-speech', or 'moderation' + * There are 8 enums and the value can be one of: chat-completion, * responses, embeddings, tool-execution, audio-transcription, - * audio-translation, audio-speech + * audio-translation, audio-speech, moderation * * @param path operation * @return the dsl builder @@ -12624,11 +12624,11 @@ public static OpenAIEndpointBuilderFactory.OpenAIEndpointBuilder openai(String p * * Path parameter: operation (required) * The operation to perform: 'chat-completion', 'responses', 'embeddings', - * 'tool-execution', 'audio-transcription', 'audio-translation', or - * 'audio-speech' - * There are 7 enums and the value can be one of: chat-completion, + * 'tool-execution', 'audio-transcription', 'audio-translation', + * 'audio-speech', or 'moderation' + * There are 8 enums and the value can be one of: chat-completion, * responses, embeddings, tool-execution, audio-transcription, - * audio-translation, audio-speech + * audio-translation, audio-speech, moderation * * @param componentName to use a custom component name for the endpoint * instead of the default name 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 794ee7972c426..db4adc8e19b4a 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 @@ -969,6 +969,21 @@ default OpenAIEndpointBuilder model(String model) { doSetProperty("model", model); return this; } + /** + * The model to use for moderation. + * + * The option is a: java.lang.String type. + * + * Default: omni-moderation-latest + * Group: producer + * + * @param moderationModel the value to set + * @return the dsl builder + */ + default OpenAIEndpointBuilder moderationModel(String moderationModel) { + doSetProperty("moderationModel", moderationModel); + return this; + } /** * Fully qualified class name for structured output using response * format. @@ -1210,7 +1225,8 @@ default OpenAIEndpointBuilder speechVoice(String speechVoice) { /** * Store the full SDK response in non-streaming mode: chat-completion * uses exchange property 'CamelOpenAIResponse'; responses uses - * 'CamelOpenAIResponsesResponse'. + * 'CamelOpenAIResponsesResponse'; moderation uses + * 'CamelOpenAIModerationResponse'. * * The option is a: boolean type. * @@ -1227,7 +1243,8 @@ default OpenAIEndpointBuilder storeFullResponse(boolean storeFullResponse) { /** * Store the full SDK response in non-streaming mode: chat-completion * uses exchange property 'CamelOpenAIResponse'; responses uses - * 'CamelOpenAIResponsesResponse'. + * 'CamelOpenAIResponsesResponse'; moderation uses + * 'CamelOpenAIModerationResponse'. * * The option will be converted to a boolean type. * @@ -1746,10 +1763,10 @@ default OpenAIHeaderNameBuilder openai() { * Path parameter: operation (required) * The operation to perform: 'chat-completion', 'responses', * 'embeddings', 'tool-execution', 'audio-transcription', - * 'audio-translation', or 'audio-speech' - * There are 7 enums and the value can be one of: chat-completion, + * 'audio-translation', 'audio-speech', or 'moderation' + * There are 8 enums and the value can be one of: chat-completion, * responses, embeddings, tool-execution, audio-transcription, - * audio-translation, audio-speech + * audio-translation, audio-speech, moderation * * @param path operation * @return the dsl builder @@ -1771,10 +1788,10 @@ default OpenAIEndpointBuilder openai(String path) { * Path parameter: operation (required) * The operation to perform: 'chat-completion', 'responses', * 'embeddings', 'tool-execution', 'audio-transcription', - * 'audio-translation', or 'audio-speech' - * There are 7 enums and the value can be one of: chat-completion, + * 'audio-translation', 'audio-speech', or 'moderation' + * There are 8 enums and the value can be one of: chat-completion, * responses, embeddings, tool-execution, audio-transcription, - * audio-translation, audio-speech + * audio-translation, audio-speech, moderation * * @param componentName to use a custom component name for the endpoint * instead of the default name @@ -2159,6 +2176,19 @@ public String openAIResponse() { public String openAIResponsesResponse() { return "CamelOpenAIResponsesResponse"; } + /** + * The complete OpenAI moderation response object. + * + * The option is a: {@code + * com.openai.models.moderations.ModerationCreateResponse} type. + * + * Group: producer + * + * @return the name of the header {@code OpenAIModerationResponse}. + */ + public String openAIModerationResponse() { + return "CamelOpenAIModerationResponse"; + } /** * The model to use for embeddings. * @@ -2255,6 +2285,88 @@ public String openAISimilarityScore() { public String openAIOriginalText() { return "CamelOpenAIOriginalText"; } + /** + * The model to use for moderation (e.g., omni-moderation-latest). + * + * The option is a: {@code String} type. + * + * Group: producer + * + * @return the name of the header {@code OpenAIModerationModel}. + */ + public String openAIModerationModel() { + return "CamelOpenAIModerationModel"; + } + /** + * Whether the moderation API flagged the input as violating the usage + * policies. For a batch of inputs this is true when at least one input + * was flagged. + * + * The option is a: {@code Boolean} type. + * + * Group: producer + * + * @return the name of the header {@code OpenAIModerationFlagged}. + */ + public String openAIModerationFlagged() { + return "CamelOpenAIModerationFlagged"; + } + /** + * One verdict per moderated input, in the order of the inputs. Each + * entry holds the keys 'input', 'flagged', 'categories' and + * 'categoryScores', so a batch can be split and routed per item. + * + * The option is a: {@code java.util.List>} type. + * + * Group: producer + * + * @return the name of the header {@code OpenAIModerationResults}. + */ + public String openAIModerationResults() { + return "CamelOpenAIModerationResults"; + } + /** + * The moderation categories and whether each one was violated, for a + * single input. Not set for a list body, where + * 'CamelOpenAIModerationResults' carries the verdicts. + * + * The option is a: {@code java.util.Map} type. + * + * Group: producer + * + * @return the name of the header {@code OpenAIModerationCategories}. + */ + public String openAIModerationCategories() { + return "CamelOpenAIModerationCategories"; + } + /** + * The moderation confidence score per category, for a single input. Not + * set for a list body, where 'CamelOpenAIModerationResults' carries the + * verdicts. + * + * The option is a: {@code java.util.Map} type. + * + * Group: producer + * + * @return the name of the header {@code + * OpenAIModerationCategoryScores}. + */ + public String openAIModerationCategoryScores() { + return "CamelOpenAIModerationCategoryScores"; + } + /** + * The moderation model used in the response. + * + * The option is a: {@code String} type. + * + * Group: producer + * + * @return the name of the header {@code OpenAIModerationResponseModel}. + */ + public String openAIModerationResponseModel() { + return "CamelOpenAIModerationResponseModel"; + } /** * The model to use for audio transcription. * diff --git a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/ModerationExpectation.java b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/ModerationExpectation.java new file mode 100644 index 0000000000000..39a5a96116547 --- /dev/null +++ b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/ModerationExpectation.java @@ -0,0 +1,120 @@ +/* + * 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.test.infra.openai.mock; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Represents a mock expectation for a moderation request. Contains the expected input text and the moderation verdict + * to reply with. + */ +public class ModerationExpectation { + private static final String DEFAULT_MODEL = "camel-moderation"; + + private final String expectedInput; + private final Map flaggedCategories = new LinkedHashMap<>(); + private final Map categoryScores = new LinkedHashMap<>(); + private boolean flagged; + private boolean illicitCategoriesIncluded = true; + private boolean resultOmitted; + private String model = DEFAULT_MODEL; + + public ModerationExpectation(String expectedInput) { + this.expectedInput = expectedInput; + } + + public String getExpectedInput() { + return expectedInput; + } + + public boolean isFlagged() { + return flagged; + } + + public void setFlagged(boolean flagged) { + this.flagged = flagged; + } + + public Map getFlaggedCategories() { + return flaggedCategories; + } + + public Map getCategoryScores() { + return categoryScores; + } + + /** + * Marks a category as violated, which also flags the whole result. + * + * @param category the OpenAI category name, for example {@code hate} or {@code self-harm/intent} + * @param score the confidence score reported for that category + */ + public void flagCategory(String category, double score) { + flaggedCategories.put(category, true); + categoryScores.put(category, score); + flagged = true; + } + + /** + * Reports a score for a category without marking it as violated. + */ + public void scoreCategory(String category, double score) { + categoryScores.put(category, score); + } + + public boolean isResultOmitted() { + return resultOmitted; + } + + /** + * Replies without a result for this input, reproducing a provider that returns fewer verdicts than inputs. + */ + public void setResultOmitted(boolean resultOmitted) { + this.resultOmitted = resultOmitted; + } + + public boolean isIllicitCategoriesIncluded() { + return illicitCategoriesIncluded; + } + + /** + * Controls whether the {@code illicit} and {@code illicit/violent} categories are reported. They are optional in + * the API model, so this allows reproducing an OpenAI-compatible provider that does not return them. + */ + public void setIllicitCategoriesIncluded(boolean illicitCategoriesIncluded) { + this.illicitCategoriesIncluded = illicitCategoriesIncluded; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public boolean matches(String input) { + return expectedInput.equals(input); + } + + @Override + public String toString() { + return String.format("ModerationExpectation{input='%s', flagged=%b, categories=%s, model='%s'}", + expectedInput, flagged, flaggedCategories.keySet(), model); + } +} diff --git a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/ModerationRequestHandler.java b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/ModerationRequestHandler.java new file mode 100644 index 0000000000000..523c614f3d457 --- /dev/null +++ b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/ModerationRequestHandler.java @@ -0,0 +1,118 @@ +/* + * 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.test.infra.openai.mock; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sun.net.httpserver.HttpExchange; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handles incoming moderation requests and matches them to appropriate mock expectations. + */ +public class ModerationRequestHandler { + private static final Logger LOG = LoggerFactory.getLogger(ModerationRequestHandler.class); + + private final List expectations; + private final ModerationResponseBuilder responseBuilder; + private final ObjectMapper objectMapper; + + public ModerationRequestHandler(List expectations, ObjectMapper objectMapper) { + this.expectations = expectations; + this.objectMapper = objectMapper; + this.responseBuilder = new ModerationResponseBuilder(objectMapper); + } + + public String handleRequest(HttpExchange exchange) throws IOException { + try { + String requestBody; + try (InputStream is = exchange.getRequestBody()) { + requestBody = new String(is.readAllBytes(), StandardCharsets.UTF_8); + } + LOG.debug("Processing moderation request: {}", requestBody); + + JsonNode rootNode = objectMapper.readTree(requestBody); + List inputs = extractInputs(rootNode); + String requestedModel = extractModel(rootNode); + + List matchedExpectations = new ArrayList<>(inputs.size()); + for (String input : inputs) { + matchedExpectations.add(findExpectationByInput(input)); + } + + return responseBuilder.createModerationResponse(matchedExpectations, requestedModel); + } catch (Exception e) { + String errorMessage = "Error processing moderation request: " + e.getMessage(); + LOG.error(errorMessage, e); + return createErrorResponse(500, errorMessage, exchange); + } + } + + /** + * The moderation API echoes back the model that served the request, so the mock does the same. + */ + private String extractModel(JsonNode rootNode) { + JsonNode modelNode = rootNode.get("model"); + return modelNode == null ? null : modelNode.asText(); + } + + private List extractInputs(JsonNode rootNode) { + JsonNode inputNode = rootNode.get("input"); + if (inputNode == null) { + throw new IllegalArgumentException("Missing 'input' field in moderation request"); + } + + List inputs = new ArrayList<>(); + if (inputNode.isArray()) { + if (inputNode.isEmpty()) { + throw new IllegalArgumentException("Empty 'input' array in moderation request"); + } + for (JsonNode node : inputNode) { + inputs.add(node.asText()); + } + } else { + inputs.add(inputNode.asText()); + } + return inputs; + } + + private ModerationExpectation findExpectationByInput(String input) { + return expectations.stream() + .filter(expectation -> expectation.matches(input)) + .findFirst() + .orElseThrow(() -> new IllegalStateException( + String.format("No matching moderation expectation found for input: %s", input))); + } + + private String createErrorResponse(int statusCode, String errorMessage, HttpExchange exchange) { + String jsonErrorMessage = String.format("{\"error\": {\"message\": \"%s\", \"type\": \"invalid_request_error\"}}", + errorMessage); + try { + exchange.sendResponseHeaders(statusCode, jsonErrorMessage.length()); + } catch (IOException e) { + throw new RuntimeException(e); + } + return jsonErrorMessage; + } +} diff --git a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/ModerationResponseBuilder.java b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/ModerationResponseBuilder.java new file mode 100644 index 0000000000000..37b225570d80e --- /dev/null +++ b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/ModerationResponseBuilder.java @@ -0,0 +1,119 @@ +/* + * 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.test.infra.openai.mock; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Builder class for creating OpenAI moderation API mock responses. + */ +public class ModerationResponseBuilder { + + /** + * The categories reported by the OpenAI moderation API. Every category is always present in a real response, so the + * mock emits all of them and lets the expectation decide which ones are violated. + */ + private static final List CATEGORIES = List.of( + "harassment", "harassment/threatening", + "hate", "hate/threatening", + "illicit", "illicit/violent", + "self-harm", "self-harm/instructions", "self-harm/intent", + "sexual", "sexual/minors", + "violence", "violence/graphic"); + + /** + * The categories that only the {@code omni-moderation-*} models report. + */ + private static final List ILLICIT_CATEGORIES = List.of("illicit", "illicit/violent"); + + private final ObjectMapper objectMapper; + + public ModerationResponseBuilder(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + public String createModerationResponse(List expectations, String requestedModel) + throws Exception { + String model = requestedModel; + if (model == null) { + model = expectations.isEmpty() ? "camel-moderation" : expectations.get(0).getModel(); + } + + List results = new ArrayList<>(expectations.size()); + for (ModerationExpectation expectation : expectations) { + if (expectation.isResultOmitted()) { + continue; + } + results.add(new ModerationResult( + expectation.isFlagged(), + categories(expectation), + categoryScores(expectation), + categoryAppliedInputTypes(expectation))); + } + + return objectMapper.writeValueAsString(new ModerationResponse("modr-camel", model, results)); + } + + private Map categories(ModerationExpectation expectation) { + Map categories = new LinkedHashMap<>(); + for (String category : CATEGORIES) { + if (!expectation.isIllicitCategoriesIncluded() && ILLICIT_CATEGORIES.contains(category)) { + continue; + } + categories.put(category, expectation.getFlaggedCategories().getOrDefault(category, false)); + } + return categories; + } + + private Map categoryScores(ModerationExpectation expectation) { + Map scores = new LinkedHashMap<>(); + for (String category : CATEGORIES) { + scores.put(category, expectation.getCategoryScores().getOrDefault(category, 0.0)); + } + return scores; + } + + /** + * The real API reports which input modality triggered each category. The mock only ever moderates text. + */ + private Map> categoryAppliedInputTypes(ModerationExpectation expectation) { + Map> appliedInputTypes = new LinkedHashMap<>(); + for (String category : categories(expectation).keySet()) { + appliedInputTypes.put(category, List.of("text")); + } + return appliedInputTypes; + } + + private record ModerationResponse( + @JsonProperty("id") String id, + @JsonProperty("model") String model, + @JsonProperty("results") List results) { + } + + private record ModerationResult( + @JsonProperty("flagged") boolean flagged, + @JsonProperty("categories") Map categories, + @JsonProperty("category_scores") Map categoryScores, + @JsonProperty("category_applied_input_types") Map> categoryAppliedInputTypes) { + } +} diff --git a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/OpenAIMock.java b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/OpenAIMock.java index 7cabdf8d055e5..75b67fbf0f746 100644 --- a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/OpenAIMock.java +++ b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/OpenAIMock.java @@ -43,6 +43,7 @@ public class OpenAIMock implements BeforeEachCallback, AfterEachCallback { private final List audioTranscriptionExpectations; private final List audioTranslationExpectations; private final List speechExpectations; + private final List moderationExpectations; private final OpenAIMockBuilder builder; private final ObjectMapper objectMapper; private ExecutorService executor; @@ -53,11 +54,12 @@ public OpenAIMock() { this.audioTranscriptionExpectations = new ArrayList<>(); this.audioTranslationExpectations = new ArrayList<>(); this.speechExpectations = new ArrayList<>(); + this.moderationExpectations = new ArrayList<>(); this.objectMapper = new ObjectMapper(); this.builder = new OpenAIMockBuilder( this, this.expectations, this.embeddingExpectations, this.audioTranscriptionExpectations, this.audioTranslationExpectations, - this.speechExpectations); + this.speechExpectations, this.moderationExpectations); } public OpenAIMockBuilder builder() { @@ -78,7 +80,7 @@ public void beforeEach(ExtensionContext context) throws Exception { new OpenAIMockServerHandler( new OpenAIMockExpectations( expectations, embeddingExpectations, audioTranscriptionExpectations, - audioTranslationExpectations, speechExpectations), + audioTranslationExpectations, speechExpectations, moderationExpectations), objectMapper)); executor = Executors.newSingleThreadExecutor(); diff --git a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/OpenAIMockBuilder.java b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/OpenAIMockBuilder.java index 471d380db3ee4..a204aae7e49de 100644 --- a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/OpenAIMockBuilder.java +++ b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/OpenAIMockBuilder.java @@ -37,23 +37,27 @@ public class OpenAIMockBuilder { private final List audioTranscriptionExpectations; private final List audioTranslationExpectations; private final List speechExpectations; + private final List moderationExpectations; private MockExpectation currentExpectation; private EmbeddingExpectation currentEmbeddingExpectation; private AudioTranscriptionExpectation currentAudioTranscriptionExpectation; private AudioTranscriptionExpectation currentAudioTranslationExpectation; private SpeechExpectation currentSpeechExpectation; + private ModerationExpectation currentModerationExpectation; public OpenAIMockBuilder(OpenAIMock mock, List expectations, List embeddingExpectations, List audioTranscriptionExpectations, List audioTranslationExpectations, - List speechExpectations) { + List speechExpectations, + List moderationExpectations) { this.mock = mock; this.expectations = expectations; this.embeddingExpectations = embeddingExpectations; this.audioTranscriptionExpectations = audioTranscriptionExpectations; this.audioTranslationExpectations = audioTranslationExpectations; this.speechExpectations = speechExpectations; + this.moderationExpectations = moderationExpectations; } public OpenAIMockBuilder when(String expectedInput) { @@ -187,6 +191,68 @@ public OpenAIMockBuilder replyWithEmbedding(int size) { return this; } + // Moderation API methods + + public OpenAIMockBuilder whenModeration(String expectedInput) { + log.debug("Setting up moderation expectation for input: {}", expectedInput); + currentModerationExpectation = new ModerationExpectation(expectedInput); + return this; + } + + /** + * Replies with a verdict that violates no category. + */ + public OpenAIMockBuilder replyWithModerationAllowed() { + validateCurrentModerationExpectation("replyWithModerationAllowed()"); + log.debug("Setting moderation verdict: allowed"); + currentModerationExpectation.setFlagged(false); + return this; + } + + /** + * Replies with a verdict that violates the given category, which flags the whole result. + * + * @param category the OpenAI category name, for example {@code hate} or {@code self-harm/intent} + * @param score the confidence score reported for that category + */ + public OpenAIMockBuilder replyWithModerationFlagged(String category, double score) { + validateCurrentModerationExpectation("replyWithModerationFlagged()"); + log.debug("Setting moderation verdict: flagged for category {} with score {}", category, score); + currentModerationExpectation.flagCategory(category, score); + return this; + } + + /** + * Reports a score for a category without marking it as violated. + */ + public OpenAIMockBuilder replyWithModerationScore(String category, double score) { + validateCurrentModerationExpectation("replyWithModerationScore()"); + log.debug("Setting moderation score for category {}: {}", category, score); + currentModerationExpectation.scoreCategory(category, score); + return this; + } + + /** + * Replies without the {@code illicit} and {@code illicit/violent} categories, as an OpenAI-compatible provider that + * does not implement them would. + */ + public OpenAIMockBuilder replyWithoutIllicitCategories() { + validateCurrentModerationExpectation("replyWithoutIllicitCategories()"); + log.debug("Omitting the illicit categories from the moderation verdict"); + currentModerationExpectation.setIllicitCategoriesIncluded(false); + return this; + } + + /** + * Replies without a result for this input, reproducing a provider that returns fewer verdicts than inputs. + */ + public OpenAIMockBuilder replyWithoutModerationResult() { + validateCurrentModerationExpectation("replyWithoutModerationResult()"); + log.debug("Omitting the moderation result for this input"); + currentModerationExpectation.setResultOmitted(true); + return this; + } + // Audio Transcription API methods public OpenAIMockBuilder whenTranscription() { @@ -274,9 +340,14 @@ public OpenAIMockBuilder end() { log.debug("Finalizing audio speech expectation"); speechExpectations.add(currentSpeechExpectation); currentSpeechExpectation = null; + } else if (currentModerationExpectation != null) { + log.debug("Finalizing moderation expectation for input: {}", currentModerationExpectation.getExpectedInput()); + moderationExpectations.add(currentModerationExpectation); + currentModerationExpectation = null; } else { throw new IllegalStateException( - "Call when(), whenEmbedding(), whenTranscription(), whenTranslation(), or whenSpeech() before end()"); + "Call when(), whenEmbedding(), whenTranscription(), whenTranslation(), whenSpeech(), " + + "or whenModeration() before end()"); } return this; } @@ -307,10 +378,15 @@ public OpenAIMock build() { speechExpectations.add(currentSpeechExpectation); currentSpeechExpectation = null; } + if (currentModerationExpectation != null) { + log.debug("Auto-finalizing current moderation expectation during build"); + moderationExpectations.add(currentModerationExpectation); + currentModerationExpectation = null; + } log.info("Built OpenAIMock with {} chat, {} embedding, {} transcription, {} translation, " - + "and {} speech expectations", + + "{} speech, and {} moderation expectations", expectations.size(), embeddingExpectations.size(), audioTranscriptionExpectations.size(), - audioTranslationExpectations.size(), speechExpectations.size()); + audioTranslationExpectations.size(), speechExpectations.size(), moderationExpectations.size()); return mock; } @@ -344,6 +420,12 @@ private void validateCurrentSpeechExpectation(String methodName) { } } + private void validateCurrentModerationExpectation(String methodName) { + if (currentModerationExpectation == null) { + throw new IllegalStateException("Call whenModeration() before " + methodName); + } + } + private AudioTranscriptionExpectation activeAudioExpectation(String methodName) { if (currentAudioTranscriptionExpectation != null) { return currentAudioTranscriptionExpectation; diff --git a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/OpenAIMockExpectations.java b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/OpenAIMockExpectations.java index aeb5b6af015a6..ec2b7f036bfde 100644 --- a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/OpenAIMockExpectations.java +++ b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/OpenAIMockExpectations.java @@ -27,12 +27,13 @@ public record OpenAIMockExpectations( List embeddings, List transcriptions, List translations, - List speeches) { + List speeches, + List moderations) { /** * Convenience factory for callers that only need chat completion expectations (e.g. SSL/TLS tests). */ public static OpenAIMockExpectations ofChat(List chat) { - return new OpenAIMockExpectations(chat, List.of(), List.of(), List.of(), List.of()); + return new OpenAIMockExpectations(chat, List.of(), List.of(), List.of(), List.of(), List.of()); } } diff --git a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/OpenAIMockServerHandler.java b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/OpenAIMockServerHandler.java index 89b5c64c8c419..1fb544a8da131 100644 --- a/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/OpenAIMockServerHandler.java +++ b/test-infra/camel-test-infra-openai-mock/src/main/java/org/apache/camel/test/infra/openai/mock/OpenAIMockServerHandler.java @@ -33,6 +33,7 @@ public class OpenAIMockServerHandler implements HttpHandler { private final AudioTranscriptionRequestHandler audioTranscriptionRequestHandler; private final AudioTranscriptionRequestHandler audioTranslationRequestHandler; private final SpeechRequestHandler speechRequestHandler; + private final ModerationRequestHandler moderationRequestHandler; public OpenAIMockServerHandler(OpenAIMockExpectations expectations, ObjectMapper objectMapper) { this.chatRequestHandler = new RequestHandler(expectations.chat(), objectMapper); @@ -43,6 +44,7 @@ public OpenAIMockServerHandler(OpenAIMockExpectations expectations, ObjectMapper this.audioTranslationRequestHandler = new AudioTranscriptionRequestHandler(expectations.translations(), objectMapper); this.speechRequestHandler = new SpeechRequestHandler(expectations.speeches(), objectMapper); + this.moderationRequestHandler = new ModerationRequestHandler(expectations.moderations(), objectMapper); } @Override @@ -64,6 +66,8 @@ public void handle(HttpExchange exchange) throws IOException { response = audioTranslationRequestHandler.handleRequest(exchange); } else if (path.endsWith("/embeddings")) { response = embeddingRequestHandler.handleRequest(exchange); + } else if (path.endsWith("/moderations")) { + response = moderationRequestHandler.handleRequest(exchange); } else if (path.endsWith("/responses")) { response = responsesRequestHandler.handleRequest(exchange); } else {