CAMEL-23965: camel-openai - Add moderation operation - #25410
Conversation
Add an openai:moderation operation that checks text against the OpenAI usage policies through the SDK moderations API. It is meant as a pre-filter for untrusted input on public-facing routes, so policy-violating content can be rejected before spending chat tokens or triggering tool calls. The message body is passed through unchanged and the verdict is exposed as headers, which keeps the original content available to the rest of the route and allows content-based routing on CamelOpenAIModerationFlagged. A String body moderates one input, a List body moderates the whole batch in a single call; for a batch the flag is true when at least one input was flagged and the category headers hold one map per input. The model comes from the new moderationModel option (default omni-moderation-latest) or the CamelOpenAIModerationModel header, and the full SDK response is available as the CamelOpenAIModerationResponse exchange property when storeFullResponse is enabled. camel-test-infra-openai-mock gains support for the /moderations endpoint, with whenModeration/replyWithModerationAllowed/replyWithModerationFlagged builder methods, so the operation can be covered without calling the live API. Co-authored-by: Claude <noreply@anthropic.com>
Verified the operation against the live OpenAI moderation API and corrected what did not match. The mock now also returns category_applied_input_types, which every real response carries, so the stored SDK response is complete; the mock-based test asserts isValid() on it to keep the two from drifting apart. The tests used text-moderation-stable as an example model name, but the legacy text-moderation-* models no longer exist - the API rejects them - so they now use the omni-moderation models that are actually served. For the same reason the note about the optional illicit categories no longer refers to older OpenAI models; they are optional because an OpenAI-compatible provider may not implement them. Co-authored-by: Claude <noreply@anthropic.com>
Code review (Bugbot + Grok)AI-generated review on behalf of @atiaomar1978-hub Thanks for the thorough PR — the moderation operation design is solid and aligns well with the embeddings producer pattern. Mock infra, docs, and test breadth are strong. A few items to address before merge: Should fix1. Empty moderation results fail open (Bugbot — high) In Suggestion: Reject empty/mismatched 2. Singleton Header unwrapping is keyed on List<Map<String, Boolean>> categories =
exchange.getMessage().getHeader(..., List.class);That is a runtime footgun for generic batch processors. Suggestion: Unwrap based on original body type ( 3. Moderation stores the full SDK response in exchange property Test gaps
Nits / follow-ups
SecurityNo trust-boundary issues found — moderation input is route-author supplied (trusted), API key handling unchanged, no unsafe deserialization. VerdictRequest changes on items 1–3 and the test gaps above. Happy to re-review once CI is green. Overall this is a valuable addition for public-facing AI routes — nice work on the mock infra and the pass-through + header routing design. |
|
🌟 Thank you for your contribution to the Apache Camel project! 🌟 🐫 Apache Camel Committers, please review the following items:
|
Fail closed when no verdict comes back. CamelOpenAIModerationFlagged was derived with anyMatch over the results, so a provider returning fewer results than inputs - an empty list in the worst case - left the flag false and a guard route let the message through. A result count that does not match the input count now raises a CamelExchangeException. Shape the category headers after the body rather than the input count. A single element List used to produce Map headers, contradicting the documented batch contract and breaking generic batch processors. A List body now always yields List headers. Reject null elements in the input list instead of moderating the literal "null", and extend the storeFullResponse option description with the moderation property. Tests cover the missing verdict through both a plain route and the guard route, the single element List, a String body, an empty list and a list with a null element. The mock can now omit a result so the fail-closed path is actually exercised. Co-authored-by: Claude <noreply@anthropic.com>
|
Thanks @atiaomar1978-hub — a genuinely useful review. All three "should fix" items and the nits are addressed in 5cdf245 and 1c8641a. 1. Empty results fail open. You were right, and this was the serious one: for an operation whose only job is gating untrusted content, silently passing the message through is the worst possible failure mode. A result count that does not match the input count now raises a I did not take the "treat as flagged" option: marking content as violating when no verdict came back asserts something we do not know, and would show up as an unexplainable false positive. Failing the exchange states the truth — there is no verdict. To prove the behaviour rather than just claim it, the mock can now omit a result ( 2. Singleton 3. Nits.
Separately, I verified the operation against the live moderation API (5cdf245), which corrected two things a mock alone could never have caught:
The live run also confirmed the batch ordering, the 13 category names (independently cross-checked against the constants in the SDK deserialiser) and the guard route end to end. That test was local and temporary — it is not in this PR, because the component's ITs run against Ollama, which does not expose Current state: 16 test cases for the operation; Reported by Claude Code on behalf of Karol Krawczyk |
CI caught these as uncommitted generated changes: the endpoint DSL builders pick up the new moderation operation, the moderationModel option and the moderation headers, and the others navigation follows the renamed operations page title. Co-authored-by: Claude <noreply@anthropic.com>
Bugbot & Grok Review Report (re-review)AI-generated review on behalf of the operator. PR: CAMEL-23965 — Executive summaryThis PR adds a well-designed moderation gate: body passthrough, explicit category name mapping, aggregate 16 unit tests in Bugbot findingsMust-fixNone for merge-blocking correctness. Should-fix
Nice-to-have
Grok findingsDesign assessment
What looks good
Test coverage matrix (
|
| Scenario | Test |
|---|---|
| Allowed input | testAllowedInput |
| Flagged input | testFlaggedInput |
| Scored but not flagged | testScoredButNotFlaggedInput |
| Batch (2 inputs) | testBatchModeration |
storeFullResponse |
testStoreFullResponse |
| Model: default / option / header | testModelFromEndpointOptionAndHeader |
| Legacy model (no illicit) | testProviderWithoutIllicitCategories |
| Non-String body coercion | testNonStringBodyIsConverted |
| Single-element List → List headers | testSingleElementListKeepsTheBatchShape |
| String body → Map headers | testStringBodyKeepsTheSingleShape |
| Missing verdict fail-closed | testMissingVerdictFailsClosed |
| Guard route blocks missing verdict | testGuardRouteDoesNotLetContentThroughWithoutVerdict |
| Empty list | testEmptyListFails |
| Null list element | testListWithNullElementFails |
| Missing body | testMissingBodyFails |
| Guard route accepts/rejects | testGuardRouteRejectsFlaggedInput |
Recommended follow-up order
- Fix stale
@UriPathoperation description + regenerate catalog - Add 4.22 upgrade-guide entry for
camel-test-infra-openai-mockSPI break - Add security/limitations NOTE in moderation docs (text-only, not a trust-boundary guarantee)
- Consider set-after-validate for
storeFullResponse; stricter list element typing
Bottom line: Ready to approve once follow-ups are tracked. Strongest follow-ups are the stale operation metadata and the upgrade-guide note for the mock SPI break.
Summary
Adds an
openai:moderationoperation, as proposed in CAMEL-23965.Moderation is the canonical pre-filter for untrusted input on a public-facing route: policy-violating content can be rejected before spending chat tokens or triggering tool calls.
Stringbody moderates one input; aListbody moderates the whole batch in a single call.CamelOpenAIModerationFlaggedis always aBoolean— for a batch it istruewhen at least one input was flagged — so the samewhen(header(...).isEqualTo(true))works for both shapes. If the provider returns fewer results than inputs, the exchange fails with aCamelExchangeExceptionrather than leaving the flagfalseand letting the message through.CamelOpenAIModerationCategories/...CategoryScoresfollow the body shape: aMapfor aStringbody and aListof maps for aListbody, including a single-element list, so batch processors need no special case.moderationModeloption (defaultomni-moderation-latest, so the operation works with no configuration) or theCamelOpenAIModerationModelheader.storeFullResponse=truethe SDK response is stored in theCamelOpenAIModerationResponseexchange property. This is a dedicated property rather than the existingCamelOpenAIResponse, whose declaredjavaTypeisChatCompletion— same reasoning asCamelOpenAIResponsesResponse.Notes for reviewers
parent/pom.xmlis now on 4.49.0. The API used here was verified against the 4.49.0 artifact.illicitcategories:Moderation.Categories.illicit()/illicitViolent()areOptionalbecause only theomni-moderation-*models return them, while the same categories are plaindoubleinCategoryScores. The category map therefore omits them for legacy models. This is documented and covered by a test.hate/threatening,self-harm/intent, ...) rather than relying on SDK serialisation.CLAUDE.mdthe upgrade guide is for migration only.Compatibility note
camel-test-infra-openai-mockis a released artifact:OpenAIMockExpectationsgained a component and theOpenAIMockBuilderconstructor gained a parameter. Nothing in this repository constructs either directly, and the two consuming modules (camel-langchain4j-agent,camel-langchain4j-tools) use the fluent API and are covered by the verification below, but a downstream project constructing them directly needs a one-line update.Test infra
camel-test-infra-openai-mockgains support for the/moderationsendpoint (whenModeration(...)withreplyWithModerationAllowed(),replyWithModerationFlagged(category, score),replyWithModerationScore(...)andreplyWithoutIllicitCategories()), so the operation is covered without calling a live API. The mock now also echoes back the model from the request, as the real API does.Verification
mvn install -Psourcecheck -DskipITsoncamel-openai,camel-test-infra-openai-mock,camel-langchain4j-agentandcamel-langchain4j-tools: all green (196 + 49 + 42 tests, 0 failures). The two LangChain4j modules are included because they consume the mock, whose builder signature changed.storeFullResponse(asserting the stored response is usable, not just deserialisable), model resolution (default / endpoint option / header), a legacy model without theillicitcategories, a non-String body, a missing body, and the guard route from the issue./v1/moderations, so an IT in the module's existing style could not pass.Reported by Claude Code on behalf of Karol Krawczyk