Skip to content

CAMEL-23967: camel-openai - Add image generation and edit operations - #25489

Open
k-krawczyk wants to merge 1 commit into
apache:mainfrom
k-krawczyk:CAMEL-23967-image-operations
Open

CAMEL-23967: camel-openai - Add image generation and edit operations#25489
k-krawczyk wants to merge 1 commit into
apache:mainfrom
k-krawczyk:CAMEL-23967-image-operations

Conversation

@k-krawczyk

Copy link
Copy Markdown
Contributor

Description

Adds the image-generation and image-edit operations to camel-openai, closing CAMEL-23967.

// generate a product image and store it
from("direct:product-image")
    .setBody(simple("Studio photo of ${header.productName} on a white background"))
    .to("openai:image-generation?imageModel=gpt-image-1&imageSize=1024x1024")
    .to("file:target/images?fileName=${header.productName}.png");

// edit an image coming from object storage
from("aws2-s3:marketing-assets")
    .setHeader(OpenAIConstants.IMAGE_PROMPT, constant("Add a red SALE banner in the top-right corner"))
    .to("openai:image-edit?imageModel=gpt-image-1")
    .to("aws2-s3:marketing-assets-processed");

Two deviations from the issue

imageResponseFormat has no default and is only sent when set. The KDoc of ImageGenerateParams in the pinned SDK says the parameter is not supported by the GPT image models, which always return base64. Checking against the live API on 13 August 2026 turned out to be stricter still: POST /v1/images/generations answers 400 Unknown parameter: 'response_format' for every model, and /v1/models no longer lists any DALL-E model. The option is kept because OpenAI-compatible providers still implement the older images API, where url is often the default — but a default value here would break every route against OpenAI, so there is none.

The body shape follows the response, not the option. Base64 payloads are decoded into byte[], URLs stay as String. A single image becomes the body directly and several images become a List, so the common case does not force routes to unwrap a one-element list.

What it adds

  • imageModel, imagePrompt, imageSize, imageQuality, imageResponseFormat, imageCount, imageBackground, imageOutputFormat, imageOutputCompression, imageStyle, imageModeration, imageInputFidelity — each overridable per exchange by a header.
  • image-edit takes the image from the body as File, Path, InputStream, byte[], or a List of those, since the GPT image models accept up to 16 reference images. An optional mask goes through CamelOpenAIImageMask.
  • Content-Type is set from the output format reported by the response, falling back to the requested one, so the result chains into file: or object storage unchanged.
  • The multipart parts of an edit declare their content type. The API validates the upload on that content type rather than on the file name, and the SDK leaves it as text/plain unless it is set, so an edit is rejected without this. It is resolved from the usual MIME type detection, then from the extension of a File/Path body, and falls back to image/png for anything the API does not accept.
  • Revised prompt and token usage as headers; storeFullResponse=true keeps the full SDK response in CamelOpenAIImageResponse.

Left out, as the issue suggests: createVariation and the streaming variants.

Testing

camel-test-infra-openai-mock gained whenImageGeneration(), whenImageEdit(), replyWithImage(byte[]), replyWithImageUrl(String), withRevisedPrompt(), withImageOutputFormat(), withImageSize(), withImageUsage() and assertImageRequest(). Image edit requests are multipart and the mock does not parse multipart — the same simplification the transcription handler already makes — so those expectations are matched in declaration order and the raw body is exposed to assertions, which is enough to verify that several images and a mask reach the wire.

24 unit tests, no model and no GPU involved.

Beyond the mock, both operations were also exercised against the real OpenAI API once, which is what surfaced the multipart content type and the state of response_format. That smoke test is not part of the contribution — there is no CI-suitable local image backend, as the issue notes — but the multipart content type is now asserted in the mock tests.

Reported by Claude Code on behalf of Karol Krawczyk

Adds the image-generation and image-edit operations backed by the images
API of the openai-java SDK.

The multipart parts of an image edit declare their content type. The API
validates the upload on that content type, not on the file name, and the
SDK leaves it as text/plain unless it is set, so an edit would otherwise
be rejected. It is resolved from the usual MIME type detection, then from
the extension of a File or Path body, and falls back to image/png for
anything the API does not accept.

imageResponseFormat has no default and is only sent when set explicitly.
The GPT image models always return base64 and reject the parameter, and
the OpenAI images endpoint now rejects it for every model, the DALL-E
models that used to accept it no longer being offered. The option is kept
for OpenAI-compatible providers that still implement the older images API.
The body shape follows the response instead, decoding base64 into byte[]
and leaving URLs as String, with a single image as the body and several
images as a List.

The image-edit body accepts File, Path, InputStream, byte[] or a List of
those, since the GPT image models take up to 16 reference images, plus an
optional mask header.

camel-test-infra-openai-mock gains image generation and edit expectations.
Image edit requests are multipart and are not parsed by the mock, as is
already the case for transcription, so those expectations are matched in
declaration order and the raw body is exposed to assertions.

Co-authored-by: Claude <noreply@anthropic.com>

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work on this PR — the documentation, test coverage, and alignment with existing component patterns are all solid. Two minor suggestions below.

Note: this review covers project conventions and contribution expectations. It does not replace specialized review tools (CodeRabbit, SonarCloud, etc.).

This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.

private static final byte[] SOURCE_IMAGE = "FAKE-PNG-SOURCE".getBytes(StandardCharsets.UTF_8);
private static final byte[] SECOND_SOURCE_IMAGE = "FAKE-PNG-SOURCE-TWO".getBytes(StandardCharsets.UTF_8);
private static final byte[] MASK_IMAGE = "FAKE-PNG-MASK".getBytes(StandardCharsets.UTF_8);
private static final byte[] EDITED_IMAGE = "FAKE-PNG-EDITED".getBytes(StandardCharsets.UTF_8);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per the project's JUnit 5 convention, new test classes should be package-private (drop public). The newer tests in this component already follow this pattern.

Suggested change
private static final byte[] EDITED_IMAGE = "FAKE-PNG-EDITED".getBytes(StandardCharsets.UTF_8);
class OpenAIImageEditMockTest extends CamelTestSupport {


@RegisterExtension
public OpenAIMock openAIMock = new OpenAIMock().builder()
.whenImageGeneration("A red bicycle")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here — drop public to follow the JUnit 5 convention.

Suggested change
.whenImageGeneration("A red bicycle")
class OpenAIImageGenerationMockTest extends CamelTestSupport {

import org.slf4j.LoggerFactory;

/**
* Shared helpers for the {@code image-generation} and {@code image-edit} operations.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: resolveParameter() at the bottom of this class is byte-for-byte identical to OpenAIProducer.resolveParameter(). Consider extracting it to a shared location to avoid the duplication. Not blocking — just a follow-up suggestion.

@github-actions

Copy link
Copy Markdown
Contributor

🌟 Thank you for your contribution to the Apache Camel project! 🌟
🤖 CI automation will test this PR automatically.

🐫 Apache Camel Committers, please review the following items:

  • First-time contributors require MANUAL approval for the GitHub Actions to run
  • You can use the command /component-test (camel-)component-name1 (camel-)component-name2.. to request a test from the test bot although they are normally detected and executed by CI.
  • You can label PRs using skip-tests and test-dependents to fine-tune the checks executed by this PR.
  • Build and test logs are available in the summary page. Only Apache Camel committers have access to the summary.

⚠️ Be careful when sharing logs. Review their contents before sharing them publicly.

@github-actions

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

  • catalog/camel-catalog
  • components/camel-ai/camel-openai
  • docs
  • dsl/camel-endpointdsl
  • test-infra/camel-test-infra-openai-mock

🔬 Scalpel shadow comparison — Scalpel: 17 tested, 26 compile-only — current: 14 all tested

Maveniverse Scalpel detected 43 affected modules (current approach: 14).

⚠️ Modules only in Scalpel (29)
  • apache-camel
  • camel-allcomponents
  • camel-catalog
  • camel-catalog-console
  • camel-catalog-lucene
  • camel-catalog-maven
  • camel-catalog-suggest
  • camel-componentdsl
  • camel-csimple-maven-plugin
  • camel-endpointdsl
  • camel-endpointdsl-support
  • camel-itest
  • camel-jbang-core
  • camel-jbang-it
  • camel-jbang-main
  • camel-jbang-plugin-edit
  • camel-jbang-plugin-generate
  • camel-jbang-plugin-kubernetes
  • camel-jbang-plugin-test
  • camel-kamelet-main
  • camel-launcher
  • camel-report-maven-plugin
  • camel-route-parser
  • camel-yaml-dsl
  • camel-yaml-dsl-deserializers
  • camel-yaml-dsl-maven-plugin
  • coverage
  • docs
  • dummy-component

Skip-tests mode would test 17 modules (5 direct + 12 downstream), skip tests for 26 (generated code, meta-modules)

Modules Scalpel would test (17)
  • camel-catalog
  • camel-endpointdsl
  • camel-jbang-mcp
  • camel-jbang-plugin-mcp
  • camel-jbang-plugin-route-parser
  • camel-jbang-plugin-tui
  • camel-jbang-plugin-validate
  • camel-langchain4j-agent
  • camel-langchain4j-tools
  • camel-launcher-container
  • camel-mcp-server
  • camel-openai
  • camel-test-infra-all
  • camel-test-infra-openai-mock
  • camel-yaml-dsl-validator
  • camel-yaml-dsl-validator-maven-plugin
  • docs
Modules with tests skipped (26)
  • apache-camel
  • camel-allcomponents
  • camel-catalog-console
  • camel-catalog-lucene
  • camel-catalog-maven
  • camel-catalog-suggest
  • camel-componentdsl
  • camel-csimple-maven-plugin
  • camel-endpointdsl-support
  • camel-itest
  • camel-jbang-core
  • camel-jbang-it
  • camel-jbang-main
  • camel-jbang-plugin-edit
  • camel-jbang-plugin-generate
  • camel-jbang-plugin-kubernetes
  • camel-jbang-plugin-test
  • camel-kamelet-main
  • camel-launcher
  • camel-report-maven-plugin
  • camel-route-parser
  • camel-yaml-dsl
  • camel-yaml-dsl-deserializers
  • camel-yaml-dsl-maven-plugin
  • coverage
  • dummy-component

ℹ️ Shadow mode — Scalpel observes but does not affect test execution. Learn more

⚠️ Some tests are disabled on GitHub Actions (@DisabledIfSystemProperty(named = "ci.env.name")) and require manual verification:

  • components/camel-ai/camel-openai: 7 test(s) disabled on GitHub Actions
All tested modules (43 modules)
  • Camel :: AI :: LangChain4j :: Agent
  • Camel :: AI :: LangChain4j :: Tools (deprecated)
  • Camel :: AI :: MCP Server
  • Camel :: AI :: OpenAI
  • Camel :: All Components Sync point
  • Camel :: Assembly
  • Camel :: Catalog :: CSimple Maven Plugin (deprecated)
  • Camel :: Catalog :: Camel Catalog
  • Camel :: Catalog :: Camel Report Maven Plugin
  • Camel :: Catalog :: Camel Route Parser
  • Camel :: Catalog :: Console
  • Camel :: Catalog :: Dummy Component
  • Camel :: Catalog :: Lucene (deprecated)
  • Camel :: Catalog :: Maven
  • Camel :: Catalog :: Suggest
  • Camel :: Component DSL
  • Camel :: Coverage
  • Camel :: Docs
  • Camel :: Endpoint DSL
  • Camel :: Endpoint DSL :: Support
  • Camel :: Integration Tests
  • Camel :: JBang :: Core
  • Camel :: JBang :: Integration tests
  • Camel :: JBang :: MCP
  • Camel :: JBang :: Main
  • Camel :: JBang :: Plugin :: Edit
  • Camel :: JBang :: Plugin :: Generate
  • Camel :: JBang :: Plugin :: Kubernetes
  • Camel :: JBang :: Plugin :: MCP
  • Camel :: JBang :: Plugin :: Route Parser
  • Camel :: JBang :: Plugin :: TUI
  • Camel :: JBang :: Plugin :: Testing
  • Camel :: JBang :: Plugin :: Validate
  • Camel :: Kamelet Main
  • Camel :: Launcher
  • Camel :: Launcher :: Container
  • Camel :: Test Infra :: All test services
  • Camel :: Test Infra :: OpenAI Mock
  • Camel :: YAML DSL
  • Camel :: YAML DSL :: Deserializers
  • Camel :: YAML DSL :: Maven Plugins
  • Camel :: YAML DSL :: Validator
  • Camel :: YAML DSL :: Validator Maven Plugin

⚙️ View full build and test results

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants