You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/09/05 08:43:27 UTC
(camel) branch main updated: CAMEL-24559: Extend GenAI observability to OpenAI embeddings, moderation, and responses
This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 220e23242dec CAMEL-24559: Extend GenAI observability to OpenAI embeddings, moderation, and responses
220e23242dec is described below
commit 220e23242dec21f30feb4c2afb8dd72c5fc5b9c3
Author: Omar Atie <at...@gmail.com>
AuthorDate: Sat Sep 5 01:43:21 2026 -0700
CAMEL-24559: Extend GenAI observability to OpenAI embeddings, moderation, and responses
Extends the existing GenAiObservability instrumentation in camel-openai
to cover three additional producer operations beyond chat-completion:
embeddings, moderation, and responses. Each producer wraps its SDK call
with the try/recordSuccess/recordError/close observation pattern from the
established OpenAIProducer. Token counts are passed as Long fields
(avoiding Math.toIntExact overflow). openai:responses maps to
gen_ai.operation.name=chat per the OpenTelemetry OpenAI semantic
conventions; openai:moderation uses a new MODERATION operation name as a
Camel extension for content-policy operations. Shared test infrastructure
and three new observability test classes are added. Documentation in
ai-observability.adoc, the catalog mirror, and the 4.23 upgrade guide are
updated.
Closes #26106
Co-authored-by: Cursor Agent <no...@cursor.com>
---
.../camel/catalog/docs/ai-observability.adoc | 3 +
.../ai/observability/GenAiOperationName.java | 7 +-
.../src/main/docs/ai-observability.adoc | 3 +
.../component/openai/OpenAIEmbeddingsProducer.java | 34 +++++-
.../component/openai/OpenAIModerationProducer.java | 26 ++++-
.../component/openai/OpenAIResponsesProducer.java | 63 +++++++++-
.../openai/OpenAIEmbeddingsObservabilityTest.java | 92 +++++++++++++++
.../openai/OpenAIModerationObservabilityTest.java | 76 ++++++++++++
.../openai/OpenAIObservabilityTestSupport.java | 129 +++++++++++++++++++++
.../openai/OpenAIResponsesObservabilityTest.java | 75 ++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 4 +
11 files changed, 497 insertions(+), 15 deletions(-)
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ai-observability.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ai-observability.adoc
index fd1965df7ce1..a8111bea5d93 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ai-observability.adoc
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ai-observability.adoc
@@ -19,6 +19,9 @@ When `camel-opentelemetry2` and/or `camel-micrometer` is on the classpath, langc
producers emit child spans and metrics per LLM call with attributes such as `gen_ai.operation.name`, `gen_ai.system`,
`gen_ai.request.model`, `gen_ai.usage.input_tokens`, and `gen_ai.usage.output_tokens`.
+OpenAI coverage includes `chat-completion` (including streaming), `responses`, `embeddings`, and `moderation`.
+Each maps to a `gen_ai.operation.name` span attribute (`chat`, `embeddings`, `moderation`; moderation has no token usage).
+
Global toggle: set `camel.aiObservability.enabled=false` (default is `true` when a tracing or metrics backend is present). Camel Main exposes the same setting
via `camel.aiObservability.enabled` in application.properties or programmatically with
`main.configure().aiObservability().withEnabled(false)`.
diff --git a/components/camel-ai/camel-ai-observability-api/src/main/java/org/apache/camel/component/ai/observability/GenAiOperationName.java b/components/camel-ai/camel-ai-observability-api/src/main/java/org/apache/camel/component/ai/observability/GenAiOperationName.java
index b6334b6c510c..09c9b763ff5e 100644
--- a/components/camel-ai/camel-ai-observability-api/src/main/java/org/apache/camel/component/ai/observability/GenAiOperationName.java
+++ b/components/camel-ai/camel-ai-observability-api/src/main/java/org/apache/camel/component/ai/observability/GenAiOperationName.java
@@ -17,12 +17,15 @@
package org.apache.camel.component.ai.observability;
/**
- * GenAI operation names from the OpenTelemetry semantic conventions.
+ * GenAI operation names aligned with the OpenTelemetry GenAI semantic conventions where applicable. Custom values (for
+ * example {@link #MODERATION}) are Camel extensions for operations not yet defined in the stable OTel subset.
*/
public enum GenAiOperationName {
CHAT("chat"),
EMBEDDINGS("embeddings"),
- GENERATE_CONTENT("generate_content");
+ GENERATE_CONTENT("generate_content"),
+ /** OpenAI moderation and similar content-policy checks. */
+ MODERATION("moderation");
private final String value;
diff --git a/components/camel-ai/camel-ai-observability/src/main/docs/ai-observability.adoc b/components/camel-ai/camel-ai-observability/src/main/docs/ai-observability.adoc
index fd1965df7ce1..a8111bea5d93 100644
--- a/components/camel-ai/camel-ai-observability/src/main/docs/ai-observability.adoc
+++ b/components/camel-ai/camel-ai-observability/src/main/docs/ai-observability.adoc
@@ -19,6 +19,9 @@ When `camel-opentelemetry2` and/or `camel-micrometer` is on the classpath, langc
producers emit child spans and metrics per LLM call with attributes such as `gen_ai.operation.name`, `gen_ai.system`,
`gen_ai.request.model`, `gen_ai.usage.input_tokens`, and `gen_ai.usage.output_tokens`.
+OpenAI coverage includes `chat-completion` (including streaming), `responses`, `embeddings`, and `moderation`.
+Each maps to a `gen_ai.operation.name` span attribute (`chat`, `embeddings`, `moderation`; moderation has no token usage).
+
Global toggle: set `camel.aiObservability.enabled=false` (default is `true` when a tracing or metrics backend is present). Camel Main exposes the same setting
via `camel.aiObservability.enabled` in application.properties or programmatically with
`main.configure().aiObservability().withEnabled(false)`.
diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIEmbeddingsProducer.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIEmbeddingsProducer.java
index 4c3377fb777c..5c97728c2fb1 100644
--- a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIEmbeddingsProducer.java
+++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIEmbeddingsProducer.java
@@ -29,6 +29,12 @@ import com.openai.models.embeddings.EmbeddingCreateParams.EncodingFormat;
import org.apache.camel.AsyncCallback;
import org.apache.camel.Exchange;
import org.apache.camel.Message;
+import org.apache.camel.component.ai.observability.GenAiErrorSupport;
+import org.apache.camel.component.ai.observability.GenAiObservability;
+import org.apache.camel.component.ai.observability.GenAiObservation;
+import org.apache.camel.component.ai.observability.GenAiObservationContext;
+import org.apache.camel.component.ai.observability.GenAiOperationName;
+import org.apache.camel.component.ai.observability.GenAiUsage;
import org.apache.camel.support.DefaultAsyncProducer;
import org.apache.camel.util.ObjectHelper;
@@ -99,21 +105,39 @@ public class OpenAIEmbeddingsProducer extends DefaultAsyncProducer {
EmbeddingCreateParams params = paramsBuilder.build();
- // Execute request
- CreateEmbeddingResponse response = getEndpoint().getClient()
- .embeddings().create(params);
+ GenAiObservationContext observationContext = GenAiObservationContext.builder()
+ .operationName(GenAiOperationName.EMBEDDINGS)
+ .system("openai")
+ .requestModel(model)
+ .componentScheme("openai")
+ .build();
+ GenAiObservation observation = GenAiObservability.start(exchange, observationContext);
+ CreateEmbeddingResponse response;
+ try {
+ response = getEndpoint().getClient().embeddings().create(params);
+ var usage = response.usage();
+ observation.recordSuccess(GenAiUsage.of(
+ usage != null ? usage.promptTokens() : null,
+ null,
+ null,
+ response.model()));
+ } catch (Exception e) {
+ GenAiErrorSupport.apply(exchange, e);
+ observation.recordError(e);
+ throw e;
+ } finally {
+ observation.close();
+ }
if (config.isStoreFullResponse()) {
exchange.setProperty(OpenAIConstants.RESPONSE, response);
}
- // Extract embeddings
List<List<Float>> embeddings = new ArrayList<>();
for (Embedding embedding : response.data()) {
embeddings.add(embedding.embedding());
}
- // Set output body (metadata is exposed via headers)
Message out = exchange.getMessage();
if (inputs.size() == 1) {
out.setBody(embeddings.isEmpty() ? List.of() : embeddings.get(0));
diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIModerationProducer.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIModerationProducer.java
index 8f53713facc8..1c1b9d24b97d 100644
--- a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIModerationProducer.java
+++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIModerationProducer.java
@@ -28,6 +28,12 @@ import org.apache.camel.AsyncCallback;
import org.apache.camel.CamelExchangeException;
import org.apache.camel.Exchange;
import org.apache.camel.Message;
+import org.apache.camel.component.ai.observability.GenAiErrorSupport;
+import org.apache.camel.component.ai.observability.GenAiObservability;
+import org.apache.camel.component.ai.observability.GenAiObservation;
+import org.apache.camel.component.ai.observability.GenAiObservationContext;
+import org.apache.camel.component.ai.observability.GenAiOperationName;
+import org.apache.camel.component.ai.observability.GenAiUsage;
import org.apache.camel.support.DefaultAsyncProducer;
import org.apache.camel.util.ObjectHelper;
@@ -87,8 +93,24 @@ public class OpenAIModerationProducer extends DefaultAsyncProducer {
paramsBuilder.inputOfStrings(inputs);
}
- ModerationCreateResponse response = getEndpoint().getClient()
- .moderations().create(paramsBuilder.build());
+ GenAiObservationContext observationContext = GenAiObservationContext.builder()
+ .operationName(GenAiOperationName.MODERATION)
+ .system("openai")
+ .requestModel(model)
+ .componentScheme("openai")
+ .build();
+ GenAiObservation observation = GenAiObservability.start(exchange, observationContext);
+ ModerationCreateResponse response;
+ try {
+ response = getEndpoint().getClient().moderations().create(paramsBuilder.build());
+ observation.recordSuccess(GenAiUsage.of((Long) null, null, null, response.model()));
+ } catch (Exception e) {
+ GenAiErrorSupport.apply(exchange, e);
+ observation.recordError(e);
+ throw e;
+ } finally {
+ observation.close();
+ }
// this operation is used to gate untrusted content, so a missing verdict must fail the exchange
// instead of leaving CamelOpenAIModerationFlagged false and letting the message through
diff --git a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIResponsesProducer.java b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIResponsesProducer.java
index a39af6b4b070..d85fe7f43b9c 100644
--- a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIResponsesProducer.java
+++ b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIResponsesProducer.java
@@ -20,14 +20,20 @@ import java.io.InputStream;
import com.openai.models.responses.Response;
import com.openai.models.responses.ResponseCreateParams;
-import com.openai.models.responses.StructuredResponse;
import com.openai.models.responses.StructuredResponseCreateParams;
import org.apache.camel.AsyncCallback;
import org.apache.camel.Exchange;
import org.apache.camel.Message;
+import org.apache.camel.component.ai.observability.GenAiErrorSupport;
+import org.apache.camel.component.ai.observability.GenAiObservability;
+import org.apache.camel.component.ai.observability.GenAiObservation;
+import org.apache.camel.component.ai.observability.GenAiObservationContext;
+import org.apache.camel.component.ai.observability.GenAiOperationName;
+import org.apache.camel.component.ai.observability.GenAiUsage;
import org.apache.camel.support.DefaultAsyncProducer;
import org.apache.camel.support.ResourceHelper;
import org.apache.camel.util.ObjectHelper;
+import org.apache.camel.util.function.ThrowingSupplier;
/**
* OpenAI producer for the Responses API (non-streaming).
@@ -136,7 +142,7 @@ public class OpenAIResponsesProducer extends DefaultAsyncProducer {
Class<?> responseClass = resolveOutputClass(in, outputClass);
if (responseClass != null) {
- processStructured(exchange, config, paramsBuilder, responseClass);
+ processStructured(exchange, config, paramsBuilder, responseClass, model);
return;
}
if (ObjectHelper.isNotEmpty(jsonSchema)) {
@@ -144,20 +150,65 @@ public class OpenAIResponsesProducer extends DefaultAsyncProducer {
}
ResponseCreateParams params = paramsBuilder.build();
- Response response = getEndpoint().getClient().responses().create(params);
+ Response response = createResponse(exchange, model, params);
finishExchange(exchange, config, response, OpenAIResponsesSupport.extractAssistantText(response));
}
private void processStructured(
Exchange exchange, OpenAIConfiguration config, ResponseCreateParams.Builder paramsBuilder,
- Class<?> responseClass)
+ Class<?> responseClass, String model)
throws Exception {
StructuredResponseCreateParams<?> structuredParams = paramsBuilder.text(responseClass).build();
- StructuredResponse<?> structured = getEndpoint().getClient().responses().create(structuredParams);
- Response raw = structured.rawResponse();
+ Response raw = createStructuredResponse(exchange, model, structuredParams);
finishExchange(exchange, config, raw, OpenAIResponsesSupport.extractAssistantText(raw));
}
+ private Response createResponse(Exchange exchange, String model, ResponseCreateParams params) throws Exception {
+ return observedCall(exchange, model, () -> getEndpoint().getClient().responses().create(params));
+ }
+
+ private Response createStructuredResponse(
+ Exchange exchange, String model, StructuredResponseCreateParams<?> structuredParams)
+ throws Exception {
+ return observedCall(exchange, model,
+ () -> getEndpoint().getClient().responses().create(structuredParams).rawResponse());
+ }
+
+ private Response observedCall(Exchange exchange, String model, ThrowingSupplier<Response, Exception> call)
+ throws Exception {
+ GenAiObservationContext observationContext = GenAiObservationContext.builder()
+ .operationName(GenAiOperationName.CHAT)
+ .system("openai")
+ .requestModel(model)
+ .componentScheme("openai")
+ .build();
+ GenAiObservation observation = GenAiObservability.start(exchange, observationContext);
+ try {
+ Response response = call.get();
+ recordResponseSuccess(observation, response);
+ return response;
+ } catch (Exception e) {
+ GenAiErrorSupport.apply(exchange, e);
+ observation.recordError(e);
+ throw e;
+ } finally {
+ observation.close();
+ }
+ }
+
+ private static void recordResponseSuccess(GenAiObservation observation, Response response) {
+ String finishReason = OpenAIResponsesSupport.extractFinishStatus(response)
+ .map(OpenAIResponsesProducer::mapFinishReason)
+ .orElse(null);
+ response.usage().ifPresentOrElse(
+ usage -> observation.recordSuccess(GenAiUsage.of(
+ usage.inputTokens(),
+ usage.outputTokens(),
+ finishReason,
+ response.model().toString())),
+ () -> observation.recordSuccess(GenAiUsage.of((Long) null, null, finishReason, response.model().toString())));
+ }
+
private void finishExchange(Exchange exchange, OpenAIConfiguration config, Response response, String body) {
if (config.isStoreFullResponse()) {
exchange.setProperty(OpenAIConstants.RESPONSES_RESPONSE, response);
diff --git a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIEmbeddingsObservabilityTest.java b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIEmbeddingsObservabilityTest.java
new file mode 100644
index 000000000000..e9349316b92f
--- /dev/null
+++ b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIEmbeddingsObservabilityTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.ai.observability.GenAiAttributes;
+import org.apache.camel.component.ai.observability.GenAiObservabilityProperties;
+import org.apache.camel.test.infra.openai.mock.OpenAIMock;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class OpenAIEmbeddingsObservabilityTest extends CamelTestSupport {
+
+ private static final String INPUT = "What is Apache Camel?";
+
+ @RegisterExtension
+ static OpenAIMock openAIMock = new OpenAIMock().builder()
+ .whenEmbedding(INPUT)
+ .replyWithEmbedding(new float[] { 0.1f, 0.2f, 0.3f, 0.4f })
+ .end()
+ .build();
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ return OpenAIObservabilityTestSupport.registerRecordingTracer(super.createCamelContext());
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:embedding")
+ .to("openai:embeddings?embeddingModel=text-embedding-ada-002&apiKey=dummy&baseUrl="
+ + openAIMock.getBaseUrl() + "/v1");
+ }
+ };
+ }
+
+ @Test
+ void shouldEmitGenAiSpanFromEmbeddingsProducer() {
+ Exchange result = template.request("direct:embedding", e -> e.getIn().setBody(INPUT));
+
+ @SuppressWarnings("unchecked")
+ List<Float> embedding = result.getMessage().getBody(List.class);
+ assertThat(embedding).hasSize(4);
+
+ OpenAIObservabilityTestSupport.RecordingTracer tracer
+ = OpenAIObservabilityTestSupport.tracer(context);
+ assertThat(tracer.genAiSpans()).hasSize(1);
+ Map<String, String> tags = tracer.genAiSpans().get(0).tags();
+ assertThat(tags.get(GenAiAttributes.OPERATION_NAME)).isEqualTo("embeddings");
+ assertThat(tags.get(GenAiAttributes.SYSTEM)).isEqualTo("openai");
+ assertThat(tags.get(GenAiAttributes.REQUEST_MODEL)).isEqualTo("text-embedding-ada-002");
+ assertThat(tags.get(GenAiAttributes.CAMEL_COMPONENT)).isEqualTo("openai");
+ assertThat(tags.get(GenAiAttributes.INPUT_TOKENS)).isEqualTo(Integer.toString(INPUT.length()));
+ }
+
+ @Test
+ void shouldNotEmitSpanWhenObservabilityDisabled() throws Exception {
+ Properties properties = new Properties();
+ properties.setProperty(GenAiObservabilityProperties.ENABLED, "false");
+ context.getPropertiesComponent().setOverrideProperties(properties);
+
+ template.sendBody("direct:embedding", INPUT);
+
+ assertThat(OpenAIObservabilityTestSupport.tracer(context).genAiSpans()).isEmpty();
+ }
+}
diff --git a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIModerationObservabilityTest.java b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIModerationObservabilityTest.java
new file mode 100644
index 000000000000..dc796f1983aa
--- /dev/null
+++ b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIModerationObservabilityTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.ai.observability.GenAiAttributes;
+import org.apache.camel.test.infra.openai.mock.OpenAIMock;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class OpenAIModerationObservabilityTest extends CamelTestSupport {
+
+ private static final String INPUT = "Apache Camel is an integration framework";
+
+ @RegisterExtension
+ static OpenAIMock openAIMock = new OpenAIMock().builder()
+ .whenModeration(INPUT)
+ .replyWithModerationAllowed()
+ .end()
+ .build();
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ return OpenAIObservabilityTestSupport.registerRecordingTracer(super.createCamelContext());
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:moderation")
+ .to("openai:moderation?moderationModel=omni-moderation-latest&apiKey=dummy&baseUrl="
+ + openAIMock.getBaseUrl() + "/v1");
+ }
+ };
+ }
+
+ @Test
+ void shouldEmitGenAiSpanFromModerationProducer() {
+ Exchange result = template.request("direct:moderation", e -> e.getIn().setBody(INPUT));
+
+ assertThat(result.getMessage().getHeader(OpenAIConstants.MODERATION_FLAGGED, Boolean.class)).isFalse();
+
+ OpenAIObservabilityTestSupport.RecordingTracer tracer
+ = OpenAIObservabilityTestSupport.tracer(context);
+ assertThat(tracer.genAiSpans()).hasSize(1);
+ Map<String, String> tags = tracer.genAiSpans().get(0).tags();
+ assertThat(tags.get(GenAiAttributes.OPERATION_NAME)).isEqualTo("moderation");
+ assertThat(tags.get(GenAiAttributes.SYSTEM)).isEqualTo("openai");
+ assertThat(tags.get(GenAiAttributes.REQUEST_MODEL)).isEqualTo("omni-moderation-latest");
+ assertThat(tags.get(GenAiAttributes.CAMEL_COMPONENT)).isEqualTo("openai");
+ assertThat(tags.get(GenAiAttributes.INPUT_TOKENS)).isNull();
+ }
+}
diff --git a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIObservabilityTestSupport.java b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIObservabilityTestSupport.java
new file mode 100644
index 000000000000..cc8eae53b884
--- /dev/null
+++ b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIObservabilityTestSupport.java
@@ -0,0 +1,129 @@
+/*
+ * 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.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.CamelContextAware;
+import org.apache.camel.component.ai.observability.GenAiAttributes;
+import org.apache.camel.telemetry.Span;
+import org.apache.camel.telemetry.SpanContextPropagationExtractor;
+import org.apache.camel.telemetry.SpanContextPropagationInjector;
+import org.apache.camel.telemetry.SpanLifecycleManager;
+import org.apache.camel.telemetry.Tracer;
+
+/**
+ * Shared recording tracer for OpenAI GenAI observability tests.
+ */
+final class OpenAIObservabilityTestSupport {
+
+ private OpenAIObservabilityTestSupport() {
+ }
+
+ static CamelContext registerRecordingTracer(CamelContext context) {
+ RecordingTracer tracer = new RecordingTracer();
+ CamelContextAware.trySetCamelContext(tracer, context);
+ tracer.init(context);
+ context.getRegistry().bind("openAiObservabilityTestTracer", tracer);
+ return context;
+ }
+
+ static RecordingTracer tracer(CamelContext context) {
+ return context.getRegistry().lookupByNameAndType("openAiObservabilityTestTracer", RecordingTracer.class);
+ }
+
+ static final class RecordingTracer extends Tracer {
+
+ private final List<RecordingSpan> closedSpans = new ArrayList<>();
+
+ @Override
+ protected void initTracer() {
+ setSpanLifecycleManager(new RecordingSpanLifecycleManager());
+ }
+
+ List<RecordingSpan> genAiSpans() {
+ return closedSpans.stream()
+ .filter(span -> span.tags().containsKey(GenAiAttributes.OPERATION_NAME))
+ .toList();
+ }
+
+ private final class RecordingSpanLifecycleManager implements SpanLifecycleManager {
+
+ @Override
+ public Span create(String spanName, String spanKind, Span parent, SpanContextPropagationExtractor extractor) {
+ return new RecordingSpan(spanName);
+ }
+
+ @Override
+ public void activate(Span span) {
+ // noop
+ }
+
+ @Override
+ public void deactivate(Span span) {
+ // noop
+ }
+
+ @Override
+ public void close(Span span) {
+ closedSpans.add((RecordingSpan) span);
+ }
+
+ @Override
+ public void inject(Span span, SpanContextPropagationInjector injector, boolean includeTracing) {
+ // noop
+ }
+ }
+ }
+
+ static final class RecordingSpan implements Span {
+
+ private final Map<String, String> tags = new HashMap<>();
+
+ private RecordingSpan(String name) {
+ tags.put("name", name);
+ }
+
+ Map<String, String> tags() {
+ return tags;
+ }
+
+ @Override
+ public void log(Map<String, String> fields) {
+ // noop
+ }
+
+ @Override
+ public void setTag(String key, String value) {
+ tags.put(key, value);
+ }
+
+ @Override
+ public void setComponent(String component) {
+ tags.put("component", component);
+ }
+
+ @Override
+ public void setError(boolean isError) {
+ // noop
+ }
+ }
+}
diff --git a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIResponsesObservabilityTest.java b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIResponsesObservabilityTest.java
new file mode 100644
index 000000000000..a041be9003f5
--- /dev/null
+++ b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIResponsesObservabilityTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.ai.observability.GenAiAttributes;
+import org.apache.camel.test.infra.openai.mock.OpenAIMock;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class OpenAIResponsesObservabilityTest extends CamelTestSupport {
+
+ @RegisterExtension
+ static OpenAIMock openAIMock = new OpenAIMock().builder()
+ .when("hello-responses")
+ .replyWith("Hi from responses mock")
+ .end()
+ .build();
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ return OpenAIObservabilityTestSupport.registerRecordingTracer(super.createCamelContext());
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:responses")
+ .to("openai:responses?model=gpt-4o&apiKey=dummy&baseUrl=" + openAIMock.getBaseUrl() + "/v1");
+ }
+ };
+ }
+
+ @Test
+ void shouldEmitGenAiSpanFromResponsesProducer() {
+ Exchange result = template.request("direct:responses", e -> e.getIn().setBody("hello-responses"));
+
+ assertThat(result.getMessage().getBody(String.class)).isEqualTo("Hi from responses mock");
+
+ OpenAIObservabilityTestSupport.RecordingTracer tracer
+ = OpenAIObservabilityTestSupport.tracer(context);
+ assertThat(tracer.genAiSpans()).hasSize(1);
+ Map<String, String> tags = tracer.genAiSpans().get(0).tags();
+ assertThat(tags.get(GenAiAttributes.OPERATION_NAME)).isEqualTo("chat");
+ assertThat(tags.get(GenAiAttributes.SYSTEM)).isEqualTo("openai");
+ assertThat(tags.get(GenAiAttributes.REQUEST_MODEL)).isEqualTo("gpt-4o");
+ assertThat(tags.get(GenAiAttributes.CAMEL_COMPONENT)).isEqualTo("openai");
+ assertThat(tags.get(GenAiAttributes.INPUT_TOKENS)).isEqualTo("10");
+ assertThat(tags.get(GenAiAttributes.OUTPUT_TOKENS)).isEqualTo("5");
+ assertThat(tags.get(GenAiAttributes.FINISH_REASONS)).isEqualTo("stop");
+ }
+}
diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 05f4623af1b8..cc55dc6d473b 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -456,6 +456,10 @@ that report `long` token counts are recorded without lossy casts.
OpenAI streaming chat sets `stream_options.include_usage=true` only when GenAI observability is enabled,
adding a final chunk with token usage for span/metric recording.
+OpenAI `embeddings`, `moderation`, and `responses` producers emit GenAI spans with the same attribute keys as
+`chat-completion`. Moderation records model metadata only (no token usage). Embeddings records input tokens when
+the API returns usage.
+
LangChain4j components also expose request model names on new exchange headers
(`CamelLangChain4j*RequestModel`). The response model header (`CamelLangChain4j*ResponseModel`) is set when
the underlying client exposes it (for example langchain4j-chat); the agent and embeddings producers omit it when