This is an automated email from the ASF dual-hosted git repository.
Croway pushed a commit to branch camel-4.18.x
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-4.18.x by this push:
new 0f6476e73420 CAMEL-23739: camel-openai - support WrappedFile, byte[]
and InputStream bodies for vision models
0f6476e73420 is described below
commit 0f6476e734207ba9cc7a74c489c7ad2a12a8d129
Author: croway <[email protected]>
AuthorDate: Thu Jun 11 19:54:57 2026 +0200
CAMEL-23739: camel-openai - support WrappedFile, byte[] and InputStream
bodies for vision models
---
.../apache/camel/catalog/components/openai.json | 31 +--
components/camel-ai/camel-openai/pom.xml | 5 +
.../org/apache/camel/component/openai/openai.json | 31 +--
.../src/main/docs/openai-component.adoc | 49 ++++-
.../camel/component/openai/MimeTypeHelper.java | 119 ++++++++++
.../camel/component/openai/OpenAIConstants.java | 5 +
.../camel/component/openai/OpenAIProducer.java | 105 ++++++---
.../openai/OpenAIVisionBodyTypesMockTest.java | 243 +++++++++++++++++++++
.../endpoint/dsl/OpenAIEndpointBuilderFactory.java | 15 ++
9 files changed, 542 insertions(+), 61 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 ba3fbf984932..384b1446fa93 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
@@ -42,21 +42,22 @@
"CamelOpenAIStreaming": { "index": 7, "kind": "header", "displayName": "",
"group": "producer", "label": "", "required": false, "javaType": "Boolean",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "Whether to stream the response back incrementally",
"constantName": "org.apache.camel.component.openai.OpenAIConstants#STREAMING" },
"CamelOpenAIOutputClass": { "index": 8, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType": "Class",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The Java class to use for structured output parsing",
"constantName":
"org.apache.camel.component.openai.OpenAIConstants#OUTPUT_CLASS" },
"CamelOpenAIJsonSchema": { "index": 9, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType": "String",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The JSON schema to use for structured output
validation", "constantName":
"org.apache.camel.component.openai.OpenAIConstants#JSON_SCHEMA" },
- "CamelOpenAIResponseModel": { "index": 10, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The model used for the completion
response", "constantName":
"org.apache.camel.component.openai.OpenAIConstants#RESPONSE_MODEL" },
- "CamelOpenAIResponseId": { "index": 11, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType": "String",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The unique identifier for the completion response",
"constantName": "org.apache.camel.component.openai.OpenAIConstants#RESPONSE_ID"
},
- "CamelOpenAIFinishReason": { "index": 12, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType": "String",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The reason the completion finished (e.g., stop, length,
content_filter)", "constantName":
"org.apache.camel.component.openai.OpenAIConstants#FINISH_REASON" },
- "CamelOpenAIPromptTokens": { "index": 13, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType": "Integer",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The number of tokens used in the prompt",
"constantName":
"org.apache.camel.component.openai.OpenAIConstants#PROMPT_TOKENS" },
- "CamelOpenAICompletionTokens": { "index": 14, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The number of tokens used in the
completion", "constantName":
"org.apache.camel.component.openai.OpenAIConstants#COMPLETION_TOKENS" },
- "CamelOpenAITotalTokens": { "index": 15, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType": "Integer",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The total number of tokens used (prompt completion)",
"constantName":
"org.apache.camel.component.openai.OpenAIConstants#TOTAL_TOKENS" },
- "CamelOpenAIResponse": { "index": 16, "kind": "header", "displayName": "",
"group": "producer", "label": "", "required": false, "javaType":
"com.openai.models.ChatCompletion", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "The complete OpenAI
response object", "constantName":
"org.apache.camel.component.openai.OpenAIConstants#RESPONSE" },
- "CamelOpenAIEmbeddingModel": { "index": 17, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The model to use for embeddings",
"constantName":
"org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_MODEL" },
- "CamelOpenAIEmbeddingDimensions": { "index": 18, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Number of output dimensions",
"constantName":
"org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_DIMENSIONS" },
- "CamelOpenAIEmbeddingResponseModel": { "index": 19, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The embedding model used in the
response", "constantName":
"org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_RESPONSE_MODEL" },
- "CamelOpenAIEmbeddingCount": { "index": 20, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Number of embeddings returned",
"constantName":
"org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_COUNT" },
- "CamelOpenAIEmbeddingVectorSize": { "index": 21, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Vector dimensions of the embeddings",
"constantName":
"org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_VECTOR_SIZE" },
- "CamelOpenAIReferenceEmbedding": { "index": 22, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "List<Float>", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "Reference embedding vector
for similarity comparison", "constantName":
"org.apache.camel.component.openai.OpenAIConstants#REFERENCE_EMBEDDING" },
- "CamelOpenAISimilarityScore": { "index": 23, "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": 24, "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" }
+ "CamelOpenAIMediaType": { "index": 10, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType": "String",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The MIME type of the message body when sending a file or
binary content (File, WrappedFile, byte or InputStream) to the model. Takes
precedence over component content-type headers and automatic MIME type
detection", "constantName": "org.apa [...]
+ "CamelOpenAIResponseModel": { "index": 11, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The model used for the completion
response", "constantName":
"org.apache.camel.component.openai.OpenAIConstants#RESPONSE_MODEL" },
+ "CamelOpenAIResponseId": { "index": 12, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType": "String",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The unique identifier for the completion response",
"constantName": "org.apache.camel.component.openai.OpenAIConstants#RESPONSE_ID"
},
+ "CamelOpenAIFinishReason": { "index": 13, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType": "String",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The reason the completion finished (e.g., stop, length,
content_filter)", "constantName":
"org.apache.camel.component.openai.OpenAIConstants#FINISH_REASON" },
+ "CamelOpenAIPromptTokens": { "index": 14, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType": "Integer",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The number of tokens used in the prompt",
"constantName":
"org.apache.camel.component.openai.OpenAIConstants#PROMPT_TOKENS" },
+ "CamelOpenAICompletionTokens": { "index": 15, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The number of tokens used in the
completion", "constantName":
"org.apache.camel.component.openai.OpenAIConstants#COMPLETION_TOKENS" },
+ "CamelOpenAITotalTokens": { "index": 16, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType": "Integer",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The total number of tokens used (prompt completion)",
"constantName":
"org.apache.camel.component.openai.OpenAIConstants#TOTAL_TOKENS" },
+ "CamelOpenAIResponse": { "index": 17, "kind": "header", "displayName": "",
"group": "producer", "label": "", "required": false, "javaType":
"com.openai.models.ChatCompletion", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "The complete OpenAI
response object", "constantName":
"org.apache.camel.component.openai.OpenAIConstants#RESPONSE" },
+ "CamelOpenAIEmbeddingModel": { "index": 18, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The model to use for embeddings",
"constantName":
"org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_MODEL" },
+ "CamelOpenAIEmbeddingDimensions": { "index": 19, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Number of output dimensions",
"constantName":
"org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_DIMENSIONS" },
+ "CamelOpenAIEmbeddingResponseModel": { "index": 20, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The embedding model used in the
response", "constantName":
"org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_RESPONSE_MODEL" },
+ "CamelOpenAIEmbeddingCount": { "index": 21, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Number of embeddings returned",
"constantName":
"org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_COUNT" },
+ "CamelOpenAIEmbeddingVectorSize": { "index": 22, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Vector dimensions of the embeddings",
"constantName":
"org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_VECTOR_SIZE" },
+ "CamelOpenAIReferenceEmbedding": { "index": 23, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "List<Float>", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "Reference embedding vector
for similarity comparison", "constantName":
"org.apache.camel.component.openai.OpenAIConstants#REFERENCE_EMBEDDING" },
+ "CamelOpenAISimilarityScore": { "index": 24, "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": 25, "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" }
},
"properties": {
"operation": { "index": 0, "kind": "path", "displayName": "Operation",
"group": "producer", "label": "", "required": true, "type": "enum", "javaType":
"org.apache.camel.component.openai.OpenAIOperations", "enum": [
"chat-completion", "embeddings" ], "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "The operation to perform:
'chat-completion' or 'embeddings'" },
diff --git a/components/camel-ai/camel-openai/pom.xml
b/components/camel-ai/camel-openai/pom.xml
index 40a370122994..c9f7ff2d6113 100644
--- a/components/camel-ai/camel-openai/pom.xml
+++ b/components/camel-ai/camel-openai/pom.xml
@@ -52,6 +52,11 @@
<artifactId>camel-test-junit5</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-file</artifactId>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
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 ba3fbf984932..384b1446fa93 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
@@ -42,21 +42,22 @@
"CamelOpenAIStreaming": { "index": 7, "kind": "header", "displayName": "",
"group": "producer", "label": "", "required": false, "javaType": "Boolean",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "Whether to stream the response back incrementally",
"constantName": "org.apache.camel.component.openai.OpenAIConstants#STREAMING" },
"CamelOpenAIOutputClass": { "index": 8, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType": "Class",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The Java class to use for structured output parsing",
"constantName":
"org.apache.camel.component.openai.OpenAIConstants#OUTPUT_CLASS" },
"CamelOpenAIJsonSchema": { "index": 9, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType": "String",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The JSON schema to use for structured output
validation", "constantName":
"org.apache.camel.component.openai.OpenAIConstants#JSON_SCHEMA" },
- "CamelOpenAIResponseModel": { "index": 10, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The model used for the completion
response", "constantName":
"org.apache.camel.component.openai.OpenAIConstants#RESPONSE_MODEL" },
- "CamelOpenAIResponseId": { "index": 11, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType": "String",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The unique identifier for the completion response",
"constantName": "org.apache.camel.component.openai.OpenAIConstants#RESPONSE_ID"
},
- "CamelOpenAIFinishReason": { "index": 12, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType": "String",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The reason the completion finished (e.g., stop, length,
content_filter)", "constantName":
"org.apache.camel.component.openai.OpenAIConstants#FINISH_REASON" },
- "CamelOpenAIPromptTokens": { "index": 13, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType": "Integer",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The number of tokens used in the prompt",
"constantName":
"org.apache.camel.component.openai.OpenAIConstants#PROMPT_TOKENS" },
- "CamelOpenAICompletionTokens": { "index": 14, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The number of tokens used in the
completion", "constantName":
"org.apache.camel.component.openai.OpenAIConstants#COMPLETION_TOKENS" },
- "CamelOpenAITotalTokens": { "index": 15, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType": "Integer",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The total number of tokens used (prompt completion)",
"constantName":
"org.apache.camel.component.openai.OpenAIConstants#TOTAL_TOKENS" },
- "CamelOpenAIResponse": { "index": 16, "kind": "header", "displayName": "",
"group": "producer", "label": "", "required": false, "javaType":
"com.openai.models.ChatCompletion", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "The complete OpenAI
response object", "constantName":
"org.apache.camel.component.openai.OpenAIConstants#RESPONSE" },
- "CamelOpenAIEmbeddingModel": { "index": 17, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The model to use for embeddings",
"constantName":
"org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_MODEL" },
- "CamelOpenAIEmbeddingDimensions": { "index": 18, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Number of output dimensions",
"constantName":
"org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_DIMENSIONS" },
- "CamelOpenAIEmbeddingResponseModel": { "index": 19, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The embedding model used in the
response", "constantName":
"org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_RESPONSE_MODEL" },
- "CamelOpenAIEmbeddingCount": { "index": 20, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Number of embeddings returned",
"constantName":
"org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_COUNT" },
- "CamelOpenAIEmbeddingVectorSize": { "index": 21, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Vector dimensions of the embeddings",
"constantName":
"org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_VECTOR_SIZE" },
- "CamelOpenAIReferenceEmbedding": { "index": 22, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "List<Float>", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "Reference embedding vector
for similarity comparison", "constantName":
"org.apache.camel.component.openai.OpenAIConstants#REFERENCE_EMBEDDING" },
- "CamelOpenAISimilarityScore": { "index": 23, "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": 24, "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" }
+ "CamelOpenAIMediaType": { "index": 10, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType": "String",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The MIME type of the message body when sending a file or
binary content (File, WrappedFile, byte or InputStream) to the model. Takes
precedence over component content-type headers and automatic MIME type
detection", "constantName": "org.apa [...]
+ "CamelOpenAIResponseModel": { "index": 11, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The model used for the completion
response", "constantName":
"org.apache.camel.component.openai.OpenAIConstants#RESPONSE_MODEL" },
+ "CamelOpenAIResponseId": { "index": 12, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType": "String",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The unique identifier for the completion response",
"constantName": "org.apache.camel.component.openai.OpenAIConstants#RESPONSE_ID"
},
+ "CamelOpenAIFinishReason": { "index": 13, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType": "String",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The reason the completion finished (e.g., stop, length,
content_filter)", "constantName":
"org.apache.camel.component.openai.OpenAIConstants#FINISH_REASON" },
+ "CamelOpenAIPromptTokens": { "index": 14, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType": "Integer",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The number of tokens used in the prompt",
"constantName":
"org.apache.camel.component.openai.OpenAIConstants#PROMPT_TOKENS" },
+ "CamelOpenAICompletionTokens": { "index": 15, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The number of tokens used in the
completion", "constantName":
"org.apache.camel.component.openai.OpenAIConstants#COMPLETION_TOKENS" },
+ "CamelOpenAITotalTokens": { "index": 16, "kind": "header", "displayName":
"", "group": "producer", "label": "", "required": false, "javaType": "Integer",
"deprecated": false, "deprecationNote": "", "autowired": false, "secret":
false, "description": "The total number of tokens used (prompt completion)",
"constantName":
"org.apache.camel.component.openai.OpenAIConstants#TOTAL_TOKENS" },
+ "CamelOpenAIResponse": { "index": 17, "kind": "header", "displayName": "",
"group": "producer", "label": "", "required": false, "javaType":
"com.openai.models.ChatCompletion", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "The complete OpenAI
response object", "constantName":
"org.apache.camel.component.openai.OpenAIConstants#RESPONSE" },
+ "CamelOpenAIEmbeddingModel": { "index": 18, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The model to use for embeddings",
"constantName":
"org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_MODEL" },
+ "CamelOpenAIEmbeddingDimensions": { "index": 19, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Number of output dimensions",
"constantName":
"org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_DIMENSIONS" },
+ "CamelOpenAIEmbeddingResponseModel": { "index": 20, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "String", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "The embedding model used in the
response", "constantName":
"org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_RESPONSE_MODEL" },
+ "CamelOpenAIEmbeddingCount": { "index": 21, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Number of embeddings returned",
"constantName":
"org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_COUNT" },
+ "CamelOpenAIEmbeddingVectorSize": { "index": 22, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired":
false, "secret": false, "description": "Vector dimensions of the embeddings",
"constantName":
"org.apache.camel.component.openai.OpenAIConstants#EMBEDDING_VECTOR_SIZE" },
+ "CamelOpenAIReferenceEmbedding": { "index": 23, "kind": "header",
"displayName": "", "group": "producer", "label": "", "required": false,
"javaType": "List<Float>", "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "Reference embedding vector
for similarity comparison", "constantName":
"org.apache.camel.component.openai.OpenAIConstants#REFERENCE_EMBEDDING" },
+ "CamelOpenAISimilarityScore": { "index": 24, "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": 25, "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" }
},
"properties": {
"operation": { "index": 0, "kind": "path", "displayName": "Operation",
"group": "producer", "label": "", "required": true, "type": "enum", "javaType":
"org.apache.camel.component.openai.OpenAIOperations", "enum": [
"chat-completion", "embeddings" ], "deprecated": false, "deprecationNote": "",
"autowired": false, "secret": false, "description": "The operation to perform:
'chat-completion' or 'embeddings'" },
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 afe483f31765..e0613be791d7 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
@@ -113,13 +113,35 @@ from("file:prompts?noop=true")
[source,java]
----
from("file:images?noop=true")
- .to("openai:chat-completion?model=gpt-4.1-mini?userMessage=Describe what
you see in this image")
+ .to("openai:chat-completion?model=gpt-4.1-mini&userMessage=Describe what
you see in this image")
+ .log("Response: ${body}");
+----
+
+Image input also works with bodies produced by remote file and cloud storage
components, such as FTP/SFTP (`WrappedFile`), AWS S3, Azure Blob Storage or
MinIO (`byte[]` or `InputStream`). The MIME type is detected from the
component's content-type header or the file name:
+
+.Usage example:
+[source,java]
+----
+from("aws2-s3:my-bucket")
+ .to("openai:chat-completion?model=gpt-4.1-mini&userMessage=Describe what
you see in this image")
+ .log("Response: ${body}");
+----
+
+When no content-type header is available, set the `CamelOpenAIMediaType`
header explicitly:
+
+.Usage example:
+[source,java]
+----
+from("direct:image")
+ .setHeader("CamelOpenAIMediaType", constant("image/png"))
+ .setHeader("CamelOpenAIUserMessage", constant("Describe what you see in
this image"))
+ .to("openai:chat-completion?model=gpt-4.1-mini")
.log("Response: ${body}");
----
[NOTE]
====
-When using image files, the userMessage is required. Supported image formats
are detected by MIME type (e.g., `image/png`, `image/jpeg`, `image/gif`,
`image/webp`).
+When using image input, the userMessage is required. Supported image formats
are detected by MIME type (e.g., `image/png`, `image/jpeg`, `image/gif`,
`image/webp`).
====
=== Streaming Response
@@ -238,13 +260,30 @@ from("direct:local")
The component accepts the following types of input in the message body:
1. *String*: The prompt text is taken directly from the body
-2. *File*: Used for file-based prompts. The component handles two types of
files:
- * *Text files* (MIME type starting with `text/`): The file content is read
and used as the prompt. If userMessage endpoint option or
`CamelOpenAIUserMessage` is set, it overrides the file content
+2. *File*, *Path* or *WrappedFile* (the body type produced by the file, FTP,
SFTP and SMB components): Used for file-based prompts. The component handles
two types of files:
+ * *Text files* (MIME type starting with `text/`, plus `application/xml` and
`application/json`): The file content is read and used as the prompt. If
userMessage endpoint option or `CamelOpenAIUserMessage` is set, it overrides
the file content
* *Image files* (MIME type starting with `image/`): The file is encoded as
a base64 data URL and sent to vision-capable models. The userMessage is
**required** when using image files
+3. *byte[]* or *InputStream* (the body types produced by cloud storage
components such as AWS S3, Azure Blob Storage, Google Cloud Storage and MinIO):
When the detected MIME type is an image, the content is encoded as a base64
data URL and sent to vision-capable models (userMessage is **required**).
Otherwise, the content is converted to a String and used as the prompt
+
+=== MIME Type Detection
+
+For `File`, `Path` and locally backed `WrappedFile` bodies, the MIME type is
resolved in the following order:
+
+1. The `CamelOpenAIMediaType` header
+2. The `CamelFileContentType` header
+3. The file name extension, using the Camel built-in MIME type table (e.g.,
`.png`, `.jpg`, `.gif`, `.webp`, `.txt`, `.csv`, `.md`, `.xml`, `.json`)
+
+For `byte[]`, `InputStream` and remote `WrappedFile` bodies, the MIME type is
resolved in the following order:
+
+1. The `CamelOpenAIMediaType` header
+2. Cloud storage content-type headers: `CamelAwsS3ContentType`,
`CamelAzureStorageBlobContentType`, `CamelAzureStorageDataLakeContentType`,
`CamelGoogleCloudStorageContentType`, `CamelMinioContentType`,
`CamelIBMCOSContentType`
+3. The `Content-Type` header
+4. The `CamelFileContentType` header
+5. The extension of the file name in the `CamelFileName` header
[NOTE]
====
-When using `File` input, the component uses `Files.probeContentType()` to
detect the file type. Ensure your system has proper MIME type detection
configured.
+Set the `CamelOpenAIMediaType` header to override the MIME type detection, for
example when the payload has no content-type metadata or the detection picks
the wrong type.
====
== Output Handling
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
new file mode 100644
index 000000000000..2d404f4d9b6e
--- /dev/null
+++
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/MimeTypeHelper.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.openai;
+
+import java.io.File;
+import java.util.Locale;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.Message;
+
+/**
+ * Resolves the MIME type of file and binary message bodies sent to
vision-capable models.
+ */
+final class MimeTypeHelper {
+
+ /**
+ * Content-type headers set by file-based and cloud storage components,
checked in order.
+ */
+ private static final String[] CONTENT_TYPE_HEADERS = {
+ "CamelAwsS3ContentType", // AWS S3
+ "CamelAzureStorageBlobContentType", // Azure Blob Storage
+ "CamelAzureStorageDataLakeContentType", // Azure Data Lake Storage
+ "CamelGoogleCloudStorageContentType", // Google Cloud Storage
+ "CamelMinioContentType", // Minio (S3-compatible)
+ "CamelIBMCOSContentType" // IBM Cloud Object Storage
+ };
+
+ private MimeTypeHelper() {
+ }
+
+ /**
+ * Resolves the MIME type of a local file. Priority: the {@code
CamelOpenAIMediaType} header, the
+ * {@code CamelFileContentType} header, and finally the file name
extension.
+ */
+ static String resolveForFile(Message in, File file) {
+ String mime = headerMimeType(in, OpenAIConstants.MEDIA_TYPE);
+ if (mime == null) {
+ mime = headerMimeType(in, Exchange.FILE_CONTENT_TYPE);
+ }
+ if (mime == null) {
+ mime = fromFileName(file.getName());
+ }
+ return mime;
+ }
+
+ /**
+ * Resolves the MIME type of a binary body (byte[], InputStream or a
remote WrappedFile) where no local file is
+ * available. Priority: the {@code CamelOpenAIMediaType} header, cloud
storage content-type headers,
+ * {@code Content-Type}, {@code CamelFileContentType}, and finally the
extension of the {@code CamelFileName}
+ * header.
+ */
+ static String resolveForBinary(Message in) {
+ String mime = headerMimeType(in, OpenAIConstants.MEDIA_TYPE);
+ for (int i = 0; mime == null && i < CONTENT_TYPE_HEADERS.length; i++) {
+ mime = headerMimeType(in, CONTENT_TYPE_HEADERS[i]);
+ }
+ if (mime == null) {
+ mime = headerMimeType(in, Exchange.CONTENT_TYPE);
+ }
+ if (mime == null) {
+ mime = headerMimeType(in, Exchange.FILE_CONTENT_TYPE);
+ }
+ if (mime == null) {
+ String fileName = in.getHeader(Exchange.FILE_NAME, String.class);
+ if (fileName != null) {
+ mime = fromFileName(fileName);
+ }
+ }
+ return mime;
+ }
+
+ static boolean isImage(String mime) {
+ return mime != null && mime.startsWith("image/");
+ }
+
+ static boolean isText(String mime) {
+ if (mime == null) {
+ return false;
+ }
+ // XML and JSON are textual formats usable as prompt text, but map to
application/* MIME types
+ return mime.startsWith("text/") || "application/xml".equals(mime) ||
"application/json".equals(mime);
+ }
+
+ private static String headerMimeType(Message in, String header) {
+ String value = in.getHeader(header, String.class);
+ if (value == null || value.isBlank()) {
+ return null;
+ }
+ // strip parameters such as "; charset=utf-8"
+ int semicolon = value.indexOf(';');
+ return semicolon > 0 ? value.substring(0, semicolon).trim() :
value.trim();
+ }
+
+ private 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
+ String name = fileName.toLowerCase(Locale.ROOT);
+ if (name.endsWith(".md") || name.endsWith(".markdown")) {
+ mime = "text/markdown";
+ }
+ }
+ return mime;
+ }
+}
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 8e40ef78908e..090d58ccab9c 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
@@ -46,6 +46,11 @@ public final class OpenAIConstants {
public static final String OUTPUT_CLASS = "CamelOpenAIOutputClass";
@Metadata(description = "The JSON schema to use for structured output
validation", javaType = "String")
public static final String JSON_SCHEMA = "CamelOpenAIJsonSchema";
+ @Metadata(description = "The MIME type of the message body when sending a
file or binary content (File, WrappedFile, "
+ + "byte[] or InputStream) to the model. Takes
precedence over component content-type headers "
+ + "and automatic MIME type detection",
+ javaType = "String")
+ public static final String MEDIA_TYPE = "CamelOpenAIMediaType";
// Output Headers
@Metadata(description = "The model used for the completion response",
javaType = "String")
diff --git
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java
index 3aa022c5b295..48ce7a57d55b 100644
---
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java
+++
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java
@@ -17,8 +17,8 @@
package org.apache.camel.component.openai;
import java.io.File;
+import java.io.IOException;
import java.io.InputStream;
-import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
@@ -46,6 +46,7 @@ import com.openai.models.completions.CompletionUsage;
import org.apache.camel.AsyncCallback;
import org.apache.camel.Exchange;
import org.apache.camel.Message;
+import org.apache.camel.WrappedFile;
import org.apache.camel.spi.Synchronization;
import org.apache.camel.support.DefaultAsyncProducer;
import org.apache.camel.support.ResourceHelper;
@@ -244,8 +245,10 @@ public class OpenAIProducer extends DefaultAsyncProducer {
userPrompt = config.getUserMessage();
}
- if (body instanceof File) {
+ if (body instanceof WrappedFile || body instanceof File || body
instanceof Path) {
return buildFileMessage(in, userPrompt, config);
+ } else if (body instanceof byte[] || body instanceof InputStream) {
+ return buildBinaryMessage(in, userPrompt, config);
} else {
return buildTextMessage(in, userPrompt, config);
}
@@ -261,15 +264,28 @@ public class OpenAIProducer extends DefaultAsyncProducer {
private ChatCompletionMessageParam buildFileMessage(Message in, String
userPrompt, OpenAIConfiguration config)
throws Exception {
- File inputFile = in.getBody(File.class);
- Path path = inputFile.toPath();
- String mime = Files.probeContentType(path);
-
- if (mime != null && mime.startsWith("text/")) {
+ Object body = in.getBody();
+ File inputFile = null;
+ if (body instanceof WrappedFile<?> wrappedFile &&
wrappedFile.getFile() instanceof File file) {
+ // local file-based components (camel-file) expose the underlying
java.io.File
+ inputFile = file;
+ } else if (body instanceof File file) {
+ inputFile = file;
+ } else if (body instanceof Path path) {
+ inputFile = path.toFile();
+ }
+
+ // for remote file-based components (FTP, SFTP, ...) there is no local
java.io.File, so the
+ // MIME type is detected from headers and the file name only, before
reading any content
+ String mime = inputFile != null
+ ? MimeTypeHelper.resolveForFile(in, inputFile) :
MimeTypeHelper.resolveForBinary(in);
+
+ if (MimeTypeHelper.isText(mime)) {
// Handle text files - read content and use buildTextMessage logic
String prompt = userPrompt;
if (prompt == null || prompt.isEmpty()) {
- prompt = new String(Files.readAllBytes(path),
StandardCharsets.UTF_8);
+ // the type converter reads the content honoring the charset
configured on file-based endpoints
+ prompt = in.getBody(String.class);
}
if (prompt == null || prompt.isEmpty()) {
@@ -277,25 +293,48 @@ public class OpenAIProducer extends DefaultAsyncProducer {
"File content or user message configuration must
contain the prompt text");
}
return createTextMessage(prompt);
- } else if (mime != null && mime.startsWith("image/")) {
- // Handle image files - require user prompt and combine with image
- if (userPrompt == null || userPrompt.isEmpty()) {
- throw new IllegalArgumentException("User message configuration
must be set when using image File body");
- }
+ } else if (MimeTypeHelper.isImage(mime)) {
+ byte[] image = inputFile != null ?
Files.readAllBytes(inputFile.toPath()) : readBodyBytes(in);
+ return createImageMessage(image, mime, userPrompt);
+ } else {
+ throw unsupportedMimeType(mime,
+ inputFile != null ? inputFile.getName() :
in.getHeader(Exchange.FILE_NAME, String.class));
+ }
+ }
- ChatCompletionContentPart imageContentPart =
createImageContentPart(inputFile, mime);
- ChatCompletionContentPart textContentPart =
createTextContentPart(userPrompt);
+ private ChatCompletionMessageParam buildBinaryMessage(Message in, String
userPrompt, OpenAIConfiguration config)
+ throws Exception {
+ String mime = MimeTypeHelper.resolveForBinary(in);
+ if (MimeTypeHelper.isImage(mime)) {
+ return createImageMessage(readBodyBytes(in), mime, userPrompt);
+ }
+ // not an image: keep the previous behavior and treat the payload as
text
+ return buildTextMessage(in, userPrompt, config);
+ }
- return ChatCompletionMessageParam.ofUser(
- ChatCompletionUserMessageParam.builder()
-
.content(ChatCompletionUserMessageParam.Content.ofArrayOfContentParts(
- List.of(textContentPart,
imageContentPart)))
- .build());
- } else {
- throw new IllegalArgumentException("Only text and image files are
supported");
+ private byte[] readBodyBytes(Message in) throws IOException {
+ Object body = in.getBody();
+ if (body instanceof byte[] bytes) {
+ return bytes;
+ }
+ InputStream is = in.getBody(InputStream.class);
+ if (is == null) {
+ throw new IllegalArgumentException(
+ "Cannot read message body as InputStream: " + (body !=
null ? body.getClass().getName() : "null"));
+ }
+ try (is) {
+ return is.readAllBytes();
}
}
+ private IllegalArgumentException unsupportedMimeType(String mime, String
fileName) {
+ return new IllegalArgumentException(
+ "Only text and image files are supported. Detected MIME type:
" + mime
+ + (fileName != null ? " for file:
" + fileName : "")
+ + ". Set the " +
OpenAIConstants.MEDIA_TYPE
+ + " header to override the MIME
type detection");
+ }
+
private ChatCompletionMessageParam createTextMessage(String prompt) {
return ChatCompletionMessageParam.ofUser(
ChatCompletionUserMessageParam.builder()
@@ -317,10 +356,24 @@ public class OpenAIProducer extends DefaultAsyncProducer {
.build());
}
- private ChatCompletionContentPart createImageContentPart(File inputFile,
String mime) throws Exception {
- Path path = inputFile.toPath();
- byte[] img = Files.readAllBytes(path);
- String dataUrl = "data:" + mime + ";base64," +
Base64.getEncoder().encodeToString(img);
+ private ChatCompletionMessageParam createImageMessage(byte[] image, String
mime, String userPrompt) {
+ // image input requires a user prompt to combine with the image
+ if (userPrompt == null || userPrompt.isEmpty()) {
+ throw new IllegalArgumentException("User message configuration
must be set when using an image body");
+ }
+
+ ChatCompletionContentPart imageContentPart =
createImageContentPart(image, mime);
+ ChatCompletionContentPart textContentPart =
createTextContentPart(userPrompt);
+
+ return ChatCompletionMessageParam.ofUser(
+ ChatCompletionUserMessageParam.builder()
+
.content(ChatCompletionUserMessageParam.Content.ofArrayOfContentParts(
+ List.of(textContentPart, imageContentPart)))
+ .build());
+ }
+
+ private ChatCompletionContentPart createImageContentPart(byte[] image,
String mime) {
+ String dataUrl = "data:" + mime + ";base64," +
Base64.getEncoder().encodeToString(image);
return ChatCompletionContentPart.ofImageUrl(
ChatCompletionContentPartImage.builder()
diff --git
a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIVisionBodyTypesMockTest.java
b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIVisionBodyTypesMockTest.java
new file mode 100644
index 000000000000..10a4f286b573
--- /dev/null
+++
b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIVisionBodyTypesMockTest.java
@@ -0,0 +1,243 @@
+/*
+ * 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.IOException;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Base64;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.infra.openai.mock.OpenAIMock;
+import org.apache.camel.test.junit5.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests vision model input with the different body types produced by
file-based and cloud storage components:
+ * WrappedFile/GenericFile, byte[] and InputStream (CAMEL-23739).
+ */
+public class OpenAIVisionBodyTypesMockTest extends CamelTestSupport {
+
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+ private static final byte[] PNG_BYTES = {
+ (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, (byte)
0xFF, 0x00, 0x01, 0x02 };
+ private static final byte[] JPEG_BYTES = {
+ (byte) 0xFF, (byte) 0xD8, (byte) 0xFF, (byte) 0xE0, (byte) 0xCA,
(byte) 0xFE, 0x00, 0x42 };
+
+ private final Path imagesDir = createTempDir();
+
+ @RegisterExtension
+ public OpenAIMock openAIMock = new OpenAIMock().builder()
+ .when("describe-png")
+ .assertRequest(request -> assertImageDataUrl(request, "image/png",
PNG_BYTES))
+ .replyWith("png response")
+ .end()
+ .when("describe-jpeg")
+ .assertRequest(request -> assertImageDataUrl(request,
"image/jpeg", JPEG_BYTES))
+ .replyWith("jpeg response")
+ .end()
+ .when("describe-webp")
+ .assertRequest(request -> assertImageDataUrl(request,
"image/webp", PNG_BYTES))
+ .replyWith("webp response")
+ .end()
+ .when("describe-avif")
+ .assertRequest(request -> assertImageDataUrl(request,
"image/avif", PNG_BYTES))
+ .replyWith("avif response")
+ .end()
+ .when("hello bytes")
+ .replyWith("text response")
+ .end()
+ .when("prompt from file")
+ .replyWith("file text response")
+ .end()
+ .when("<note>xml prompt</note>")
+ .replyWith("xml text response")
+ .end()
+ .build();
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:chat")
+
.to("openai:chat-completion?model=gpt-5&apiKey=dummy&baseUrl=" +
openAIMock.getBaseUrl()
+ + "/v1");
+
+ // consumes GenericFile bodies, the typical vision use case
from the file component
+ from("file:" + imagesDir + "?noop=true&initialDelay=0")
+
.to("openai:chat-completion?model=gpt-5&apiKey=dummy&userMessage=describe-png&baseUrl="
+ + openAIMock.getBaseUrl() + "/v1")
+ .to("mock:fileResult");
+ }
+ };
+ }
+
+ @Test
+ void genericFileBodyFromFileConsumerIsSentAsImage() throws Exception {
+ MockEndpoint mock = getMockEndpoint("mock:fileResult");
+ mock.expectedBodiesReceived("png response");
+
+ Files.write(imagesDir.resolve("picture.png"), PNG_BYTES);
+
+ mock.assertIsSatisfied();
+ }
+
+ @Test
+ void byteArrayBodyWithCloudContentTypeHeaderIsSentAsImage() {
+ Exchange result = template.request("direct:chat", e -> {
+ e.getIn().setBody(PNG_BYTES);
+ e.getIn().setHeader("CamelAwsS3ContentType", "image/png");
+ e.getIn().setHeader(OpenAIConstants.USER_MESSAGE, "describe-png");
+ });
+ assertEquals("png response",
result.getMessage().getBody(String.class));
+ }
+
+ @Test
+ void inputStreamBodyWithContentTypeHeaderIsSentAsImage() {
+ Exchange result = template.request("direct:chat", e -> {
+ e.getIn().setBody(new ByteArrayInputStream(JPEG_BYTES));
+ e.getIn().setHeader(Exchange.CONTENT_TYPE, "image/jpeg");
+ e.getIn().setHeader(OpenAIConstants.USER_MESSAGE, "describe-jpeg");
+ });
+ assertEquals("jpeg response",
result.getMessage().getBody(String.class));
+ }
+
+ @Test
+ void mediaTypeHeaderOverridesOtherContentTypeHeaders() {
+ Exchange result = template.request("direct:chat", e -> {
+ e.getIn().setBody(PNG_BYTES);
+ e.getIn().setHeader(Exchange.CONTENT_TYPE,
"application/octet-stream");
+ e.getIn().setHeader(OpenAIConstants.MEDIA_TYPE, "image/webp");
+ e.getIn().setHeader(OpenAIConstants.USER_MESSAGE, "describe-webp");
+ });
+ assertEquals("webp response",
result.getMessage().getBody(String.class));
+ }
+
+ @Test
+ void byteArrayBodyWithFileNameHeaderUsesExtensionDetection() {
+ Exchange result = template.request("direct:chat", e -> {
+ e.getIn().setBody(PNG_BYTES);
+ e.getIn().setHeader(Exchange.FILE_NAME, "photo.png");
+ e.getIn().setHeader(OpenAIConstants.USER_MESSAGE, "describe-png");
+ });
+ assertEquals("png response",
result.getMessage().getBody(String.class));
+ }
+
+ @Test
+ void byteArrayBodyWithoutMimeInfoIsTreatedAsText() {
+ Exchange result = template.request("direct:chat",
+ e -> e.getIn().setBody("hello
bytes".getBytes(StandardCharsets.UTF_8)));
+ assertEquals("text response",
result.getMessage().getBody(String.class));
+ }
+
+ @Test
+ void textFileBodyContentIsUsedAsPrompt() throws Exception {
+ Path textFile = Files.createTempFile("camel-openai-prompt", ".txt");
+ Files.writeString(textFile, "prompt from file");
+
+ Exchange result = template.request("direct:chat", e ->
e.getIn().setBody(textFile.toFile()));
+ assertEquals("file text response",
result.getMessage().getBody(String.class));
+ }
+
+ @Test
+ void avifFileNameExtensionIsDetectedFromMimeTypeTable() {
+ Exchange result = template.request("direct:chat", e -> {
+ e.getIn().setBody(PNG_BYTES);
+ e.getIn().setHeader(Exchange.FILE_NAME, "photo.avif");
+ e.getIn().setHeader(OpenAIConstants.USER_MESSAGE, "describe-avif");
+ });
+ assertEquals("avif response",
result.getMessage().getBody(String.class));
+ }
+
+ @Test
+ void xmlFileBodyContentIsUsedAsPrompt() throws Exception {
+ Path xmlFile = Files.createTempFile("camel-openai-prompt", ".xml");
+ Files.writeString(xmlFile, "<note>xml prompt</note>");
+
+ Exchange result = template.request("direct:chat", e ->
e.getIn().setBody(xmlFile.toFile()));
+ assertEquals("xml text response",
result.getMessage().getBody(String.class));
+ }
+
+ @Test
+ void imageBodyWithoutUserMessageFails() {
+ Exchange result = template.request("direct:chat", e -> {
+ e.getIn().setBody(PNG_BYTES);
+ e.getIn().setHeader(Exchange.CONTENT_TYPE, "image/png");
+ });
+ Exception exception = result.getException();
+ assertInstanceOf(IllegalArgumentException.class, exception);
+ assertTrue(exception.getMessage().contains("User message"));
+ }
+
+ @Test
+ void unsupportedFileTypeFails() throws Exception {
+ Path binFile = Files.createTempFile("camel-openai-unsupported",
".bin");
+ Files.write(binFile, PNG_BYTES);
+
+ Exchange result = template.request("direct:chat", e -> {
+ e.getIn().setBody(binFile.toFile());
+ e.getIn().setHeader(OpenAIConstants.USER_MESSAGE, "describe-png");
+ });
+ Exception exception = result.getException();
+ assertInstanceOf(IllegalArgumentException.class, exception);
+ assertTrue(exception.getMessage().contains("Only text and image files
are supported"));
+ }
+
+ private static void assertImageDataUrl(String request, String
expectedMime, byte[] expectedBytes) {
+ try {
+ JsonNode root = OBJECT_MAPPER.readTree(request);
+ JsonNode messages = root.path("messages");
+ JsonNode lastMessage = messages.get(messages.size() - 1);
+ String url = null;
+ for (JsonNode part : lastMessage.path("content")) {
+ if ("image_url".equals(part.path("type").asText())) {
+ url = part.path("image_url").path("url").asText();
+ }
+ }
+ assertNotNull(url, "Expected an image_url content part in the
request");
+ String prefix = "data:" + expectedMime + ";base64,";
+ assertTrue(url.startsWith(prefix), "Expected data URL with MIME
type " + expectedMime);
+ assertArrayEquals(expectedBytes,
Base64.getDecoder().decode(url.substring(prefix.length())));
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private static Path createTempDir() {
+ try {
+ return Files.createTempDirectory("camel-openai-vision-test");
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+}
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 3809ee39d3c2..a133e09f7937 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
@@ -715,6 +715,21 @@ public interface OpenAIEndpointBuilderFactory {
public String openAIJsonSchema() {
return "CamelOpenAIJsonSchema";
}
+ /**
+ * The MIME type of the message body when sending a file or binary
+ * content (File, WrappedFile, byte or InputStream) to the model. Takes
+ * precedence over component content-type headers and automatic MIME
+ * type detection.
+ *
+ * The option is a: {@code String} type.
+ *
+ * Group: producer
+ *
+ * @return the name of the header {@code OpenAIMediaType}.
+ */
+ public String openAIMediaType() {
+ return "CamelOpenAIMediaType";
+ }
/**
* The model used for the completion response.
*