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 ----
