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 7985fce6e4fd CAMEL-24560: Use Long for GenAiUsage token counts
7985fce6e4fd is described below

commit 7985fce6e4fdee20574b0068483f20543980e26f
Author: Omar Atie <[email protected]>
AuthorDate: Thu Sep 3 23:17:57 2026 -0700

    CAMEL-24560: Use Long for GenAiUsage token counts
    
    Changes GenAiUsage inputTokens/outputTokens fields from Integer to Long
    to avoid lossy casts and Math.toIntExact failures for large token counts
    returned by OpenAI and other providers. A convenience Integer factory
    overload is kept for LangChain4j/Spring AI call sites. OpenAI embeddings
    producer no longer narrows usage headers to int. Micrometer counters use
    increment(double) for large values. Tests and upgrade guide included.
    
    Closes #26105
    Co-authored-by: Cursor Agent <[email protected]>
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---
 .../component/ai/observability/GenAiUsage.java     | 17 ++++-
 .../component/ai/observability/GenAiUsageTest.java | 82 ++++++++++++++++++++++
 .../ai/observability/GenAiMicrometerSupport.java   |  4 +-
 .../observability/GenAiObservabilitySpanTest.java  | 20 ++++++
 .../ai/observability/GenAiObservabilityTest.java   | 46 ++++++++++++
 .../component/openai/OpenAIEmbeddingsProducer.java |  4 +-
 .../camel/component/openai/OpenAIProducer.java     | 12 ++--
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  3 +
 8 files changed, 173 insertions(+), 15 deletions(-)

diff --git 
a/components/camel-ai/camel-ai-observability-api/src/main/java/org/apache/camel/component/ai/observability/GenAiUsage.java
 
b/components/camel-ai/camel-ai-observability-api/src/main/java/org/apache/camel/component/ai/observability/GenAiUsage.java
index a21f66397bd8..0a40e357f1b4 100644
--- 
a/components/camel-ai/camel-ai-observability-api/src/main/java/org/apache/camel/component/ai/observability/GenAiUsage.java
+++ 
b/components/camel-ai/camel-ai-observability-api/src/main/java/org/apache/camel/component/ai/observability/GenAiUsage.java
@@ -20,13 +20,24 @@ package org.apache.camel.component.ai.observability;
  * Token usage and completion metadata captured after a GenAI operation.
  */
 public record GenAiUsage(
-        Integer inputTokens,
-        Integer outputTokens,
+        Long inputTokens,
+        Long outputTokens,
         String finishReason,
         String responseModel) {
 
-    public static GenAiUsage of(Integer inputTokens, Integer outputTokens, 
Object finishReason, String responseModel) {
+    public static GenAiUsage of(Long inputTokens, Long outputTokens, Object 
finishReason, String responseModel) {
         String reason = finishReason == null ? null : finishReason.toString();
         return new GenAiUsage(inputTokens, outputTokens, reason, 
responseModel);
     }
+
+    /**
+     * Convenience factory when token counts come from APIs that expose {@code 
Integer} counts (e.g. LangChain4j).
+     */
+    public static GenAiUsage of(Integer inputTokens, Integer outputTokens, 
Object finishReason, String responseModel) {
+        return of(toLong(inputTokens), toLong(outputTokens), finishReason, 
responseModel);
+    }
+
+    private static Long toLong(Integer value) {
+        return value == null ? null : value.longValue();
+    }
 }
diff --git 
a/components/camel-ai/camel-ai-observability-api/src/test/java/org/apache/camel/component/ai/observability/GenAiUsageTest.java
 
b/components/camel-ai/camel-ai-observability-api/src/test/java/org/apache/camel/component/ai/observability/GenAiUsageTest.java
new file mode 100644
index 000000000000..80760ef7a94b
--- /dev/null
+++ 
b/components/camel-ai/camel-ai-observability-api/src/test/java/org/apache/camel/component/ai/observability/GenAiUsageTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.ai.observability;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class GenAiUsageTest {
+
+    @Test
+    void shouldCreateUsageWithLongTokenCounts() {
+        GenAiUsage usage = GenAiUsage.of(100L, 50L, "stop", "gpt-4o");
+
+        assertThat(usage.inputTokens()).isEqualTo(100L);
+        assertThat(usage.outputTokens()).isEqualTo(50L);
+        assertThat(usage.finishReason()).isEqualTo("stop");
+        assertThat(usage.responseModel()).isEqualTo("gpt-4o");
+    }
+
+    @Test
+    void shouldAcceptTokenCountsBeyondIntegerMaxValue() {
+        long largeInput = Integer.MAX_VALUE + 1024L;
+        long largeOutput = Integer.MAX_VALUE + 2048L;
+
+        GenAiUsage usage = GenAiUsage.of(largeInput, largeOutput, "length", 
"gpt-4.1");
+
+        assertThat(usage.inputTokens()).isEqualTo(largeInput);
+        assertThat(usage.outputTokens()).isEqualTo(largeOutput);
+    }
+
+    @Test
+    void shouldConvertIntegerFactoryArgumentsToLong() {
+        GenAiUsage usage = GenAiUsage.of(12, 8, "stop", "gpt-4o-mini");
+
+        assertThat(usage.inputTokens()).isEqualTo(12L);
+        assertThat(usage.outputTokens()).isEqualTo(8L);
+    }
+
+    @Test
+    void shouldAllowNullTokenCountsAndFinishReason() {
+        GenAiUsage usage = GenAiUsage.of((Long) null, null, null, "gpt-4o");
+
+        assertThat(usage.inputTokens()).isNull();
+        assertThat(usage.outputTokens()).isNull();
+        assertThat(usage.finishReason()).isNull();
+        assertThat(usage.responseModel()).isEqualTo("gpt-4o");
+    }
+
+    @Test
+    void shouldConvertNullIntegerFactoryArgumentsToNullLongFields() {
+        GenAiUsage usage = GenAiUsage.of((Integer) null, (Integer) null, null, 
"gpt-4o");
+
+        assertThat(usage.inputTokens()).isNull();
+        assertThat(usage.outputTokens()).isNull();
+    }
+
+    @Test
+    void shouldStringifyNonStringFinishReason() {
+        GenAiUsage usage = GenAiUsage.of(1L, 2L, FinishReason.STOP, "model");
+
+        assertThat(usage.finishReason()).isEqualTo("STOP");
+    }
+
+    private enum FinishReason {
+        STOP
+    }
+}
diff --git 
a/components/camel-ai/camel-ai-observability/src/main/java/org/apache/camel/component/ai/observability/GenAiMicrometerSupport.java
 
b/components/camel-ai/camel-ai-observability/src/main/java/org/apache/camel/component/ai/observability/GenAiMicrometerSupport.java
index 149a4cfc20c1..35bbef8bb662 100644
--- 
a/components/camel-ai/camel-ai-observability/src/main/java/org/apache/camel/component/ai/observability/GenAiMicrometerSupport.java
+++ 
b/components/camel-ai/camel-ai-observability/src/main/java/org/apache/camel/component/ai/observability/GenAiMicrometerSupport.java
@@ -71,7 +71,7 @@ final class GenAiMicrometerSupport implements 
GenAiMetricsBackend {
     }
 
     private void recordTokenCounter(
-            Integer tokens, String tokenType, GenAiObservationContext context, 
Throwable error) {
+            Long tokens, String tokenType, GenAiObservationContext context, 
Throwable error) {
         if (tokens == null || tokens <= 0) {
             return;
         }
@@ -79,7 +79,7 @@ final class GenAiMicrometerSupport implements 
GenAiMetricsBackend {
                 .tags(baseTags(context, error))
                 .tag(GenAiMetrics.TAG_TOKEN_TYPE, tokenType)
                 .register(meterRegistry)
-                .increment(tokens);
+                .increment(tokens.doubleValue());
     }
 
     private static Iterable<Tag> baseTags(GenAiObservationContext context, 
Throwable error) {
diff --git 
a/components/camel-ai/camel-ai-observability/src/test/java/org/apache/camel/component/ai/observability/GenAiObservabilitySpanTest.java
 
b/components/camel-ai/camel-ai-observability/src/test/java/org/apache/camel/component/ai/observability/GenAiObservabilitySpanTest.java
index 2a99235aea3b..ccf0d3eee575 100644
--- 
a/components/camel-ai/camel-ai-observability/src/test/java/org/apache/camel/component/ai/observability/GenAiObservabilitySpanTest.java
+++ 
b/components/camel-ai/camel-ai-observability/src/test/java/org/apache/camel/component/ai/observability/GenAiObservabilitySpanTest.java
@@ -74,6 +74,26 @@ class GenAiObservabilitySpanTest extends ExchangeTestSupport 
{
         
assertThat(tags.get(GenAiAttributes.CAMEL_COMPONENT)).isEqualTo("langchain4j-chat");
     }
 
+    @Test
+    void shouldRecordLargeTokenCountsOnSpanAttributes() {
+        long largeInput = Integer.MAX_VALUE + 4096L;
+        long largeOutput = Integer.MAX_VALUE + 8192L;
+
+        Exchange exchange = new DefaultExchange(context);
+        GenAiObservation observation = GenAiObservability.start(exchange, 
GenAiObservationContext.builder()
+                .operationName(GenAiOperationName.CHAT)
+                .system("openai")
+                .requestModel("gpt-4.1")
+                .componentScheme("openai")
+                .build());
+        observation.recordSuccess(GenAiUsage.of(largeInput, largeOutput, 
"stop", "gpt-4.1"));
+        observation.close();
+
+        Map<String, String> tags = tracer.closedSpans().get(0).tags();
+        
assertThat(tags.get(GenAiAttributes.INPUT_TOKENS)).isEqualTo(Long.toString(largeInput));
+        
assertThat(tags.get(GenAiAttributes.OUTPUT_TOKENS)).isEqualTo(Long.toString(largeOutput));
+    }
+
     @Test
     void shouldMarkSpanAsErrorWhenFailureRecorded() {
         Exchange exchange = new DefaultExchange(context);
diff --git 
a/components/camel-ai/camel-ai-observability/src/test/java/org/apache/camel/component/ai/observability/GenAiObservabilityTest.java
 
b/components/camel-ai/camel-ai-observability/src/test/java/org/apache/camel/component/ai/observability/GenAiObservabilityTest.java
index f58cb29dc2ca..658c3e73b0be 100644
--- 
a/components/camel-ai/camel-ai-observability/src/test/java/org/apache/camel/component/ai/observability/GenAiObservabilityTest.java
+++ 
b/components/camel-ai/camel-ai-observability/src/test/java/org/apache/camel/component/ai/observability/GenAiObservabilityTest.java
@@ -70,6 +70,52 @@ class GenAiObservabilityTest extends ExchangeTestSupport {
                 .count()).isEqualTo(5);
     }
 
+    @Test
+    void shouldRecordLargeTokenCountsInMicrometerMetrics() {
+        long largeInput = Integer.MAX_VALUE + 1024L;
+        long largeOutput = Integer.MAX_VALUE + 2048L;
+
+        SimpleMeterRegistry registry = new SimpleMeterRegistry();
+        context.getRegistry().bind("metricsRegistry", registry);
+
+        Exchange exchange = new DefaultExchange(context);
+        GenAiObservation observation = GenAiObservability.start(exchange, 
GenAiObservationContext.builder()
+                .operationName(GenAiOperationName.CHAT)
+                .system("openai")
+                .requestModel("gpt-4.1")
+                .componentScheme("openai")
+                .build());
+        observation.recordSuccess(GenAiUsage.of(largeInput, largeOutput, 
"stop", "gpt-4.1"));
+        observation.close();
+
+        assertThat(registry.find(GenAiMetrics.CLIENT_TOKEN_USAGE)
+                .tag(GenAiMetrics.TAG_TOKEN_TYPE, 
GenAiMetrics.TOKEN_TYPE_INPUT)
+                .counter()
+                .count()).isEqualTo((double) largeInput);
+        assertThat(registry.find(GenAiMetrics.CLIENT_TOKEN_USAGE)
+                .tag(GenAiMetrics.TAG_TOKEN_TYPE, 
GenAiMetrics.TOKEN_TYPE_OUTPUT)
+                .counter()
+                .count()).isEqualTo((double) largeOutput);
+    }
+
+    @Test
+    void shouldSkipMicrometerTokenCountersWhenUsageIsNullOrZero() {
+        SimpleMeterRegistry registry = new SimpleMeterRegistry();
+        context.getRegistry().bind("metricsRegistry", registry);
+
+        Exchange exchange = new DefaultExchange(context);
+        GenAiObservation observation = GenAiObservability.start(exchange, 
GenAiObservationContext.builder()
+                .operationName(GenAiOperationName.CHAT)
+                .system("openai")
+                .requestModel("gpt-4o")
+                .componentScheme("openai")
+                .build());
+        observation.recordSuccess(GenAiUsage.of((Long) null, 0L, "stop", 
"gpt-4o"));
+        observation.close();
+
+        
assertThat(registry.find(GenAiMetrics.CLIENT_TOKEN_USAGE).counter()).isNull();
+    }
+
     @Test
     void shouldReturnNoopWhenDisabled() {
         Properties properties = new Properties();
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 08035cae875f..4c3377fb777c 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
@@ -156,8 +156,8 @@ public class OpenAIEmbeddingsProducer extends 
DefaultAsyncProducer {
         if (response != null) {
             message.setHeader(OpenAIConstants.EMBEDDING_RESPONSE_MODEL, 
response.model());
             if (response.usage() != null) {
-                message.setHeader(OpenAIConstants.PROMPT_TOKENS, (int) 
response.usage().promptTokens());
-                message.setHeader(OpenAIConstants.TOTAL_TOKENS, (int) 
response.usage().totalTokens());
+                message.setHeader(OpenAIConstants.PROMPT_TOKENS, 
response.usage().promptTokens());
+                message.setHeader(OpenAIConstants.TOTAL_TOKENS, 
response.usage().totalTokens());
             }
         }
         message.setHeader(OpenAIConstants.EMBEDDING_COUNT, embeddings.size());
diff --git 
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java
 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java
index 6cb5d85c4dc3..b26b1d4fddaa 100644
--- 
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java
+++ 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java
@@ -680,8 +680,8 @@ public class OpenAIProducer extends DefaultAsyncProducer {
                 CompletionUsage usage = usageRef.get();
                 String responseModel = responseModelRef.get() != null ? 
responseModelRef.get() : requestModel;
                 observation.recordSuccess(GenAiUsage.of(
-                        usage != null ? toTokenCount(usage.promptTokens()) : 
null,
-                        usage != null ? toTokenCount(usage.completionTokens()) 
: null,
+                        usage != null ? usage.promptTokens() : null,
+                        usage != null ? usage.completionTokens() : null,
                         null,
                         responseModel));
                 observation.close();
@@ -731,10 +731,6 @@ public class OpenAIProducer extends DefaultAsyncProducer {
                 .orElse("stop");
     }
 
-    private static Integer toTokenCount(long tokens) {
-        return Math.toIntExact(tokens);
-    }
-
     private ChatCompletion createChatCompletion(Exchange exchange, 
ChatCompletionCreateParams params) {
         String requestModel = params.model().toString();
         GenAiObservationContext observationContext = 
GenAiObservationContext.builder()
@@ -748,8 +744,8 @@ public class OpenAIProducer extends DefaultAsyncProducer {
             ChatCompletion response = 
getEndpoint().getClient().chat().completions().create(params);
             CompletionUsage usage = response.usage().orElse(null);
             observation.recordSuccess(GenAiUsage.of(
-                    usage != null ? toTokenCount(usage.promptTokens()) : null,
-                    usage != null ? toTokenCount(usage.completionTokens()) : 
null,
+                    usage != null ? usage.promptTokens() : null,
+                    usage != null ? usage.completionTokens() : null,
                     response.choices().isEmpty() ? null : 
getFinishReasonString(response.choices().get(0)),
                     response.model()));
             return response;
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 14da60b11062..2a62a71a8cb6 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
@@ -436,6 +436,9 @@ the registry has tracing and meter handlers respectively. 
Token usage counters s
 `MeterRegistry`. Applications without an `ObservationRegistry` bean keep the 
previous OpenTelemetry
 and MeterRegistry behavior. `camel-micrometer-observability` is not required.
 
+`GenAiUsage` token fields (`inputTokens`, `outputTokens`) are `Long` so OpenAI 
and other providers
+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.
 

Reply via email to