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