Skip to content

CAMEL-24538: camel-openai - fix operator precedence so an empty userMessage does not drop the body prompt - #25939

Merged
davsclaus merged 2 commits into
apache:mainfrom
oscerd:fix/CAMEL-24538
Aug 31, 2026
Merged

CAMEL-24538: camel-openai - fix operator precedence so an empty userMessage does not drop the body prompt#25939
davsclaus merged 2 commits into
apache:mainfrom
oscerd:fix/CAMEL-24538

Conversation

@oscerd

@oscerd oscerd commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Issue

CAMEL-24538

Problem

OpenAIProducer.buildUserMessage resolves the user prompt with:

if (userPrompt == null || userPrompt.isEmpty() && ObjectHelper.isNotEmpty(config.getUserMessage())) {
    userPrompt = config.getUserMessage();
}

&& binds tighter than ||, so this parses as userPrompt == null || (userPrompt.isEmpty() && configHasMessage).
When the CamelOpenAIUserMessage header is absent (userPrompt == null) and the configured userMessage
option is an empty string, the first branch is already true, so userPrompt is set to that empty
string. buildTextMessage then does userPrompt != null ? userPrompt : body, picks the empty string over
the message body, and the request fails with "No input provided to LLM" — the body prompt is silently
dropped.

Fix

Parenthesise as (userPrompt == null || userPrompt.isEmpty()) && ObjectHelper.isNotEmpty(config.getUserMessage())
so the configured message is only substituted when it is actually set; otherwise userPrompt stays null and
the body is used.

The same precedence appears in two more places in buildMessages(), and both are corrected here:

  • systemPrompt
  • developerPrompt — added after review feedback from @davsclaus

Neither of those two is observable today: their downstream ObjectHelper.isNotEmpty(...) guard drops a null
before the message is added. They are fixed because leaving one of three identical instances behind would be
an oversight, not a decision.

Testing

  • New OpenAIEmptyUserMessageBodyPromptTest configures the endpoint with an empty userMessage and asserts
    the body prompt still reaches the model. Verified it fails with "No input provided" against the
    unpatched code and passes with the fix.
  • mvn -Psourcecheck validate green.
  • Full reactor build from the root (mvn clean install -DskipTests) green after the developerPrompt change,
    with no regenerated artifacts left uncommitted.

Claude Code on behalf of oscerd

…essage does not drop the body prompt

buildUserMessage used `userPrompt == null || userPrompt.isEmpty() && isNotEmpty(config.getUserMessage())`.
Because && binds tighter than ||, this is `userPrompt == null || (userPrompt.isEmpty() && ...)`, so when
the USER_MESSAGE header is absent and the configured userMessage is an empty string, userPrompt was set to
that empty string and buildTextMessage then used "" instead of the message body, failing with
"No input provided". Parenthesize as `(userPrompt == null || userPrompt.isEmpty()) && isNotEmpty(...)` so
the configured message is only used when it is actually set, otherwise the body is used. The same
precedence is corrected for the (benign) system-message twin for consistency.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Andrea Cosentino <ancosen@gmail.com>
@oscerd oscerd added the bug Something isn't working label Aug 31, 2026
@oscerd oscerd added this to the 4.23.0 milestone Aug 31, 2026
@oscerd
oscerd requested review from Croway and davsclaus August 31, 2026 08:18
@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.

@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.

Review

Verified beyond the diff:

  • Checked out the PR branch and ran OpenAIEmptyUserMessageBodyPromptTest — passes.
  • Reverted just the userPrompt precedence fix locally and re-ran the same test — it fails with exactly the reported symptom (IllegalArgumentException: No input provided to LLM), confirming the test genuinely exercises the bug rather than passing vacuously. Restored the fix afterward.
  • Confirmed JIRA CAMEL-24538 is Bug, In Progress, correctly assigned to the author.
  • Checked git blame on OpenAIProducer.java — the buggy precedence has no intentional design history; it's leftover boilerplate from several unrelated feature commits, not a deliberate choice, so this fix doesn't revert anything intentional.
  • Confirmed the systemPrompt companion fix is behavior-neutral today (masked by the downstream ObjectHelper.isNotEmpty(systemPrompt) guard before the message is added) — the PR's own reasoning for touching it "for consistency" checks out.

Suggestion (non-blocking)

The identical precedence bug still exists a few lines below the systemPrompt fix, in the developerPrompt branch (outside this diff's hunk, so I can't anchor an inline suggestion to it):

// components/camel-ai/camel-openai/.../OpenAIProducer.java, buildMessages()
if (developerPrompt == null
        || developerPrompt.isEmpty() && ObjectHelper.isNotEmpty(config.getDeveloperMessage())) {
    developerPrompt = config.getDeveloperMessage();
}

should become:

if ((developerPrompt == null || developerPrompt.isEmpty()) && ObjectHelper.isNotEmpty(config.getDeveloperMessage())) {
    developerPrompt = config.getDeveloperMessage();
}

It's currently harmless (masked by the downstream ObjectHelper.isNotEmpty(developerPrompt) guard before the message is added), same as the systemPrompt case this PR already fixed "for consistency." Since two of three identical instances are being cleaned up here, it seems like an oversight to leave this third one — worth completing in the same pass, or in a fast follow-up.

Good fix, well tested, no blocking concerns.

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

@Croway

Croway commented Aug 31, 2026

Copy link
Copy Markdown
Contributor
There are uncommitted changes
HEAD detached at pull/25939/merge
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-agent-component.adoc
	modified:   catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-embeddings-component.adoc
	modified:   catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spring-ai-chat-component.adoc

no changes added to commit (use "git add" and/or "git commit -a")


diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-agent-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-agent-component.adoc
index a331982d11df..e70651121988 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-agent-component.adoc
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-agent-component.adoc
@@ -1178,7 +1178,6 @@ You can also define the `ResponseFormat` at the `ChatModel` level. See the https
 * The same schema file can be shared across `camel-openai` and `camel-langchain4j-agent` components
 ====
 
-[[structured_error_exchange_properties]]
 === Structured error exchange properties
 
 When a LangChain4j agent call fails, Camel sets structured metadata on the exchange **before** the model exception propagates. This works even when GenAI observability is disabled.
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-embeddings-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-embeddings-component.adoc
index 387d9e382f24..7ea8ec94431c 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-embeddings-component.adoc
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/langchain4j-embeddings-component.adoc
@@ -395,7 +395,6 @@ YAML::
 ----
 ====
 
-[[structured_error_exchange_properties]]
 === Structured error exchange properties
 
 When a LangChain4j embeddings call fails, Camel sets structured metadata on the exchange **before** the model exception propagates. This works even when GenAI observability is disabled.
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spring-ai-chat-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spring-ai-chat-component.adoc
index c8d8c0a84040..2dea4dd89a09 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spring-ai-chat-component.adoc
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spring-ai-chat-component.adoc
@@ -1407,7 +1407,6 @@ The component automatically adds Spring AI's `SimpleLoggerAdvisor` to log reques
 logging.level.org.springframework.ai.chat.client.advisor=DEBUG
 ----
 
-[[structured_error_exchange_properties]]
 === Structured error exchange properties
 
 When a Spring AI chat call fails, Camel sets structured metadata on the exchange **before** the Spring AI exception propagates. This works even when GenAI observability is disabled.

Spotted by Claus Ibsen on the PR. The developerPrompt guard had the same
missing parentheses as userPrompt and systemPrompt:

    if (developerPrompt == null
            || developerPrompt.isEmpty() && ObjectHelper.isNotEmpty(config.getDeveloperMessage()))

A null header therefore assigned the configured message unconditionally instead
of only when one is configured. Harmless today, since the downstream
ObjectHelper.isNotEmpty(developerPrompt) guard drops a null before the message is
added, but leaving one of three identical instances unfixed was an oversight.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@oscerd

oscerd commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for checking the test actually fails without the fix — that is the part I would have wanted verified too.

Taken: the developerPrompt branch now reads

if ((developerPrompt == null || developerPrompt.isEmpty())
        && ObjectHelper.isNotEmpty(config.getDeveloperMessage())) {
    developerPrompt = config.getDeveloperMessage();
}

You are right that it is harmless today, for the same reason systemPrompt was: the downstream ObjectHelper.isNotEmpty(developerPrompt) guard drops a null before the message is added. Leaving one of three identical instances behind was an oversight rather than a decision, so it belongs in this pass.

Built the module and ran the OpenAI tests, plus a full reactor build from the root, both clean. The three catalog doc files that the CI bot flagged as uncommitted are the structured_error_exchange_properties anchor drift on main, which b46db0bad3f1 (#25941) regenerated at 10:25 today — after that CI run — so a re-run should be clean.

Claude Code on behalf of oscerd

@gnodet gnodet 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.

Clean, correct bug fix. The operator precedence issue is real — Java's && binds tighter than ||, so the original a == null || a.isEmpty() && b evaluates as a == null || (a.isEmpty() && b) instead of the intended (a == null || a.isEmpty()) && b. The parenthesisation fix is applied consistently to all three affected branches (systemPrompt, developerPrompt, userPrompt), and the new test properly exercises the primary user-visible symptom.

Good investigation noting that the systemPrompt/developerPrompt instances are currently masked by downstream ObjectHelper.isNotEmpty() guards, but fixing them is the right call — leaving one of three identical instances unfixed would be an oversight.

🔀 Backport Status

⚠️ This bug fix targets main but no backport PR was found for:

  • camel-4.18.x — the component exists on this branch and has the same bug

Consider creating a backport PR or adding a backport-to-camel-4.18.x label.

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

Claude Code on behalf of Guillaume Nodet

@github-actions

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

  • components/camel-ai/camel-openai

🔬 Scalpel shadow comparison — Scalpel: 10 tested, 27 compile-only — current: 10 all tested

Maveniverse Scalpel detected 37 affected modules (current approach: 10).

⚠️ Modules only in Scalpel (27)
  • apache-camel
  • camel-allcomponents
  • camel-catalog
  • camel-catalog-console
  • camel-catalog-maven
  • camel-catalog-suggest
  • camel-componentdsl
  • 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 10 modules (1 direct + 9 downstream), skip tests for 27 (generated code, meta-modules)

Modules Scalpel would test (10)
  • camel-jbang-mcp
  • camel-jbang-plugin-mcp
  • camel-jbang-plugin-route-parser
  • camel-jbang-plugin-tui
  • camel-jbang-plugin-validate
  • camel-launcher-container
  • camel-mcp-server
  • camel-openai
  • camel-yaml-dsl-validator
  • camel-yaml-dsl-validator-maven-plugin
Modules with tests skipped (27)
  • apache-camel
  • camel-allcomponents
  • camel-catalog
  • camel-catalog-console
  • camel-catalog-maven
  • camel-catalog-suggest
  • camel-componentdsl
  • 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

ℹ️ 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 (37 modules)
  • Camel :: AI :: MCP Server
  • Camel :: AI :: OpenAI
  • Camel :: All Components Sync point
  • Camel :: Assembly
  • Camel :: Catalog :: Camel Catalog
  • Camel :: Catalog :: Camel Report Maven Plugin
  • Camel :: Catalog :: Camel Route Parser
  • Camel :: Catalog :: Console
  • Camel :: Catalog :: Dummy Component
  • 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 :: 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

@davsclaus
davsclaus merged commit f8c89e9 into apache:main Aug 31, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working components components-ai

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants