Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

*{component-header}*

The OpenAI component provides integration with OpenAI and OpenAI-compatible APIs for chat completion, text embeddings, audio transcription, audio translation, and text-to-speech using the official openai-java SDK.
The OpenAI component provides integration with OpenAI and OpenAI-compatible APIs for chat completion, text embeddings, content moderation, audio transcription, audio translation, and text-to-speech using the official openai-java SDK.

Maven users will need to add the following dependency to their `pom.xml` for this component:

Expand Down Expand Up @@ -47,6 +47,7 @@ See xref:others:openai-responses.adoc[Responses API operation] for usage (`previ
* `audio-transcription` - Transcribe audio files to text using speech-to-text models (e.g., Whisper, GPT-4o Transcribe)
* `audio-translation` - Transcribe and translate audio files into English text (e.g., Whisper)
* `audio-speech` - Synthesize spoken audio from text using text-to-speech models (e.g., gpt-4o-mini-tts, tts-1)
* `moderation` - Check text against the OpenAI usage policies before it reaches a model

// component options: START
include::partial$component-configure-options.adoc[]
Expand Down Expand Up @@ -1254,17 +1255,20 @@ For more details on specific features, see:
* xref:others:openai-responses.adoc[Responses API operation] - OpenAI Responses API, hosted tools, and server-side conversation state
* xref:others:openai-mcp.adoc[MCP Tool Calling] - Model Context Protocol server configuration, agentic loop, streaming, and connection recovery
* xref:others:openai-providers.adoc[OpenAI-Compatible Providers] - Using Ollama, LM Studio, vLLM, and OpenRouter as alternative backends
* xref:others:openai-operations.adoc[Embeddings and Audio Operations] - Text embeddings, vector database integration, and audio transcription
* xref:others:openai-operations.adoc[Embeddings, Moderation and Audio Operations] - Text embeddings, vector database integration, content moderation, and audio transcription

== Error Handling

The component may throw the following exceptions:

* `IllegalArgumentException`:
** When an invalid operation is specified (supported: `chat-completion`, `embeddings`, `tool-execution`, `audio-transcription`, `audio-translation`, `audio-speech`)
** When an invalid operation is specified (supported: `chat-completion`, `responses`, `embeddings`, `tool-execution`, `audio-transcription`, `audio-translation`, `audio-speech`, `moderation`)
** When message body or user message is missing
** When the audio model is missing (audio-transcription, audio-translation) or the speech model is missing (audio-speech)
** When image file is provided without userMessage (chat-completion)
** When unsupported file type is provided (only text and image files are supported)
** When invalid JSON schema string is provided
** When the moderation input list is empty or contains null elements (moderation)
* `CamelExchangeException`:
** When moderation returns a number of results that does not match the number of inputs (moderation)
* API-specific exceptions from the OpenAI SDK for network errors, authentication failures, rate limiting, etc.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
= OpenAI - Embeddings and Audio Operations
= OpenAI - Embeddings, Moderation and Audio Operations
:tabs-sync-option:

xref:ROOT:openai-component.adoc[Back to OpenAI Component]
Expand Down Expand Up @@ -168,6 +168,131 @@ The following headers are set after an embeddings request:
| `CamelOpenAISimilarityScore` | Double | Cosine similarity (if reference embedding provided)
|===

== Moderation Operation

The `moderation` operation checks text against the OpenAI usage policies. It is the canonical pre-filter for untrusted
input on a public-facing route: rejecting policy-violating content before spending chat tokens or triggering tool calls.

The message body is passed through unchanged and the verdict is exposed as headers, so the result can be used for
content-based routing while the original content stays available to the rest of the route.

IMPORTANT: Moderation is a policy filter, not a trust boundary. The verdict is probabilistic and its categories are
defined by the provider, so it is not a substitute for authentication, authorization, schema validation or defences
against prompt injection. The operation only reports a verdict — a flagged body keeps flowing unless the route stops
or replaces it, as in the example below.

NOTE: The operation moderates text only. The body, or each element of a list body, is converted to a `String` and sent
as text input; the multi-modal inputs of the moderation API are not exposed.

=== Guarding a Route

[tabs]
====
Java::
+
[source,java]
----
from("platform-http:/chat")
.to("openai:moderation?moderationModel=omni-moderation-latest")
.choice()
.when(header(OpenAIConstants.MODERATION_FLAGGED).isEqualTo(true))
.setBody(constant("Your message violates our usage policy."))
.otherwise()
.to("openai:chat-completion?model=gpt-5")
.end();
----

YAML::
+
[source,yaml]
----
- from:
uri: platform-http:/chat
steps:
- to: openai:moderation?moderationModel=omni-moderation-latest
- choice:
when:
- simple: "${header.CamelOpenAIModerationFlagged} == true"
steps:
- setBody:
constant: "Your message violates our usage policy."
otherwise:
steps:
- to: openai:chat-completion?model=gpt-5
----
====

=== Verdicts per Input

`CamelOpenAIModerationResults` always holds one verdict per moderated input, in the order of the inputs. Each entry is
a map with the keys `input`, `flagged`, `categories` and `categoryScores`, which makes a batch straightforward to split
and route per item:

[source,java]
----
from("direct:moderate-batch")
.to("openai:moderation")
.split(header(OpenAIConstants.MODERATION_RESULTS))
.choice()
.when(simple("${body[flagged]}"))
.to("direct:quarantine")
.otherwise()
.to("direct:downstream")
.end();
----

A `List` body moderates every element in a single API call, and `CamelOpenAIModerationFlagged` is then `true` when at
least one element was flagged.

For a single input, the same categories are also exposed as plain maps in `CamelOpenAIModerationCategories` and
`CamelOpenAIModerationCategoryScores`, which is convenient for acting on one category directly — for example routing
anything the model is fairly confident about to human review:

[source,java]
----
from("direct:moderate")
.to("openai:moderation")
.choice()
.when(simple("${header.CamelOpenAIModerationCategoryScores[hate]} > 0.85"))
.to("direct:human-review")
.otherwise()
.to("direct:downstream")
.end();
----

Those two headers are not set for a list body, where `CamelOpenAIModerationResults` carries the verdicts.

=== Failure Modes

The operation is meant to gate untrusted content, so it fails the exchange rather than letting a message through
without a verdict:

* the API returning a number of results that does not match the number of inputs raises a `CamelExchangeException`,
instead of leaving `CamelOpenAIModerationFlagged` as `false`;
* a missing body, an empty list, or a list containing `null` elements raises an `IllegalArgumentException`.

=== Moderation Output Headers

The following headers are set after a moderation request:

[cols="1,1,3"]
|===
| Header | Type | Description

| `CamelOpenAIModerationFlagged` | Boolean | Whether the input violates the usage policies. For a batch, `true` when at least one input was flagged
| `CamelOpenAIModerationResults` | List | One verdict per input, in input order. Each entry holds `input`, `flagged`, `categories` and `categoryScores`
| `CamelOpenAIModerationCategories` | Map | Category name to violation flag, for a single input. Not set for a list body
| `CamelOpenAIModerationCategoryScores` | Map | Category name to confidence score, for a single input. Not set for a list body
| `CamelOpenAIModerationResponseModel` | String | The model used for moderation
|===

The category names are the ones returned by the API, for example `hate`, `hate/threatening`, `self-harm/intent`,
`sexual/minors` and `violence/graphic`.

NOTE: The `illicit` and `illicit/violent` categories are optional in the API model. OpenAI returns them, but an
xref:others:openai-providers.adoc[OpenAI-compatible provider] may not, in which case they are absent from the
category map. The score map always contains every category.

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.

would it be possible to provide an example (and a test) that show how the header CamelOpenAIModerationCategoryScores can be used? for example:

simple("${header.CamelOpenAIModerationCategoryScores[hate]} > 0.85")

I do think this use case is interesting for the users

== Audio Transcription Operation

The `audio-transcription` operation transcribes audio files to text using OpenAI's speech-to-text models (Whisper, GPT-4o Transcribe).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ public boolean configure(CamelContext camelContext, Object obj, String name, Obj
case "mcptoolrefresh":
case "mcpToolRefresh": target.getConfiguration().setMcpToolRefresh(property(camelContext, boolean.class, value)); return true;
case "model": target.getConfiguration().setModel(property(camelContext, java.lang.String.class, value)); return true;
case "moderationmodel":
case "moderationModel": target.getConfiguration().setModerationModel(property(camelContext, java.lang.String.class, value)); return true;
case "oauthprofile":
case "oauthProfile": target.getConfiguration().setOauthProfile(property(camelContext, java.lang.String.class, value)); return true;
case "outputclass":
Expand Down Expand Up @@ -230,6 +232,8 @@ public Class<?> getOptionType(String name, boolean ignoreCase) {
case "mcptoolrefresh":
case "mcpToolRefresh": return boolean.class;
case "model": return java.lang.String.class;
case "moderationmodel":
case "moderationModel": return java.lang.String.class;
case "oauthprofile":
case "oauthProfile": return java.lang.String.class;
case "outputclass":
Expand Down Expand Up @@ -368,6 +372,8 @@ public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
case "mcptoolrefresh":
case "mcpToolRefresh": return target.getConfiguration().isMcpToolRefresh();
case "model": return target.getConfiguration().getModel();
case "moderationmodel":
case "moderationModel": return target.getConfiguration().getModerationModel();
case "oauthprofile":
case "oauthProfile": return target.getConfiguration().getOauthProfile();
case "outputclass":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class OpenAIEndpointUriFactory extends org.apache.camel.support.component
private static final Set<String> ENDPOINT_IDENTITY_PROPERTY_NAMES;
private static final Map<String, String> MULTI_VALUE_PREFIXES;
static {
Set<String> props = new HashSet<>(68);
Set<String> props = new HashSet<>(69);
props.add("additionalBodyProperty");
props.add("additionalHeader");
props.add("additionalResponseHeader");
Expand Down Expand Up @@ -61,6 +61,7 @@ public class OpenAIEndpointUriFactory extends org.apache.camel.support.component
props.add("mcpTimeout");
props.add("mcpToolRefresh");
props.add("model");
props.add("moderationModel");
props.add("oauthProfile");
props.add("operation");
props.add("outputClass");
Expand Down
Loading