This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 360734d228c7 CAMEL-24672: camel-openai - moderate images with 
moderation operation
360734d228c7 is described below

commit 360734d228c7fb952e09403cf9368c78ac840a67
Author: Karol Krawczyk <[email protected]>
AuthorDate: Sun Sep 13 22:37:15 2026 +0200

    CAMEL-24672: camel-openai - moderate images with moderation operation
    
    The moderation operation now accepts an image body. A File, Path,
    WrappedFile, byte[] or InputStream body with an image MIME type is sent
    to the multi-modal input of the moderation API as an image_url data URL,
    using the same MimeTypeHelper detection as the chat-completion vision
    input. A binary body without an image type is still moderated as text.
    
    The new CamelOpenAIModerationText header carries text posted with the
    image, such as a caption; text and image share one verdict. Each verdict
    also reports categoryAppliedInputTypes when the provider returns it. The
    image body is passed through unchanged: a stream-cached body is reset
    and a plain InputStream is replaced by its bytes. A list body containing
    an image file now fails with an IllegalArgumentException pointing at
    Split, since the API accepts one image per request.
    
    The OpenAI mock accepts multi-modal moderation input, and answers a
    failed request assertion with an error instead of leaving the client
    waiting for its read timeout.
    
    Closes #26360
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
 .../apache/camel/catalog/components/openai.json    |  75 ++++----
 .../camel/catalog/docs/openai-component.adoc       |   4 +-
 .../camel/catalog/docs/openai-operations.adoc      |  81 +++++++-
 .../org/apache/camel/component/openai/openai.json  |  75 ++++----
 .../src/main/docs/openai-component.adoc            |   4 +-
 .../src/main/docs/openai-operations.adoc           |  81 +++++++-
 .../camel/component/openai/MimeTypeHelper.java     |   2 +-
 .../camel/component/openai/OpenAIConstants.java    |  10 +-
 .../camel/component/openai/OpenAIImageSupport.java |   2 +-
 .../component/openai/OpenAIModerationProducer.java | 141 +++++++++++++-
 .../openai/OpenAIModerationImageMockTest.java      | 210 +++++++++++++++++++++
 .../OpenAIModerationExternalServiceIT.java         |  47 +++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |   7 +
 .../endpoint/dsl/OpenAIEndpointBuilderFactory.java |  20 +-
 .../infra/openai/mock/ModerationExpectation.java   |  42 ++++-
 .../openai/mock/ModerationRequestHandler.java      |  58 +++++-
 .../openai/mock/ModerationResponseBuilder.java     |  22 ++-
 .../test/infra/openai/mock/OpenAIMockBuilder.java  |  28 +++
 18 files changed, 786 insertions(+), 123 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/openai.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/openai.json
index 4176ef697a0f..ef7be91073c6 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
@@ -77,43 +77,44 @@
     "CamelOpenAISimilarityScore": { "index": 40, "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": 41, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String or 
List<String>", "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": 42, "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": 43, "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_FL [...]
-    "CamelOpenAIModerationResults": { "index": 44, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "java.util.List<java.util.Map<String, Object>>", "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", "cons [...]
-    "CamelOpenAIModerationCategories": { "index": 45, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "java.util.Map<String, Boolean>", "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.compo [...]
-    "CamelOpenAIModerationCategoryScores": { "index": 46, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "java.util.Map<String, Double>", "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. [...]
-    "CamelOpenAIModerationResponseModel": { "index": 47, "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": 48, "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": 49, "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": 50, "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": 51, "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": 52, "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": 53, "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": 54, "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": 55, "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": 56, "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": 57, "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": 58, "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": 59, "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": 60, "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" },
-    "CamelOpenAIImageModel": { "index": 61, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The model to use for image generation or editing (e.g., 
gpt-image-1, dall-e-3, dall-e-2)", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_MODEL" },
-    "CamelOpenAIImagePrompt": { "index": 62, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The prompt describing the image to generate, or the edit 
to apply. Takes precedence over the imagePrompt endpoint option and, for 
image-generation, over the message body", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants [...]
-    "CamelOpenAIImageSize": { "index": 63, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The size of the generated image (e.g., 1024x1024, 
1536x1024, auto)", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_SIZE" },
-    "CamelOpenAIImageQuality": { "index": 64, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The quality of the generated image (auto, high, medium, 
low for GPT image models; hd, standard for dall-e-3; standard for dall-e-2)", 
"constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_QUALITY" },
-    "CamelOpenAIImageResponseFormat": { "index": 65, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The response format of the generated 
image (url or b64_json). Only supported by dall-e-2 and dall-e-3; GPT image 
models always return base64", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_RESPONSE_FORMAT" },
-    "CamelOpenAIImageCount": { "index": 66, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "Integer", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The number of images to generate", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_COUNT" },
-    "CamelOpenAIImageBackground": { "index": 67, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The background of the generated image 
(transparent, opaque, auto). Only supported by GPT image models", 
"constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_BACKGROUND" },
-    "CamelOpenAIImageOutputFormat": { "index": 68, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The output format of the generated 
image (png, jpeg, webp). Only supported by GPT image models", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_OUTPUT_FORMAT" },
-    "CamelOpenAIImageOutputCompression": { "index": 69, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The compression level (0-100) for the 
webp or jpeg output formats. Only supported by GPT image models", 
"constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_OUTPUT_COMPRESSION" },
-    "CamelOpenAIImageStyle": { "index": 70, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The style of the generated image (vivid or natural). 
Only supported by dall-e-3", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_STYLE" },
-    "CamelOpenAIImageModeration": { "index": 71, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The content moderation level for image 
generation (low or auto). Only supported by GPT image models", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_MODERATION" },
-    "CamelOpenAIImageInputFidelity": { "index": 72, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "How closely the edit must match the 
style and features of the input image (high or low). Only supported by the 
image-edit operation on gpt-image-1 and gpt-image-1.5", "constantName": 
"org.apache.camel.component.openai.OpenAIConstan [...]
-    "CamelOpenAIImageMask": { "index": 73, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "byte[], 
java.io.File, java.nio.file.Path or java.io.InputStream", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": "An 
optional PNG mask for the image-edit operation, where the fully transparent 
areas indicate where the image should be edited", "constantName": 
"org.apache.camel.component.openai.Ope [...]
-    "CamelOpenAIImageResultCount": { "index": 74, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The number of images returned in the 
response", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_RESULT_COUNT" },
-    "CamelOpenAIImageRevisedPrompt": { "index": 75, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The prompt as revised by the model, 
when a single image is returned (dall-e-3)", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_REVISED_PROMPT" },
-    "CamelOpenAIImageRevisedPrompts": { "index": 76, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "java.util.List<String>", "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "The prompts as revised 
by the model, one entry per returned image (dall-e-3)", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_REVISED_PROMPTS" },
-    "CamelOpenAIImageInputTokens": { "index": 77, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The number of input tokens billed for 
the image request. Only reported by GPT image models", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_INPUT_TOKENS" },
-    "CamelOpenAIImageOutputTokens": { "index": 78, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The number of output tokens billed for 
the image request. Only reported by GPT image models", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_OUTPUT_TOKENS" },
-    "CamelOpenAIImageTotalTokens": { "index": 79, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The total number of tokens billed for 
the image request. Only reported by GPT image models", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_TOTAL_TOKENS" }
+    "CamelOpenAIModerationText": { "index": 43, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "Text to moderate together with an image 
body, such as the caption the image was posted with. The text and the image are 
scored as one input and share a single verdict. Ignored when the body is not an 
image", "constantName": "org.apache [...]
+    "CamelOpenAIModerationFlagged": { "index": 44, "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_FL [...]
+    "CamelOpenAIModerationResults": { "index": 45, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "java.util.List<java.util.Map<String, Object>>", "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', 'categoryScores' 
and, when the provider reports it, 'categoryAppliedInpu [...]
+    "CamelOpenAIModerationCategories": { "index": 46, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "java.util.Map<String, Boolean>", "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.compo [...]
+    "CamelOpenAIModerationCategoryScores": { "index": 47, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "java.util.Map<String, Double>", "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. [...]
+    "CamelOpenAIModerationResponseModel": { "index": 48, "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": 49, "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": 50, "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": 51, "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": 52, "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": 53, "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": 54, "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": 55, "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": 56, "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": 57, "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": 58, "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": 59, "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": 60, "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": 61, "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" },
+    "CamelOpenAIImageModel": { "index": 62, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The model to use for image generation or editing (e.g., 
gpt-image-1, dall-e-3, dall-e-2)", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_MODEL" },
+    "CamelOpenAIImagePrompt": { "index": 63, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The prompt describing the image to generate, or the edit 
to apply. Takes precedence over the imagePrompt endpoint option and, for 
image-generation, over the message body", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants [...]
+    "CamelOpenAIImageSize": { "index": 64, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The size of the generated image (e.g., 1024x1024, 
1536x1024, auto)", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_SIZE" },
+    "CamelOpenAIImageQuality": { "index": 65, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The quality of the generated image (auto, high, medium, 
low for GPT image models; hd, standard for dall-e-3; standard for dall-e-2)", 
"constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_QUALITY" },
+    "CamelOpenAIImageResponseFormat": { "index": 66, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The response format of the generated 
image (url or b64_json). Only supported by dall-e-2 and dall-e-3; GPT image 
models always return base64", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_RESPONSE_FORMAT" },
+    "CamelOpenAIImageCount": { "index": 67, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "Integer", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The number of images to generate", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_COUNT" },
+    "CamelOpenAIImageBackground": { "index": 68, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The background of the generated image 
(transparent, opaque, auto). Only supported by GPT image models", 
"constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_BACKGROUND" },
+    "CamelOpenAIImageOutputFormat": { "index": 69, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The output format of the generated 
image (png, jpeg, webp). Only supported by GPT image models", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_OUTPUT_FORMAT" },
+    "CamelOpenAIImageOutputCompression": { "index": 70, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The compression level (0-100) for the 
webp or jpeg output formats. Only supported by GPT image models", 
"constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_OUTPUT_COMPRESSION" },
+    "CamelOpenAIImageStyle": { "index": 71, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The style of the generated image (vivid or natural). 
Only supported by dall-e-3", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_STYLE" },
+    "CamelOpenAIImageModeration": { "index": 72, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The content moderation level for image 
generation (low or auto). Only supported by GPT image models", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_MODERATION" },
+    "CamelOpenAIImageInputFidelity": { "index": 73, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "How closely the edit must match the 
style and features of the input image (high or low). Only supported by the 
image-edit operation on gpt-image-1 and gpt-image-1.5", "constantName": 
"org.apache.camel.component.openai.OpenAIConstan [...]
+    "CamelOpenAIImageMask": { "index": 74, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "byte[], 
java.io.File, java.nio.file.Path or java.io.InputStream", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": "An 
optional PNG mask for the image-edit operation, where the fully transparent 
areas indicate where the image should be edited", "constantName": 
"org.apache.camel.component.openai.Ope [...]
+    "CamelOpenAIImageResultCount": { "index": 75, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The number of images returned in the 
response", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_RESULT_COUNT" },
+    "CamelOpenAIImageRevisedPrompt": { "index": 76, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The prompt as revised by the model, 
when a single image is returned (dall-e-3)", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_REVISED_PROMPT" },
+    "CamelOpenAIImageRevisedPrompts": { "index": 77, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "java.util.List<String>", "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "The prompts as revised 
by the model, one entry per returned image (dall-e-3)", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_REVISED_PROMPTS" },
+    "CamelOpenAIImageInputTokens": { "index": 78, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The number of input tokens billed for 
the image request. Only reported by GPT image models", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_INPUT_TOKENS" },
+    "CamelOpenAIImageOutputTokens": { "index": 79, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The number of output tokens billed for 
the image request. Only reported by GPT image models", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_OUTPUT_TOKENS" },
+    "CamelOpenAIImageTotalTokens": { "index": 80, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The total number of tokens billed for 
the image request. Only reported by GPT image models", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_TOTAL_TOKENS" }
   },
   "properties": {
     "operation": { "index": 0, "kind": "path", "displayName": "Operation", 
"group": "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", 
"image-generation", "image-edit" ], "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Th [...]
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 90a32cae5157..dad735f8f202 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
@@ -47,7 +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
+* `moderation` - Check text or an image against the OpenAI usage policies 
before it reaches a model
 * `image-generation` - Generate images from a text prompt (e.g., gpt-image-1, 
gpt-image-1-mini)
 * `image-edit` - Edit an existing image, optionally through a mask, from a 
text prompt
 
@@ -1297,7 +1297,7 @@ The component may throw the following exceptions:
   ** 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)
+  ** When the moderation input list is empty, contains null elements, or 
contains an image file (moderation)
   ** When the image model is missing (image-generation, image-edit)
   ** When the prompt is missing (image-generation, image-edit) or the image 
list is empty (image-edit)
   ** When an unsupported image or mask body type is provided (image-edit)
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 7188c55c03d0..7989b9a40716 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
@@ -172,7 +172,7 @@ Set `storeFullResponse=true` to keep the complete SDK 
response in the `CamelOpen
 
 == Moderation Operation
 
-The `moderation` operation checks text against the OpenAI usage policies. It 
is the canonical pre-filter for untrusted
+The `moderation` operation checks text, or an image, 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
@@ -183,9 +183,6 @@ defined by the provider, so it is not a substitute for 
authentication, authoriza
 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]
@@ -224,11 +221,78 @@ YAML::
 ----
 ====
 
+=== Moderating an Image
+
+An image body is sent to the multi-modal input of the moderation API instead 
of being converted to text. The body is
+treated as an image when its MIME type is an image type, detected the same way 
as for the vision input of
+`chat-completion`: a `File`, `Path` or `WrappedFile` body from its file name 
or content-type header, and a `byte[]` or
+`InputStream` body, as produced by cloud storage components, from its 
content-type header. Set the
+`CamelOpenAIMediaType` header when the body carries no content-type metadata. 
A `byte[]` or `InputStream` body without
+an image type is still moderated as text.
+
+Text posted together with the image, such as its caption, goes in the 
`CamelOpenAIModerationText` header. The API
+scores the text and the image as one input, so they share a single verdict, 
whose `input` key holds that text, or
+`null` for an image moderated on its own. The header is ignored when the body 
is not an image.
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("platform-http:/upload")
+    .setHeader(OpenAIConstants.MODERATION_TEXT, header("caption"))
+    .to("openai:moderation?moderationModel=omni-moderation-latest")
+    .choice()
+        .when(header(OpenAIConstants.MODERATION_FLAGGED).isEqualTo(true))
+            .setBody(constant("Your upload violates our usage policy."))
+        .otherwise()
+            .to("aws2-s3:uploads")
+    .end();
+----
+
+YAML::
++
+[source,yaml]
+----
+- from:
+    uri: platform-http:/upload
+    steps:
+      - setHeader:
+          name: CamelOpenAIModerationText
+          simple: "${header.caption}"
+      - to: openai:moderation?moderationModel=omni-moderation-latest
+      - choice:
+          when:
+            - simple: "${header.CamelOpenAIModerationFlagged} == true"
+              steps:
+                - setBody:
+                    constant: "Your upload violates our usage policy."
+          otherwise:
+            steps:
+              - to: aws2-s3:uploads
+----
+====
+
+The image is still in the body after the operation: a stream-cached body is 
reset, and a plain `InputStream`, which
+can be read only once, is replaced by its bytes.
+
+Things to keep in mind when moderating images:
+
+* The API accepts one image per request. A list body is moderated as text, and 
fails when it contains an image file;
+  use the xref:eips:split-eip.adoc[Split] EIP to moderate several images.
+* Images are scored for the `self-harm`, `self-harm/intent`, 
`self-harm/instructions`, `sexual`, `violence` and
+  `violence/graphic` categories only. The `categoryAppliedInputTypes` key of 
each verdict lists, per category, the
+  input types the score was computed from, so a category reported as `false` 
with no `image` entry was not checked
+  against the image.
+* Put only the content to check in the request. The text and the image are 
scored together, and unrelated text, such
+  as a prompt meant for a later `chat-completion` call, lowers the scores of 
the content that matters.
+
 === 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:
+a map with the keys `input`, `flagged`, `categories` and `categoryScores`, 
plus `categoryAppliedInputTypes` when the
+provider reports it, which makes a batch straightforward to split and route 
per item:
 
 [source,java]
 ----
@@ -271,7 +335,8 @@ 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`.
+* a missing body, an empty list, a list containing `null` elements, or a list 
containing an image file raises an
+  `IllegalArgumentException`.
 
 === Moderation Output Headers
 
@@ -282,7 +347,7 @@ The following headers are set after a moderation request:
 | 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`
+| `CamelOpenAIModerationResults` | List | One verdict per input, in input 
order. Each entry holds `input`, `flagged`, `categories`, `categoryScores` and, 
when the provider reports it, `categoryAppliedInputTypes`
 | `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
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 4176ef697a0f..ef7be91073c6 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
@@ -77,43 +77,44 @@
     "CamelOpenAISimilarityScore": { "index": 40, "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": 41, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String or 
List<String>", "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": 42, "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": 43, "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_FL [...]
-    "CamelOpenAIModerationResults": { "index": 44, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "java.util.List<java.util.Map<String, Object>>", "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", "cons [...]
-    "CamelOpenAIModerationCategories": { "index": 45, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "java.util.Map<String, Boolean>", "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.compo [...]
-    "CamelOpenAIModerationCategoryScores": { "index": 46, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "java.util.Map<String, Double>", "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. [...]
-    "CamelOpenAIModerationResponseModel": { "index": 47, "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": 48, "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": 49, "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": 50, "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": 51, "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": 52, "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": 53, "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": 54, "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": 55, "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": 56, "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": 57, "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": 58, "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": 59, "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": 60, "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" },
-    "CamelOpenAIImageModel": { "index": 61, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The model to use for image generation or editing (e.g., 
gpt-image-1, dall-e-3, dall-e-2)", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_MODEL" },
-    "CamelOpenAIImagePrompt": { "index": 62, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The prompt describing the image to generate, or the edit 
to apply. Takes precedence over the imagePrompt endpoint option and, for 
image-generation, over the message body", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants [...]
-    "CamelOpenAIImageSize": { "index": 63, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The size of the generated image (e.g., 1024x1024, 
1536x1024, auto)", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_SIZE" },
-    "CamelOpenAIImageQuality": { "index": 64, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The quality of the generated image (auto, high, medium, 
low for GPT image models; hd, standard for dall-e-3; standard for dall-e-2)", 
"constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_QUALITY" },
-    "CamelOpenAIImageResponseFormat": { "index": 65, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The response format of the generated 
image (url or b64_json). Only supported by dall-e-2 and dall-e-3; GPT image 
models always return base64", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_RESPONSE_FORMAT" },
-    "CamelOpenAIImageCount": { "index": 66, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "Integer", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The number of images to generate", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_COUNT" },
-    "CamelOpenAIImageBackground": { "index": 67, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The background of the generated image 
(transparent, opaque, auto). Only supported by GPT image models", 
"constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_BACKGROUND" },
-    "CamelOpenAIImageOutputFormat": { "index": 68, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The output format of the generated 
image (png, jpeg, webp). Only supported by GPT image models", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_OUTPUT_FORMAT" },
-    "CamelOpenAIImageOutputCompression": { "index": 69, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The compression level (0-100) for the 
webp or jpeg output formats. Only supported by GPT image models", 
"constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_OUTPUT_COMPRESSION" },
-    "CamelOpenAIImageStyle": { "index": 70, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The style of the generated image (vivid or natural). 
Only supported by dall-e-3", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_STYLE" },
-    "CamelOpenAIImageModeration": { "index": 71, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The content moderation level for image 
generation (low or auto). Only supported by GPT image models", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_MODERATION" },
-    "CamelOpenAIImageInputFidelity": { "index": 72, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "How closely the edit must match the 
style and features of the input image (high or low). Only supported by the 
image-edit operation on gpt-image-1 and gpt-image-1.5", "constantName": 
"org.apache.camel.component.openai.OpenAIConstan [...]
-    "CamelOpenAIImageMask": { "index": 73, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "byte[], 
java.io.File, java.nio.file.Path or java.io.InputStream", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": "An 
optional PNG mask for the image-edit operation, where the fully transparent 
areas indicate where the image should be edited", "constantName": 
"org.apache.camel.component.openai.Ope [...]
-    "CamelOpenAIImageResultCount": { "index": 74, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The number of images returned in the 
response", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_RESULT_COUNT" },
-    "CamelOpenAIImageRevisedPrompt": { "index": 75, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The prompt as revised by the model, 
when a single image is returned (dall-e-3)", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_REVISED_PROMPT" },
-    "CamelOpenAIImageRevisedPrompts": { "index": 76, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "java.util.List<String>", "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "The prompts as revised 
by the model, one entry per returned image (dall-e-3)", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_REVISED_PROMPTS" },
-    "CamelOpenAIImageInputTokens": { "index": 77, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The number of input tokens billed for 
the image request. Only reported by GPT image models", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_INPUT_TOKENS" },
-    "CamelOpenAIImageOutputTokens": { "index": 78, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The number of output tokens billed for 
the image request. Only reported by GPT image models", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_OUTPUT_TOKENS" },
-    "CamelOpenAIImageTotalTokens": { "index": 79, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The total number of tokens billed for 
the image request. Only reported by GPT image models", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_TOTAL_TOKENS" }
+    "CamelOpenAIModerationText": { "index": 43, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "Text to moderate together with an image 
body, such as the caption the image was posted with. The text and the image are 
scored as one input and share a single verdict. Ignored when the body is not an 
image", "constantName": "org.apache [...]
+    "CamelOpenAIModerationFlagged": { "index": 44, "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_FL [...]
+    "CamelOpenAIModerationResults": { "index": 45, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "java.util.List<java.util.Map<String, Object>>", "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', 'categoryScores' 
and, when the provider reports it, 'categoryAppliedInpu [...]
+    "CamelOpenAIModerationCategories": { "index": 46, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "java.util.Map<String, Boolean>", "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.compo [...]
+    "CamelOpenAIModerationCategoryScores": { "index": 47, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "java.util.Map<String, Double>", "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. [...]
+    "CamelOpenAIModerationResponseModel": { "index": 48, "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": 49, "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": 50, "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": 51, "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": 52, "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": 53, "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": 54, "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": 55, "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": 56, "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": 57, "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": 58, "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": 59, "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": 60, "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": 61, "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" },
+    "CamelOpenAIImageModel": { "index": 62, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The model to use for image generation or editing (e.g., 
gpt-image-1, dall-e-3, dall-e-2)", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_MODEL" },
+    "CamelOpenAIImagePrompt": { "index": 63, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The prompt describing the image to generate, or the edit 
to apply. Takes precedence over the imagePrompt endpoint option and, for 
image-generation, over the message body", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants [...]
+    "CamelOpenAIImageSize": { "index": 64, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The size of the generated image (e.g., 1024x1024, 
1536x1024, auto)", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_SIZE" },
+    "CamelOpenAIImageQuality": { "index": 65, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The quality of the generated image (auto, high, medium, 
low for GPT image models; hd, standard for dall-e-3; standard for dall-e-2)", 
"constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_QUALITY" },
+    "CamelOpenAIImageResponseFormat": { "index": 66, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The response format of the generated 
image (url or b64_json). Only supported by dall-e-2 and dall-e-3; GPT image 
models always return base64", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_RESPONSE_FORMAT" },
+    "CamelOpenAIImageCount": { "index": 67, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "Integer", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The number of images to generate", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_COUNT" },
+    "CamelOpenAIImageBackground": { "index": 68, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The background of the generated image 
(transparent, opaque, auto). Only supported by GPT image models", 
"constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_BACKGROUND" },
+    "CamelOpenAIImageOutputFormat": { "index": 69, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The output format of the generated 
image (png, jpeg, webp). Only supported by GPT image models", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_OUTPUT_FORMAT" },
+    "CamelOpenAIImageOutputCompression": { "index": 70, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The compression level (0-100) for the 
webp or jpeg output formats. Only supported by GPT image models", 
"constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_OUTPUT_COMPRESSION" },
+    "CamelOpenAIImageStyle": { "index": 71, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "String", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The style of the generated image (vivid or natural). 
Only supported by dall-e-3", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_STYLE" },
+    "CamelOpenAIImageModeration": { "index": 72, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The content moderation level for image 
generation (low or auto). Only supported by GPT image models", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_MODERATION" },
+    "CamelOpenAIImageInputFidelity": { "index": 73, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "How closely the edit must match the 
style and features of the input image (high or low). Only supported by the 
image-edit operation on gpt-image-1 and gpt-image-1.5", "constantName": 
"org.apache.camel.component.openai.OpenAIConstan [...]
+    "CamelOpenAIImageMask": { "index": 74, "kind": "header", "displayName": 
"", "group": "producer", "label": "", "required": false, "javaType": "byte[], 
java.io.File, java.nio.file.Path or java.io.InputStream", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "description": "An 
optional PNG mask for the image-edit operation, where the fully transparent 
areas indicate where the image should be edited", "constantName": 
"org.apache.camel.component.openai.Ope [...]
+    "CamelOpenAIImageResultCount": { "index": 75, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The number of images returned in the 
response", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_RESULT_COUNT" },
+    "CamelOpenAIImageRevisedPrompt": { "index": 76, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The prompt as revised by the model, 
when a single image is returned (dall-e-3)", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_REVISED_PROMPT" },
+    "CamelOpenAIImageRevisedPrompts": { "index": 77, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "java.util.List<String>", "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "The prompts as revised 
by the model, one entry per returned image (dall-e-3)", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_REVISED_PROMPTS" },
+    "CamelOpenAIImageInputTokens": { "index": 78, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The number of input tokens billed for 
the image request. Only reported by GPT image models", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_INPUT_TOKENS" },
+    "CamelOpenAIImageOutputTokens": { "index": 79, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The number of output tokens billed for 
the image request. Only reported by GPT image models", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_OUTPUT_TOKENS" },
+    "CamelOpenAIImageTotalTokens": { "index": 80, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "Long", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "description": "The total number of tokens billed for 
the image request. Only reported by GPT image models", "constantName": 
"org.apache.camel.component.openai.OpenAIConstants#IMAGE_TOTAL_TOKENS" }
   },
   "properties": {
     "operation": { "index": 0, "kind": "path", "displayName": "Operation", 
"group": "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", 
"image-generation", "image-edit" ], "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Th [...]
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 90a32cae5157..dad735f8f202 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
@@ -47,7 +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
+* `moderation` - Check text or an image against the OpenAI usage policies 
before it reaches a model
 * `image-generation` - Generate images from a text prompt (e.g., gpt-image-1, 
gpt-image-1-mini)
 * `image-edit` - Edit an existing image, optionally through a mask, from a 
text prompt
 
@@ -1297,7 +1297,7 @@ The component may throw the following exceptions:
   ** 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)
+  ** When the moderation input list is empty, contains null elements, or 
contains an image file (moderation)
   ** When the image model is missing (image-generation, image-edit)
   ** When the prompt is missing (image-generation, image-edit) or the image 
list is empty (image-edit)
   ** When an unsupported image or mask body type is provided (image-edit)
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 7188c55c03d0..7989b9a40716 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
@@ -172,7 +172,7 @@ Set `storeFullResponse=true` to keep the complete SDK 
response in the `CamelOpen
 
 == Moderation Operation
 
-The `moderation` operation checks text against the OpenAI usage policies. It 
is the canonical pre-filter for untrusted
+The `moderation` operation checks text, or an image, 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
@@ -183,9 +183,6 @@ defined by the provider, so it is not a substitute for 
authentication, authoriza
 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]
@@ -224,11 +221,78 @@ YAML::
 ----
 ====
 
+=== Moderating an Image
+
+An image body is sent to the multi-modal input of the moderation API instead 
of being converted to text. The body is
+treated as an image when its MIME type is an image type, detected the same way 
as for the vision input of
+`chat-completion`: a `File`, `Path` or `WrappedFile` body from its file name 
or content-type header, and a `byte[]` or
+`InputStream` body, as produced by cloud storage components, from its 
content-type header. Set the
+`CamelOpenAIMediaType` header when the body carries no content-type metadata. 
A `byte[]` or `InputStream` body without
+an image type is still moderated as text.
+
+Text posted together with the image, such as its caption, goes in the 
`CamelOpenAIModerationText` header. The API
+scores the text and the image as one input, so they share a single verdict, 
whose `input` key holds that text, or
+`null` for an image moderated on its own. The header is ignored when the body 
is not an image.
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("platform-http:/upload")
+    .setHeader(OpenAIConstants.MODERATION_TEXT, header("caption"))
+    .to("openai:moderation?moderationModel=omni-moderation-latest")
+    .choice()
+        .when(header(OpenAIConstants.MODERATION_FLAGGED).isEqualTo(true))
+            .setBody(constant("Your upload violates our usage policy."))
+        .otherwise()
+            .to("aws2-s3:uploads")
+    .end();
+----
+
+YAML::
++
+[source,yaml]
+----
+- from:
+    uri: platform-http:/upload
+    steps:
+      - setHeader:
+          name: CamelOpenAIModerationText
+          simple: "${header.caption}"
+      - to: openai:moderation?moderationModel=omni-moderation-latest
+      - choice:
+          when:
+            - simple: "${header.CamelOpenAIModerationFlagged} == true"
+              steps:
+                - setBody:
+                    constant: "Your upload violates our usage policy."
+          otherwise:
+            steps:
+              - to: aws2-s3:uploads
+----
+====
+
+The image is still in the body after the operation: a stream-cached body is 
reset, and a plain `InputStream`, which
+can be read only once, is replaced by its bytes.
+
+Things to keep in mind when moderating images:
+
+* The API accepts one image per request. A list body is moderated as text, and 
fails when it contains an image file;
+  use the xref:eips:split-eip.adoc[Split] EIP to moderate several images.
+* Images are scored for the `self-harm`, `self-harm/intent`, 
`self-harm/instructions`, `sexual`, `violence` and
+  `violence/graphic` categories only. The `categoryAppliedInputTypes` key of 
each verdict lists, per category, the
+  input types the score was computed from, so a category reported as `false` 
with no `image` entry was not checked
+  against the image.
+* Put only the content to check in the request. The text and the image are 
scored together, and unrelated text, such
+  as a prompt meant for a later `chat-completion` call, lowers the scores of 
the content that matters.
+
 === 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:
+a map with the keys `input`, `flagged`, `categories` and `categoryScores`, 
plus `categoryAppliedInputTypes` when the
+provider reports it, which makes a batch straightforward to split and route 
per item:
 
 [source,java]
 ----
@@ -271,7 +335,8 @@ 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`.
+* a missing body, an empty list, a list containing `null` elements, or a list 
containing an image file raises an
+  `IllegalArgumentException`.
 
 === Moderation Output Headers
 
@@ -282,7 +347,7 @@ The following headers are set after a moderation request:
 | 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`
+| `CamelOpenAIModerationResults` | List | One verdict per input, in input 
order. Each entry holds `input`, `flagged`, `categories`, `categoryScores` and, 
when the provider reports it, `categoryAppliedInputTypes`
 | `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
diff --git 
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/MimeTypeHelper.java
 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/MimeTypeHelper.java
index 2d404f4d9b6e..fadfc97d8446 100644
--- 
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/MimeTypeHelper.java
+++ 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/MimeTypeHelper.java
@@ -105,7 +105,7 @@ final class MimeTypeHelper {
         return semicolon > 0 ? value.substring(0, semicolon).trim() : 
value.trim();
     }
 
-    private static String fromFileName(String fileName) {
+    static String fromFileName(String fileName) {
         String mime = 
org.apache.camel.util.MimeTypeHelper.probeMimeType(fileName);
         if (mime == null) {
             // markdown is not in the camel-util MIME type table
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 e81816d01167..08e5d4a86c4d 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
@@ -144,6 +144,11 @@ public final class OpenAIConstants {
     // 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";
+    @Metadata(description = "Text to moderate together with an image body, 
such as the caption the image was posted "
+                            + "with. The text and the image are scored as one 
input and share a single verdict. "
+                            + "Ignored when the body is not an image",
+              javaType = "String")
+    public static final String MODERATION_TEXT = "CamelOpenAIModerationText";
 
     // Moderation Output Headers
     @Metadata(description = "Whether the moderation API flagged the input as 
violating the usage policies. "
@@ -151,8 +156,8 @@ public final class OpenAIConstants {
               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",
+                            + "'input', 'flagged', 'categories', 
'categoryScores' and, when the provider reports it, "
+                            + "'categoryAppliedInputTypes', so a batch can be 
split and routed per item",
               javaType = "java.util.List<java.util.Map<String, Object>>")
     public static final String MODERATION_RESULTS = 
"CamelOpenAIModerationResults";
     @Metadata(description = "The moderation categories and whether each one 
was violated, for a single input. "
@@ -169,6 +174,7 @@ public final class OpenAIConstants {
     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";
+    public static final String MODERATION_RESULT_CATEGORY_APPLIED_INPUT_TYPES 
= "categoryAppliedInputTypes";
     @Metadata(description = "The moderation model used in the response", 
javaType = "String")
     public static final String MODERATION_RESPONSE_MODEL = 
"CamelOpenAIModerationResponseModel";
 
diff --git 
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIImageSupport.java
 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIImageSupport.java
index 352ca5ecca2d..c624a71cf610 100644
--- 
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIImageSupport.java
+++ 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIImageSupport.java
@@ -190,7 +190,7 @@ final class OpenAIImageSupport {
         return null;
     }
 
-    private static String fileNameOfBody(Object body) {
+    static String fileNameOfBody(Object body) {
         Object source = body instanceof WrappedFile<?> wrappedFile ? 
wrappedFile.getFile() : body;
         if (source instanceof File file) {
             return file.getName();
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
index 1c1b9d24b97d..1f06bff7bdca 100644
--- 
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
@@ -16,18 +16,31 @@
  */
 package org.apache.camel.component.openai;
 
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
+import com.openai.core.JsonField;
 import com.openai.models.moderations.Moderation;
 import com.openai.models.moderations.ModerationCreateParams;
 import com.openai.models.moderations.ModerationCreateResponse;
+import com.openai.models.moderations.ModerationImageUrlInput;
+import com.openai.models.moderations.ModerationMultiModalInput;
+import com.openai.models.moderations.ModerationTextInput;
 import org.apache.camel.AsyncCallback;
 import org.apache.camel.CamelExchangeException;
 import org.apache.camel.Exchange;
 import org.apache.camel.Message;
+import org.apache.camel.StreamCache;
+import org.apache.camel.WrappedFile;
 import org.apache.camel.component.ai.observability.GenAiErrorSupport;
 import org.apache.camel.component.ai.observability.GenAiObservability;
 import org.apache.camel.component.ai.observability.GenAiObservation;
@@ -35,10 +48,11 @@ import 
org.apache.camel.component.ai.observability.GenAiObservationContext;
 import org.apache.camel.component.ai.observability.GenAiOperationName;
 import org.apache.camel.component.ai.observability.GenAiUsage;
 import org.apache.camel.support.DefaultAsyncProducer;
+import org.apache.camel.support.MessageHelper;
 import org.apache.camel.util.ObjectHelper;
 
 /**
- * OpenAI producer for moderating text against the usage policies.
+ * OpenAI producer for moderating text or an image against the usage policies.
  * <p>
  * 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.
@@ -78,19 +92,27 @@ public class OpenAIModerationProducer extends 
DefaultAsyncProducer {
             throw new IllegalArgumentException("Moderation model must be 
specified via moderationModel parameter");
         }
 
-        boolean batch = in.getBody() instanceof List;
-        List<String> 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));
+        boolean batch = in.getBody() instanceof List;
+        String imageMimeType = batch ? null : resolveImageMimeType(in);
+        List<String> inputs;
+        if (imageMimeType != null) {
+            String text = in.getHeader(OpenAIConstants.MODERATION_TEXT, 
String.class);
+            
paramsBuilder.inputOfModerationMultiModalArray(multiModalInput(readImage(in), 
imageMimeType, text));
+            // the API scores a multi-modal input as a whole, so the text and 
the image share a single verdict
+            inputs = Collections.singletonList(ObjectHelper.isNotEmpty(text) ? 
text : null);
         } else {
-            paramsBuilder.inputOfStrings(inputs);
+            inputs = extractInputs(exchange);
+            if (inputs.isEmpty()) {
+                throw new IllegalArgumentException("No input text provided for 
moderation");
+            }
+            if (inputs.size() == 1) {
+                paramsBuilder.input(inputs.get(0));
+            } else {
+                paramsBuilder.inputOfStrings(inputs);
+            }
         }
 
         GenAiObservationContext observationContext = 
GenAiObservationContext.builder()
@@ -144,6 +166,11 @@ public class OpenAIModerationProducer extends 
DefaultAsyncProducer {
                 }
                 if (item instanceof String s) {
                     inputs.add(s);
+                } else if (isImageFile(item)) {
+                    // converted to a String, the image bytes would be 
moderated as text and most likely pass
+                    throw new IllegalArgumentException(
+                            "A list body is moderated as text, and the 
moderation API accepts one image per request. "
+                                                       + "Split the list and 
moderate each image on its own");
                 } else {
                     // convert as a non-String body would be, so an 
unconvertible type fails instead of
                     // being moderated as its toString()
@@ -163,6 +190,75 @@ public class OpenAIModerationProducer extends 
DefaultAsyncProducer {
         return inputs;
     }
 
+    /**
+     * Returns the MIME type of an image body, or {@code null} when the body 
is moderated as text. The type is detected
+     * the same way as for the vision input of chat-completion, from the 
headers first and then from the file name.
+     */
+    private static String resolveImageMimeType(Message in) {
+        Object body = in.getBody();
+        File file = localFile(body);
+        String mimeType;
+        if (file != null) {
+            mimeType = MimeTypeHelper.resolveForFile(in, file);
+        } else if (body instanceof WrappedFile || body instanceof byte[] || 
body instanceof InputStream) {
+            mimeType = MimeTypeHelper.resolveForBinary(in);
+        } else {
+            return null;
+        }
+        return MimeTypeHelper.isImage(mimeType) ? mimeType : null;
+    }
+
+    private static boolean isImageFile(Object value) {
+        String fileName = OpenAIImageSupport.fileNameOfBody(value);
+        return fileName != null && 
MimeTypeHelper.isImage(MimeTypeHelper.fromFileName(fileName));
+    }
+
+    private static File localFile(Object body) {
+        if (body instanceof WrappedFile<?> wrappedFile && 
wrappedFile.getFile() instanceof File file) {
+            return file;
+        } else if (body instanceof File file) {
+            return file;
+        } else if (body instanceof Path path) {
+            return path.toFile();
+        }
+        return null;
+    }
+
+    private static byte[] readImage(Message in) throws IOException {
+        Object body = in.getBody();
+        File file = localFile(body);
+        if (file != null) {
+            return Files.readAllBytes(file.toPath());
+        } else if (body instanceof byte[] bytes) {
+            return bytes;
+        }
+
+        byte[] image = in.getBody(byte[].class);
+        if (image == null) {
+            throw new IllegalArgumentException(
+                    "Cannot read the image to moderate from a body of type " + 
body.getClass().getName());
+        }
+        // the body is passed through, so reading it here must leave it 
readable for the rest of the route
+        if (body instanceof StreamCache) {
+            MessageHelper.resetStreamCache(in);
+        } else if (body instanceof InputStream) {
+            in.setBody(image);
+        }
+        return image;
+    }
+
+    private static List<ModerationMultiModalInput> multiModalInput(byte[] 
image, String mimeType, String text) {
+        List<ModerationMultiModalInput> parts = new ArrayList<>(2);
+        if (ObjectHelper.isNotEmpty(text)) {
+            
parts.add(ModerationMultiModalInput.ofText(ModerationTextInput.builder().text(text).build()));
+        }
+        String dataUrl = "data:" + mimeType + ";base64," + 
Base64.getEncoder().encodeToString(image);
+        
parts.add(ModerationMultiModalInput.ofImageUrl(ModerationImageUrlInput.builder()
+                
.imageUrl(ModerationImageUrlInput.ImageUrl.builder().url(dataUrl).build())
+                .build()));
+        return parts;
+    }
+
     private void setResponseHeaders(
             Message message, ModerationCreateResponse response, List<String> 
inputs, boolean batch) {
         message.setHeader(OpenAIConstants.MODERATION_RESPONSE_MODEL, 
response.model());
@@ -178,6 +274,9 @@ public class OpenAIModerationProducer extends 
DefaultAsyncProducer {
             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()));
+            // an OpenAI-compatible provider may not report the input types, 
which must not fail the verdict
+            result._categoryAppliedInputTypes().asKnown().ifPresent(types -> 
verdict
+                    
.put(OpenAIConstants.MODERATION_RESULT_CATEGORY_APPLIED_INPUT_TYPES, 
toAppliedInputTypes(types)));
             verdicts.add(verdict);
         }
         message.setHeader(OpenAIConstants.MODERATION_RESULTS, verdicts);
@@ -225,6 +324,28 @@ public class OpenAIModerationProducer extends 
DefaultAsyncProducer {
         return map;
     }
 
+    private Map<String, List<String>> 
toAppliedInputTypes(Moderation.CategoryAppliedInputTypes types) {
+        Map<String, List<String>> map = new LinkedHashMap<>();
+        putInputTypes(map, "harassment", types._harassment());
+        putInputTypes(map, "harassment/threatening", 
types._harassmentThreatening());
+        putInputTypes(map, "hate", types._hate());
+        putInputTypes(map, "hate/threatening", types._hateThreatening());
+        putInputTypes(map, "illicit", types._illicit());
+        putInputTypes(map, "illicit/violent", types._illicitViolent());
+        putInputTypes(map, "self-harm", types._selfHarm());
+        putInputTypes(map, "self-harm/instructions", 
types._selfHarmInstructions());
+        putInputTypes(map, "self-harm/intent", types._selfHarmIntent());
+        putInputTypes(map, "sexual", types._sexual());
+        putInputTypes(map, "sexual/minors", types._sexualMinors());
+        putInputTypes(map, "violence", types._violence());
+        putInputTypes(map, "violence/graphic", types._violenceGraphic());
+        return map;
+    }
+
+    private static void putInputTypes(Map<String, List<String>> map, String 
category, JsonField<? extends List<?>> field) {
+        field.asKnown().ifPresent(values -> map.put(category, 
values.stream().map(Object::toString).toList()));
+    }
+
     private <T> T resolveParameter(
             Message message, String headerName,
             T defaultValue, Class<T> type) {
diff --git 
a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIModerationImageMockTest.java
 
b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIModerationImageMockTest.java
new file mode 100644
index 000000000000..1f6a96ec35e5
--- /dev/null
+++ 
b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIModerationImageMockTest.java
@@ -0,0 +1,210 @@
+/*
+ * 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.io.ByteArrayInputStream;
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Base64;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.CamelExchangeException;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.infra.openai.mock.OpenAIMock;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class OpenAIModerationImageMockTest extends CamelTestSupport {
+
+    private static final byte[] PNG = { (byte) 0x89, 'P', 'N', 'G', '\r', 
'\n', 0x1a, '\n' };
+    private static final byte[] JPEG = { (byte) 0xff, (byte) 0xd8, (byte) 
0xff, (byte) 0xe0 };
+
+    @TempDir
+    Path tempDir;
+
+    @RegisterExtension
+    public OpenAIMock openAIMock = new OpenAIMock().builder()
+            .whenImageModeration()
+            .assertModerationImageUrl(url -> assertThat(url)
+                    .isEqualTo("data:image/png;base64," + 
Base64.getEncoder().encodeToString(PNG)))
+            .replyWithModerationAllowed()
+            .end()
+            .whenImageModeration("Look what I found")
+            .assertModerationImageUrl(url -> 
assertThat(url).startsWith("data:image/jpeg;base64,"))
+            .replyWithModerationFlagged("violence", 0.91)
+            .end()
+            .whenImageModeration("Provider returns no verdict")
+            .replyWithoutModerationResult()
+            .end()
+            .whenModeration("Apache Camel is an integration framework")
+            .replyWithModerationAllowed()
+            .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-no-stream-caching")
+                        .noStreamCaching()
+                        .to("openai:moderation?apiKey=dummy&baseUrl=" + 
openAIMock.getBaseUrl() + "/v1");
+            }
+        };
+    }
+
+    @Test
+    void testImageAlone() {
+        Exchange result = template.request("direct:moderation", e -> {
+            e.getIn().setBody(PNG);
+            e.getIn().setHeader(OpenAIConstants.MEDIA_TYPE, "image/png");
+        });
+
+        assertThat(result.getException()).isNull();
+        assertThat(result.getMessage().getBody()).isSameAs(PNG);
+        
assertThat(result.getMessage().getHeader(OpenAIConstants.MODERATION_FLAGGED)).isEqualTo(false);
+
+        List<Map<String, Object>> verdicts = verdicts(result);
+        assertThat(verdicts).hasSize(1);
+        // there is no text to report back for an image moderated on its own
+        
assertThat(verdicts.get(0)).containsEntry(OpenAIConstants.MODERATION_RESULT_INPUT,
 null);
+
+        @SuppressWarnings("unchecked")
+        Map<String, List<String>> inputTypes = (Map<String, List<String>>) 
verdicts.get(0)
+                
.get(OpenAIConstants.MODERATION_RESULT_CATEGORY_APPLIED_INPUT_TYPES);
+        assertThat(inputTypes).containsEntry("sexual", 
List.of("image")).containsEntry("hate", List.of());
+    }
+
+    @Test
+    void testImageFileWithText() throws Exception {
+        File image = Files.write(tempDir.resolve("found.jpg"), JPEG).toFile();
+
+        Exchange result = template.request("direct:moderation", e -> {
+            e.getIn().setBody(image);
+            e.getIn().setHeader(OpenAIConstants.MODERATION_TEXT, "Look what I 
found");
+        });
+
+        assertThat(result.getException()).isNull();
+        assertThat(result.getMessage().getBody()).isSameAs(image);
+        
assertThat(result.getMessage().getHeader(OpenAIConstants.MODERATION_FLAGGED)).isEqualTo(true);
+
+        // the text and the image share one verdict, which carries the text as 
its input
+        List<Map<String, Object>> verdicts = verdicts(result);
+        assertThat(verdicts).hasSize(1);
+        assertThat(verdicts.get(0))
+                .containsEntry(OpenAIConstants.MODERATION_RESULT_INPUT, "Look 
what I found")
+                .containsEntry(OpenAIConstants.MODERATION_RESULT_FLAGGED, 
true);
+
+        @SuppressWarnings("unchecked")
+        Map<String, Double> scores
+                = 
result.getMessage().getHeader(OpenAIConstants.MODERATION_CATEGORY_SCORES, 
Map.class);
+        assertThat(scores.get("violence")).isEqualTo(0.91);
+
+        @SuppressWarnings("unchecked")
+        Map<String, List<String>> inputTypes = (Map<String, List<String>>) 
verdicts.get(0)
+                
.get(OpenAIConstants.MODERATION_RESULT_CATEGORY_APPLIED_INPUT_TYPES);
+        assertThat(inputTypes)
+                .containsEntry("violence", List.of("text", "image"))
+                .containsEntry("hate", List.of("text"));
+    }
+
+    @Test
+    void testStreamCachedImageStaysReadable() {
+        Exchange result = template.request("direct:moderation", e -> {
+            e.getIn().setBody(new ByteArrayInputStream(PNG));
+            e.getIn().setHeader(Exchange.CONTENT_TYPE, "image/png");
+        });
+
+        assertThat(result.getException()).isNull();
+        assertThat(result.getMessage().getBody(byte[].class)).isEqualTo(PNG);
+    }
+
+    @Test
+    void testPlainStreamImageStaysReadable() {
+        Exchange result = 
template.request("direct:moderation-no-stream-caching", e -> {
+            e.getIn().setBody(new ByteArrayInputStream(PNG));
+            e.getIn().setHeader(Exchange.CONTENT_TYPE, "image/png");
+        });
+
+        assertThat(result.getException()).isNull();
+        // a stream can be read only once, so the body is kept as the bytes 
that were moderated
+        assertThat(result.getMessage().getBody()).isEqualTo(PNG);
+    }
+
+    @Test
+    void testMissingImageVerdictFailsClosed() {
+        Exchange result = template.request("direct:moderation", e -> {
+            e.getIn().setBody(PNG);
+            e.getIn().setHeader(OpenAIConstants.MEDIA_TYPE, "image/png");
+            e.getIn().setHeader(OpenAIConstants.MODERATION_TEXT, "Provider 
returns no verdict");
+        });
+
+        assertThat(result.getException())
+                .isInstanceOf(CamelExchangeException.class)
+                .hasMessageContaining("Moderation returned 0 result(s) for 1 
input(s)");
+        
assertThat(result.getMessage().getHeader(OpenAIConstants.MODERATION_RESULTS)).isNull();
+    }
+
+    @Test
+    void testTextHeaderIsIgnoredForTextBody() {
+        Exchange result = template.request("direct:moderation", e -> {
+            e.getIn().setBody("Apache Camel is an integration framework");
+            e.getIn().setHeader(OpenAIConstants.MODERATION_TEXT, "Look what I 
found");
+        });
+
+        assertThat(result.getException()).isNull();
+        assertThat(verdicts(result).get(0))
+                .containsEntry(OpenAIConstants.MODERATION_RESULT_INPUT, 
"Apache Camel is an integration framework");
+    }
+
+    @Test
+    void testBinaryBodyWithoutImageTypeIsModeratedAsText() {
+        Exchange result = template.request("direct:moderation",
+                e -> e.getIn().setBody("Apache Camel is an integration 
framework".getBytes(StandardCharsets.UTF_8)));
+
+        assertThat(result.getException()).isNull();
+        assertThat(verdicts(result).get(0))
+                .containsEntry(OpenAIConstants.MODERATION_RESULT_INPUT, 
"Apache Camel is an integration framework");
+    }
+
+    @Test
+    void testImageInListFails() throws Exception {
+        File image = Files.write(tempDir.resolve("found.png"), PNG).toFile();
+
+        Exchange result = template.request("direct:moderation",
+                e -> e.getIn().setBody(List.of("Apache Camel is an integration 
framework", image)));
+
+        assertThat(result.getException())
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("Split the list and moderate each image 
on its own");
+    }
+
+    @SuppressWarnings("unchecked")
+    private static List<Map<String, Object>> verdicts(Exchange result) {
+        return 
result.getMessage().getHeader(OpenAIConstants.MODERATION_RESULTS, List.class);
+    }
+}
diff --git 
a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/integration/OpenAIModerationExternalServiceIT.java
 
b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/integration/OpenAIModerationExternalServiceIT.java
index 73e8808e75b3..a82e974d829e 100644
--- 
a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/integration/OpenAIModerationExternalServiceIT.java
+++ 
b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/integration/OpenAIModerationExternalServiceIT.java
@@ -16,6 +16,15 @@
  */
 package org.apache.camel.component.openai.integration;
 
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.util.List;
+import java.util.Map;
+
+import javax.imageio.ImageIO;
+
 import org.apache.camel.Exchange;
 import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.component.openai.OpenAIConstants;
@@ -52,4 +61,42 @@ public class OpenAIModerationExternalServiceIT extends 
OpenAIExternalServiceTest
         
assertThat(result.getMessage().getHeader(OpenAIConstants.MODERATION_FLAGGED, 
Boolean.class)).isNotNull();
         
assertThat(result.getMessage().getHeader(OpenAIConstants.MODERATION_RESPONSE_MODEL,
 String.class)).isNotBlank();
     }
+
+    @Test
+    void imageAndTextShareOneVerdict() throws Exception {
+        byte[] image = bluePng();
+        Exchange result = template.request("direct:moderate", exchange -> {
+            exchange.getIn().setBody(image);
+            exchange.getIn().setHeader(OpenAIConstants.MEDIA_TYPE, 
"image/png");
+            exchange.getIn().setHeader(OpenAIConstants.MODERATION_TEXT, "A 
plain blue square.");
+        });
+
+        assertThat(result.getException())
+                .as("The configured moderation model must accept image input, 
for example omni-moderation-latest")
+                .isNull();
+        assertThat(result.getMessage().getBody()).isSameAs(image);
+        
assertThat(result.getMessage().getHeader(OpenAIConstants.MODERATION_FLAGGED, 
Boolean.class)).isFalse();
+
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> verdicts
+                = 
result.getMessage().getHeader(OpenAIConstants.MODERATION_RESULTS, List.class);
+        assertThat(verdicts).hasSize(1);
+
+        @SuppressWarnings("unchecked")
+        Map<String, List<String>> inputTypes = (Map<String, List<String>>) 
verdicts.get(0)
+                
.get(OpenAIConstants.MODERATION_RESULT_CATEGORY_APPLIED_INPUT_TYPES);
+        assertThat(inputTypes.get("violence")).contains("text", "image");
+        assertThat(inputTypes.get("hate")).containsExactly("text");
+    }
+
+    private static byte[] bluePng() throws Exception {
+        BufferedImage image = new BufferedImage(64, 64, 
BufferedImage.TYPE_INT_RGB);
+        Graphics2D graphics = image.createGraphics();
+        graphics.setColor(Color.BLUE);
+        graphics.fillRect(0, 0, 64, 64);
+        graphics.dispose();
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        ImageIO.write(image, "png", out);
+        return out.toByteArray();
+    }
 }
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 9a58332b789a..b87d2913d5d9 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -1790,6 +1790,13 @@ so a downstream reader that expected that type received 
an incompatible object.
 A route that reads the full embeddings or audio response from 
`CamelOpenAIResponse` must switch to the
 matching property. The chat-completion, responses, moderation and image 
operations are unchanged.
 
+The `moderation` operation now sends an image body to the multi-modal input of 
the moderation API. A `File`,
+`Path`, `WrappedFile`, `byte[]` or `InputStream` body whose detected MIME type 
is an image type was previously
+converted to a `String` and moderated as text; it is now moderated as an 
image, which needs a moderation model
+that accepts images, such as the default `omni-moderation-latest`. A list body 
containing an image file now fails
+with an `IllegalArgumentException` instead of moderating the file content as 
text; split the list to moderate
+each image on its own.
+
 === camel-infinispan
 
 The `CamelInfinispanOperationResult` header is deprecated. It has not been set 
or read by the component
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 7935d3be47e7..647dbfc2e9ef 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
@@ -2713,6 +2713,20 @@ public interface OpenAIEndpointBuilderFactory {
         public String openAIModerationModel() {
             return "CamelOpenAIModerationModel";
         }
+        /**
+         * Text to moderate together with an image body, such as the caption 
the
+         * image was posted with. The text and the image are scored as one 
input
+         * and share a single verdict. Ignored when the body is not an image.
+         * 
+         * The option is a: {@code String} type.
+         * 
+         * Group: producer
+         * 
+         * @return the name of the header {@code OpenAIModerationText}.
+         */
+        public String openAIModerationText() {
+            return "CamelOpenAIModerationText";
+        }
         /**
          * 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
@@ -2729,8 +2743,10 @@ public interface OpenAIEndpointBuilderFactory {
         }
         /**
          * 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.
+         * entry holds the keys 'input', 'flagged', 'categories',
+         * 'categoryScores' and, when the provider reports it,
+         * 'categoryAppliedInputTypes', so a batch can be split and routed per
+         * item.
          * 
          * The option is a: {@code java.util.List<java.util.Map<String,
          * Object>>} type.
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
index 39a5a9611654..9a9139ee863f 100644
--- 
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
@@ -18,6 +18,8 @@ package org.apache.camel.test.infra.openai.mock;
 
 import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.Objects;
+import java.util.function.Consumer;
 
 /**
  * Represents a mock expectation for a moderation request. Contains the 
expected input text and the moderation verdict
@@ -27,21 +29,48 @@ public class ModerationExpectation {
     private static final String DEFAULT_MODEL = "camel-moderation";
 
     private final String expectedInput;
+    private final boolean imageExpected;
     private final Map<String, Boolean> flaggedCategories = new 
LinkedHashMap<>();
     private final Map<String, Double> categoryScores = new LinkedHashMap<>();
     private boolean flagged;
     private boolean illicitCategoriesIncluded = true;
     private boolean resultOmitted;
     private String model = DEFAULT_MODEL;
+    private Consumer<String> imageUrlAssertion;
 
     public ModerationExpectation(String expectedInput) {
+        this(expectedInput, false);
+    }
+
+    /**
+     * @param expectedInput the text expected in the request, or {@code null} 
for an image moderated on its own
+     * @param imageExpected whether the request is expected to carry an image 
as multi-modal input
+     */
+    public ModerationExpectation(String expectedInput, boolean imageExpected) {
         this.expectedInput = expectedInput;
+        this.imageExpected = imageExpected;
     }
 
     public String getExpectedInput() {
         return expectedInput;
     }
 
+    public boolean isImageExpected() {
+        return imageExpected;
+    }
+
+    public Consumer<String> getImageUrlAssertion() {
+        return imageUrlAssertion;
+    }
+
+    /**
+     * Runs against the {@code image_url} of a matching multi-modal request, 
for example to check the data URL built
+     * from the message body.
+     */
+    public void setImageUrlAssertion(Consumer<String> imageUrlAssertion) {
+        this.imageUrlAssertion = imageUrlAssertion;
+    }
+
     public boolean isFlagged() {
         return flagged;
     }
@@ -109,12 +138,19 @@ public class ModerationExpectation {
     }
 
     public boolean matches(String input) {
-        return expectedInput.equals(input);
+        return !imageExpected && Objects.equals(expectedInput, input);
+    }
+
+    /**
+     * Matches a multi-modal request by its text part, which is {@code null} 
when the image is sent without text.
+     */
+    public boolean matchesMultiModal(String text, boolean hasImage) {
+        return imageExpected == hasImage && Objects.equals(expectedInput, 
text);
     }
 
     @Override
     public String toString() {
-        return String.format("ModerationExpectation{input='%s', flagged=%b, 
categories=%s, model='%s'}",
-                expectedInput, flagged, flaggedCategories.keySet(), model);
+        return String.format("ModerationExpectation{input='%s', image=%b, 
flagged=%b, categories=%s, model='%s'}",
+                expectedInput, imageExpected, 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
index 523c614f3d45..32885c46f17a 100644
--- 
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
@@ -24,6 +24,7 @@ import java.util.List;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.sun.net.httpserver.HttpExchange;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -53,16 +54,23 @@ public class ModerationRequestHandler {
             LOG.debug("Processing moderation request: {}", requestBody);
 
             JsonNode rootNode = objectMapper.readTree(requestBody);
-            List<String> inputs = extractInputs(rootNode);
             String requestedModel = extractModel(rootNode);
 
-            List<ModerationExpectation> matchedExpectations = new 
ArrayList<>(inputs.size());
-            for (String input : inputs) {
-                matchedExpectations.add(findExpectationByInput(input));
+            List<ModerationExpectation> matchedExpectations;
+            JsonNode inputNode = rootNode.get("input");
+            if (inputNode != null && inputNode.isArray() && 
!inputNode.isEmpty() && inputNode.get(0).isObject()) {
+                matchedExpectations = 
List.of(findExpectationByMultiModalInput(inputNode));
+            } else {
+                List<String> inputs = extractInputs(rootNode);
+                matchedExpectations = new ArrayList<>(inputs.size());
+                for (String input : inputs) {
+                    matchedExpectations.add(findExpectationByInput(input));
+                }
             }
 
             return 
responseBuilder.createModerationResponse(matchedExpectations, requestedModel);
-        } catch (Exception e) {
+        } catch (Exception | AssertionError e) {
+            // a failed request assertion must answer the client, which 
otherwise waits for its read timeout
             String errorMessage = "Error processing moderation request: " + 
e.getMessage();
             LOG.error(errorMessage, e);
             return createErrorResponse(500, errorMessage, exchange);
@@ -97,6 +105,38 @@ public class ModerationRequestHandler {
         return inputs;
     }
 
+    /**
+     * The API scores a multi-modal input as a whole and replies with a single 
result, whatever the number of parts, so
+     * the whole array is matched against one expectation.
+     */
+    private ModerationExpectation findExpectationByMultiModalInput(JsonNode 
inputNode) {
+        List<String> texts = new ArrayList<>();
+        List<String> imageUrls = new ArrayList<>();
+        for (JsonNode part : inputNode) {
+            String type = part.path("type").asText();
+            if ("text".equals(type)) {
+                texts.add(part.path("text").asText());
+            } else if ("image_url".equals(type)) {
+                imageUrls.add(part.path("image_url").path("url").asText());
+            } else {
+                throw new IllegalArgumentException("Unsupported multi-modal 
moderation input type: " + type);
+            }
+        }
+
+        String text = texts.isEmpty() ? null : String.join("\n", texts);
+        ModerationExpectation expectation = expectations.stream()
+                .filter(candidate -> candidate.matchesMultiModal(text, 
!imageUrls.isEmpty()))
+                .findFirst()
+                .orElseThrow(() -> new IllegalStateException(
+                        String.format("No matching moderation expectation 
found for multi-modal input: text=%s, images=%d",
+                                text, imageUrls.size())));
+
+        if (expectation.getImageUrlAssertion() != null && 
!imageUrls.isEmpty()) {
+            expectation.getImageUrlAssertion().accept(imageUrls.get(0));
+        }
+        return expectation;
+    }
+
     private ModerationExpectation findExpectationByInput(String input) {
         return expectations.stream()
                 .filter(expectation -> expectation.matches(input))
@@ -106,10 +146,12 @@ public class ModerationRequestHandler {
     }
 
     private String createErrorResponse(int statusCode, String errorMessage, 
HttpExchange exchange) {
-        String jsonErrorMessage = String.format("{\"error\": {\"message\": 
\"%s\", \"type\": \"invalid_request_error\"}}",
-                errorMessage);
+        ObjectNode root = objectMapper.createObjectNode();
+        // assertion messages carry quotes and line breaks, so the payload is 
serialized rather than formatted
+        root.putObject("error").put("message", errorMessage).put("type", 
"invalid_request_error");
+        String jsonErrorMessage = root.toString();
         try {
-            exchange.sendResponseHeaders(statusCode, 
jsonErrorMessage.length());
+            exchange.sendResponseHeaders(statusCode, 
jsonErrorMessage.getBytes().length);
         } catch (IOException e) {
             throw new RuntimeException(e);
         }
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
index 37b225570d80..a22608b1a9e1 100644
--- 
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
@@ -46,6 +46,14 @@ public class ModerationResponseBuilder {
      */
     private static final List<String> ILLICIT_CATEGORIES = List.of("illicit", 
"illicit/violent");
 
+    /**
+     * The categories the {@code omni-moderation-*} models also score for 
image input.
+     */
+    private static final List<String> IMAGE_CATEGORIES = List.of(
+            "self-harm", "self-harm/instructions", "self-harm/intent",
+            "sexual",
+            "violence", "violence/graphic");
+
     private final ObjectMapper objectMapper;
 
     public ModerationResponseBuilder(ObjectMapper objectMapper) {
@@ -94,12 +102,22 @@ public class ModerationResponseBuilder {
     }
 
     /**
-     * The real API reports which input modality triggered each category. The 
mock only ever moderates text.
+     * The real API reports, per category, the input types the score was 
computed from. Images are only scored for a
+     * subset of the categories, so a category that does not support images 
reports an empty list for an image sent
+     * without text.
      */
     private Map<String, List<String>> 
categoryAppliedInputTypes(ModerationExpectation expectation) {
+        boolean text = !expectation.isImageExpected() || 
expectation.getExpectedInput() != null;
         Map<String, List<String>> appliedInputTypes = new LinkedHashMap<>();
         for (String category : categories(expectation).keySet()) {
-            appliedInputTypes.put(category, List.of("text"));
+            List<String> inputTypes = new ArrayList<>(2);
+            if (text) {
+                inputTypes.add("text");
+            }
+            if (expectation.isImageExpected() && 
IMAGE_CATEGORIES.contains(category)) {
+                inputTypes.add("image");
+            }
+            appliedInputTypes.put(category, inputTypes);
         }
         return appliedInputTypes;
     }
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 cc8ea0b0d99c..43d570f2f928 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
@@ -208,6 +208,34 @@ public class OpenAIMockBuilder {
         return this;
     }
 
+    /**
+     * Expects an image moderated on its own, sent as multi-modal input.
+     */
+    public OpenAIMockBuilder whenImageModeration() {
+        return whenImageModeration(null);
+    }
+
+    /**
+     * Expects an image moderated together with the given text, sent as one 
multi-modal input.
+     *
+     * @param expectedText the text part of the request, or {@code null} for 
an image sent without text
+     */
+    public OpenAIMockBuilder whenImageModeration(String expectedText) {
+        log.debug("Setting up image moderation expectation with text: {}", 
expectedText);
+        currentModerationExpectation = new ModerationExpectation(expectedText, 
true);
+        return this;
+    }
+
+    /**
+     * Asserts the {@code image_url} of the matching image moderation request, 
typically the data URL built from the
+     * message body.
+     */
+    public OpenAIMockBuilder assertModerationImageUrl(Consumer<String> 
imageUrlAssertion) {
+        validateCurrentModerationExpectation("assertModerationImageUrl()");
+        currentModerationExpectation.setImageUrlAssertion(imageUrlAssertion);
+        return this;
+    }
+
     /**
      * Replies with a verdict that violates no category.
      */

Reply via email to