This is an automated email from the ASF dual-hosted git repository.

luigidemasi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 4925d73ab670a94051e8ce78569e0f63ee059fe3
Author: Luigi De Masi <[email protected]>
AuthorDate: Thu Oct 15 08:17:18 2026 +0200

    CAMEL-25669: Observe Decisions producer calls and clarify documentation
    
    Record GenAI models, optional token usage and failures using the shared
    observation helpers. Clarify native versus Semantic model selection and
    use canonical YAML expression syntax. Cover success, absent usage, errors
    and the observability toggle in the HTTP tests.
    
    Co-authored-by: OpenAI Codex <[email protected]>
    Signed-off-by: Luigi De Masi <[email protected]>
---
 .../camel/catalog/docs/openai-component.adoc       |  8 +++
 .../apache/camel/catalog/docs/semantic-openai.adoc |  4 +-
 .../ai/observability/GenAiOperationName.java       |  4 +-
 .../src/main/docs/openai-component.adoc            |  8 +++
 .../component/openai/OpenAIDecisionsProducer.java  | 20 +++++-
 .../component/openai/OpenAIDecisionsHttpTest.java  | 74 +++++++++++++++++++++-
 .../src/main/docs/semantic-openai.adoc             |  4 +-
 7 files changed, 117 insertions(+), 5 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-component.adoc
index fa7468334fef..a0d28d6b580f 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-component.adoc
@@ -37,6 +37,14 @@ Maven users will need to add the following dependency to 
their `pom.xml` for thi
 `com.openai.models.decisions.Decision` body. It uses the endpoint's managed 
OpenAI client,
 including credentials, base URL, TLS, proxy, timeout and retry settings.
 Decisions does not initialize configured MCP servers or route tools.
+The model is taken from the `DecisionCreateParams` body; the endpoint `model` 
option is
+not applied by this producer. The Semantic adapter instead builds its request 
using the
+endpoint's configured `model`.
+
+The producer emits GenAI observations with operation name `decisions`, request 
and response
+models, and input/output token counts when the provider returns usage. 
Failures are recorded
+through the shared GenAI error handling. Direct `OpenAIEndpoint.decide()` 
calls, including
+the Semantic adapter, do not emit these observations because they have no 
Exchange.
 
 [source,java]
 ----
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-openai.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-openai.adoc
index eaeb98334c49..46ed63971901 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-openai.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-openai.adoc
@@ -61,7 +61,9 @@ camel.component.openai.api-key={{env:OPENAI_API_KEY}}
       steps:
         - setVariable:
             name: damaged
-            simple: "${semantic('damaged')}"
+            expression:
+              simple:
+                expression: "${semantic('damaged')}"
         - to: direct:route-policy
 ----
 
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 114fa78229e0..39cf086f4826 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
@@ -31,7 +31,9 @@ public enum GenAiOperationName {
     /** Text-to-speech operation. */
     SPEECH("speech"),
     /** OpenAI moderation and similar content-policy checks. */
-    MODERATION("moderation");
+    MODERATION("moderation"),
+    /** Typed predicate, choice and rubric assessments (Camel extension). */
+    DECISIONS("decisions");
 
     private final String value;
 
diff --git 
a/components/camel-ai/camel-openai/src/main/docs/openai-component.adoc 
b/components/camel-ai/camel-openai/src/main/docs/openai-component.adoc
index fa7468334fef..a0d28d6b580f 100644
--- a/components/camel-ai/camel-openai/src/main/docs/openai-component.adoc
+++ b/components/camel-ai/camel-openai/src/main/docs/openai-component.adoc
@@ -37,6 +37,14 @@ Maven users will need to add the following dependency to 
their `pom.xml` for thi
 `com.openai.models.decisions.Decision` body. It uses the endpoint's managed 
OpenAI client,
 including credentials, base URL, TLS, proxy, timeout and retry settings.
 Decisions does not initialize configured MCP servers or route tools.
+The model is taken from the `DecisionCreateParams` body; the endpoint `model` 
option is
+not applied by this producer. The Semantic adapter instead builds its request 
using the
+endpoint's configured `model`.
+
+The producer emits GenAI observations with operation name `decisions`, request 
and response
+models, and input/output token counts when the provider returns usage. 
Failures are recorded
+through the shared GenAI error handling. Direct `OpenAIEndpoint.decide()` 
calls, including
+the Semantic adapter, do not emit these observations because they have no 
Exchange.
 
 [source,java]
 ----
diff --git 
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIDecisionsProducer.java
 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIDecisionsProducer.java
index b616fd8cd878..a2c8a8ff8966 100644
--- 
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIDecisionsProducer.java
+++ 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIDecisionsProducer.java
@@ -16,8 +16,12 @@
  */
 package org.apache.camel.component.openai;
 
+import com.openai.models.decisions.Decision;
 import com.openai.models.decisions.DecisionCreateParams;
 import org.apache.camel.Exchange;
+import org.apache.camel.component.ai.observability.GenAiObservation;
+import org.apache.camel.component.ai.observability.GenAiOperationName;
+import org.apache.camel.component.ai.observability.GenAiUsage;
 import org.apache.camel.support.DefaultProducer;
 
 /** Sends native Decisions requests without executing tools or applying 
decision policy. */
@@ -31,6 +35,20 @@ public class OpenAIDecisionsProducer extends DefaultProducer 
{
         if (!(exchange.getMessage().getBody() instanceof DecisionCreateParams 
request)) {
             throw new IllegalArgumentException("Decisions requires a 
DecisionCreateParams body");
         }
-        exchange.getMessage().setBody(((OpenAIEndpoint) 
getEndpoint()).decide(request));
+        GenAiObservation observation
+                = OpenAIGenAiProducerSupport.start(exchange, 
GenAiOperationName.DECISIONS, request.model());
+        try {
+            Decision response = ((OpenAIEndpoint) 
getEndpoint()).decide(request);
+            var usage = response._usage().asKnown();
+            OpenAIGenAiProducerSupport.recordSuccess(observation,
+                    
GenAiUsage.of(usage.map(Decision.Usage::inputTokens).orElse(null),
+                            
usage.map(Decision.Usage::outputTokens).orElse(null), null, response.model()));
+            exchange.getMessage().setBody(response);
+        } catch (Exception e) {
+            OpenAIGenAiProducerSupport.recordFailure(exchange, observation, e);
+            throw e;
+        } finally {
+            observation.close();
+        }
     }
 }
diff --git 
a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIDecisionsHttpTest.java
 
b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIDecisionsHttpTest.java
index 50ca4aed1a39..0c4bf46b0b89 100644
--- 
a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIDecisionsHttpTest.java
+++ 
b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIDecisionsHttpTest.java
@@ -19,6 +19,7 @@ package org.apache.camel.component.openai;
 import java.net.InetSocketAddress;
 import java.nio.charset.StandardCharsets;
 import java.util.List;
+import java.util.Properties;
 import java.util.concurrent.atomic.AtomicReference;
 
 import com.openai.models.decisions.DecisionChoiceOption;
@@ -27,6 +28,10 @@ import com.openai.models.decisions.DecisionInputImage;
 import com.openai.models.decisions.DecisionInputMessage;
 import com.openai.models.decisions.DecisionInputPart;
 import com.sun.net.httpserver.HttpServer;
+import org.apache.camel.Exchange;
+import org.apache.camel.component.ai.observability.GenAiAttributes;
+import org.apache.camel.component.ai.observability.GenAiErrorProperties;
+import 
org.apache.camel.component.ai.observability.GenAiObservabilityProperties;
 import org.apache.camel.impl.DefaultCamelContext;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
@@ -61,8 +66,9 @@ class OpenAIDecisionsHttpTest {
         });
         server.start();
         context = new DefaultCamelContext();
+        OpenAIObservabilityTestSupport.registerRecordingTracer(context);
         endpoint = 
context.getEndpoint("openai:decisions?apiKey=test&maxRetries=0&baseUrl=http://127.0.0.1:";
-                                       + server.getAddress().getPort() + "/v1",
+                                       + server.getAddress().getPort() + 
"/v1&model=endpoint-model",
                 OpenAIEndpoint.class);
         context.start();
     }
@@ -80,6 +86,72 @@ class OpenAIDecisionsHttpTest {
         }
     }
 
+    @Test
+    void producerRecordsModelsAndTokenUsage() throws Exception {
+        response = """
+                
{"model":"response-model","answers":[{"type":"predicate","name":"test","probability":0.9}],
+                 
"usage":{"input_tokens":12,"output_tokens":3,"total_tokens":15}}
+                """;
+        Exchange result = sendRequest();
+        assertThat(result.getException()).isNull();
+        var spans = 
OpenAIObservabilityTestSupport.tracer(context).genAiSpans();
+        assertThat(spans).hasSize(1);
+        assertThat(spans.get(0).tags())
+                .containsEntry(GenAiAttributes.OPERATION_NAME, "decisions")
+                .containsEntry(GenAiAttributes.SYSTEM, "openai")
+                .containsEntry(GenAiAttributes.REQUEST_MODEL, "gpt-6-luna")
+                .containsEntry(GenAiAttributes.RESPONSE_MODEL, 
"response-model")
+                .containsEntry(GenAiAttributes.INPUT_TOKENS, "12")
+                .containsEntry(GenAiAttributes.OUTPUT_TOKENS, "3")
+                .containsEntry(GenAiAttributes.CAMEL_COMPONENT, "openai");
+        
assertThat(received.get()).contains("gpt-6-luna").doesNotContain("endpoint-model");
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "", ",\"usage\":null" })
+    void producerAllowsAbsentUsage(String usage) throws Exception {
+        response = """
+                
{"model":"gpt-6-luna","answers":[{"type":"predicate","name":"test","probability":0.9}]%s}
+                """.formatted(usage);
+        assertThat(sendRequest().getException()).isNull();
+        var spans = 
OpenAIObservabilityTestSupport.tracer(context).genAiSpans();
+        assertThat(spans).hasSize(1);
+        assertThat(spans.get(0).tags())
+                .containsEntry(GenAiAttributes.RESPONSE_MODEL, "gpt-6-luna")
+                .doesNotContainKeys(GenAiAttributes.INPUT_TOKENS, 
GenAiAttributes.OUTPUT_TOKENS);
+    }
+
+    @Test
+    void producerRecordsAndClosesFailedObservation() throws Exception {
+        status = 503;
+        response = "{\"error\":{\"message\":\"unavailable\"}}";
+        Exchange result = sendRequest();
+        
assertThat(result.getException()).isInstanceOf(OpenAIDecisionServiceException.class);
+        
assertThat(result.getProperty(GenAiErrorProperties.ERROR_CATEGORY)).isEqualTo("SERVER_ERROR");
+        var spans = 
OpenAIObservabilityTestSupport.tracer(context).genAiSpans();
+        assertThat(spans).hasSize(1);
+        assertThat(spans.get(0).tags())
+                .containsEntry(GenAiAttributes.ERROR_TYPE, 
"OpenAIDecisionServiceException");
+    }
+
+    @Test
+    void producerRespectsObservabilityToggle() throws Exception {
+        Properties properties = new Properties();
+        properties.setProperty(GenAiObservabilityProperties.ENABLED, "false");
+        context.getPropertiesComponent().setOverrideProperties(properties);
+        response = """
+                
{"model":"gpt-6-luna","answers":[{"type":"predicate","name":"test","probability":0.9}]}
+                """;
+        assertThat(sendRequest().getException()).isNull();
+        
assertThat(OpenAIObservabilityTestSupport.tracer(context).genAiSpans()).isEmpty();
+    }
+
+    private Exchange sendRequest() throws Exception {
+        try (var template = context.createProducerTemplate()) {
+            return template.request(endpoint.getEndpointUri(), exchange -> 
exchange.getMessage().setBody(request()));
+        }
+    }
+
     @Test
     void serializesNativeImageAndDeserializesPredicate() throws Exception {
         response = """
diff --git 
a/components/camel-ai/camel-semantic-openai/src/main/docs/semantic-openai.adoc 
b/components/camel-ai/camel-semantic-openai/src/main/docs/semantic-openai.adoc
index eaeb98334c49..46ed63971901 100644
--- 
a/components/camel-ai/camel-semantic-openai/src/main/docs/semantic-openai.adoc
+++ 
b/components/camel-ai/camel-semantic-openai/src/main/docs/semantic-openai.adoc
@@ -61,7 +61,9 @@ camel.component.openai.api-key={{env:OPENAI_API_KEY}}
       steps:
         - setVariable:
             name: damaged
-            simple: "${semantic('damaged')}"
+            expression:
+              simple:
+                expression: "${semantic('damaged')}"
         - to: direct:route-policy
 ----
 

Reply via email to