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 6ae5e852416e CAMEL-23861: GenAI observability Phase 3 - Spring AI chat 
and camel-main config
6ae5e852416e is described below

commit 6ae5e852416e73847ccba2b2e6c50080c8b3a8de
Author: Omar Atie <[email protected]>
AuthorDate: Thu Aug 27 23:07:05 2026 -0700

    CAMEL-23861: GenAI observability Phase 3 - Spring AI chat and camel-main 
config
    
    Extends GenAI observability (Phase 1: langchain4j/openai, Phase 2: TUI usage
    view) to Spring AI chat producers and adds first-class camel.aiObservability
    configuration in Camel Main.
    
    - SpringAiChatProducer now emits GenAI OpenTelemetry span attributes and
      records token usage via GenAiObservability, matching the langchain4j-chat
      pattern (chat, structured output, and entity call paths).
    - GenAiModelResolver extended with reflection-based Spring AI provider/model
      resolution, with no hard dependency on Spring AI in the API module.
    - Camel Main gains typed aiObservability() configuration (properties,
      programmatic DSL, and metadata), replacing ad-hoc property lookups.
    - Fixes PrepareCamelMainMojo metadata prefix and adds tests and docs
      (ai-observability.adoc, upgrade guide 4.23 entry).
    
    Co-authored-by: Cursor Agent <[email protected]>
    Co-authored-by: Omar Atie <[email protected]>
    
    Closes #25507
---
 .../camel/catalog/docs/ai-observability.adoc       |  17 +-
 .../org/apache/camel/catalog/docs/main.adoc        |  10 +
 .../catalog/docs/spring-ai-chat-component.adoc     |  17 +
 .../main/camel-main-configuration-metadata.json    |   2 +
 .../ai/observability/GenAiModelResolver.java       | 368 +++++++++++++++++++--
 .../GenAiObservabilityProperties.java              |   2 +-
 .../src/main/docs/ai-observability.adoc            |  17 +-
 .../ai/observability/GenAiModelResolverTest.java   | 126 ++++++-
 .../ai/openai/FakeOpenAiChatModel.java}            |  24 +-
 .../agent/LangChain4jAgentProducer.java            |   4 +-
 .../langchain4j/chat/LangChain4jChatProducer.java  |  14 +-
 .../embeddings/LangChain4jEmbeddingsProducer.java  |   4 +-
 .../camel-spring-ai/camel-spring-ai-chat/pom.xml   |  17 +
 .../src/main/docs/spring-ai-chat-component.adoc    |  17 +
 .../springai/chat/SpringAiChatProducer.java        | 143 +++++++-
 .../chat/SpringAiChatObservabilityTest.java        | 291 ++++++++++++++++
 .../ai/openai/EntityJsonStubOpenAiChatModel.java   |  82 +++++
 .../ai/openai/StubOpenAiChatModel.java             |  82 +++++
 core/camel-main/pom.xml                            |   6 +
 ...rvabilityConfigurationPropertiesConfigurer.java |  60 ++++
 .../camel-main-configuration-metadata.json         |   2 +
 ...mel.main.AiObservabilityConfigurationProperties |   2 +
 core/camel-main/src/main/docs/main.adoc            |  10 +
 .../AiObservabilityConfigurationProperties.java    |  67 ++++
 .../org/apache/camel/main/BaseMainSupport.java     |  36 +-
 .../camel/main/MainConfigurationProperties.java    |  22 ++
 ...AiObservabilityConfigurationPropertiesTest.java |  65 ++++
 .../src/test/resources/ai-observability.properties |  17 +
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |   5 +-
 .../maven/packaging/PrepareCamelMainMojo.java      |   5 +
 30 files changed, 1448 insertions(+), 86 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 04e3893b1427..2fe8750d7714 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
@@ -15,16 +15,25 @@ OpenTelemetry semantic conventions for GenAI (stable subset 
aligned with Spring
 AI producers depend on the lightweight `camel-ai-observability-api` module. 
Add `camel-ai-observability` to your
 application classpath together with a tracing or metrics backend to enable 
span and metric emission.
 
-When `camel-opentelemetry2` and/or `camel-micrometer` is on the classpath, 
langchain4j and OpenAI producers
-emit child spans and metrics per LLM call with attributes such as 
`gen_ai.operation.name`, `gen_ai.system`,
+When `camel-opentelemetry2` and/or `camel-micrometer` is on the classpath, 
langchain4j, OpenAI, and Spring AI chat
+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`.
 
-Global toggle: set `camel.ai.observability.enabled=false` to disable GenAI 
observability across all AI
-components (default is `true` when a tracing or metrics backend is present).
+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)`.
 
 Phase 1 coverage: `langchain4j-chat`, `langchain4j-tools`, 
`langchain4j-agent`, `langchain4j-embeddings`,
 and `openai`.
 
+Phase 3 coverage: `spring-ai-chat`.
+
+When using `spring-ai-chat` with only a pre-built `chatClient` (no `chatModel` 
on the endpoint), Camel attempts to read
+the backing Spring AI `ChatModel` via a private field on Spring AI's internal 
`ChatClientRequestSpec` implementation.
+That field is not public API and may change between Spring AI releases. If 
extraction fails, `gen_ai.system` and
+`gen_ai.request.model` span attributes may report `unknown`. Configure 
`chatModel` explicitly on the endpoint URI or
+component when GenAI observability is enabled so model metadata is always 
populated.
+
 New exchange headers for model identification on langchain4j components:
 
 * `CamelLangChain4jChatRequestModel` / `CamelLangChain4jChatResponseModel`
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/main.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/main.adoc
index 869f38ff8ff3..3cb8a5dd8622 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/main.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/main.adoc
@@ -761,6 +761,16 @@ The camel.errorRegistry supports 8 options, which are 
listed below.
 |===
 
 
+=== Camel GenAI Observability configurations
+The camel.aiObservability supports 1 options, which are listed below.
+
+[width="100%",cols="2,5,^1,2",options="header"]
+|===
+| Name | Description | Default | Type
+| *camel.aiObservability.enabled* | Enables GenAI observability for Camel AI 
producers (OpenTelemetry spans and Micrometer metrics when backends are 
present). | true | boolean
+|===
+
+
 === Camel File Cluster Service configurations
 The camel.cluster.file supports 10 options, which are listed below.
 
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spring-ai-chat-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spring-ai-chat-component.adoc
index eb6b2d7d4ce4..d2fd642a768a 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spring-ai-chat-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spring-ai-chat-component.adoc
@@ -1381,6 +1381,23 @@ is on the classpath.
 Refer to the 
https://docs.spring.io/spring-ai/reference/observability/index.html[Spring AI 
Observability documentation]
 for details on available metrics, tracing, and prompt/completion logging 
configuration.
 
+==== Camel GenAI observability
+
+When `camel-ai-observability` and a tracing backend (for example 
`camel-opentelemetry2`) are on the classpath, the
+producer emits OpenTelemetry spans with `gen_ai.*` attributes per chat call. 
Enable or disable globally with
+`camel.aiObservability.enabled` (see xref:others:ai-observability.adoc[AI 
Observability]).
+
+If the endpoint is configured with `chatClient` only (no `chatModel`), Camel 
tries to resolve the backing `ChatModel`
+for span metadata by reading a private field on Spring AI's internal 
`ChatClientRequestSpec` implementation. That is not
+public Spring AI API and may break on Spring AI upgrades. When GenAI 
observability is enabled, configure `chatModel`
+explicitly alongside `chatClient` so `gen_ai.system` and 
`gen_ai.request.model` attributes are reliably populated:
+
+[source,java]
+----
+from("direct:start")
+    .to("spring-ai-chat:chat?chatClient=#chatClient&chatModel=#chatModel");
+----
+
 === Request/Response Logging
 
 The component automatically adds Spring AI's `SimpleLoggerAdvisor` to log 
requests and responses for debugging. Enable it by setting the logger 
`org.springframework.ai.chat.client.advisor` to DEBUG level:
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
index fc0b05196272..68fd96c319ab 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
@@ -29,6 +29,7 @@
     { "name": "camel.resilience4j", "description": "Resilience4j EIP Circuit 
Breaker configurations", "sourceType": 
"org.apache.camel.main.Resilience4jConfigurationProperties" },
     { "name": "camel.lra", "description": "Camel Saga EIP (Long Running 
Actions) configurations", "sourceType": 
"org.apache.camel.main.LraConfigurationProperties" },
     { "name": "camel.errorRegistry", "description": "Camel Error Registry 
configurations", "sourceType": 
"org.apache.camel.main.ErrorRegistryConfigurationProperties" },
+    { "name": "camel.aiObservability", "description": "Camel GenAI 
Observability configurations", "sourceType": 
"org.apache.camel.main.AiObservabilityConfigurationProperties" },
     { "name": "camel.cluster.file", "description": "Camel File Cluster Service 
configurations", "sourceType": 
"org.apache.camel.component.file.cluster.FileLockClusterService" },
     { "name": "camel.cluster.kubernetes", "description": "Camel Kubernetes 
Cluster Service configurations", "sourceType": 
"org.apache.camel.component.kubernetes.cluster.KubernetesClusterService" },
     { "name": "camel.cluster.zookeeper", "description": "Camel ZooKeeper 
Cluster Service configurations", "sourceType": 
"org.apache.camel.component.zookeeper.cluster.ZooKeeperClusterService" },
@@ -167,6 +168,7 @@
     { "name": "camel.main.uuidGenerator", "required": false, "description": 
"UUID generator to use. default (32 bytes), short (16 bytes), classic (32 bytes 
or longer), simple (long incrementing counter), off (turned off for exchanges - 
only intended for performance profiling)", "sourceType": 
"org.apache.camel.main.DefaultConfigurationProperties", "type": "enum", 
"javaType": "java.lang.String", "defaultValue": "default", "secret": false, 
"enum": [ "classic", "default", "short", "simple",  [...]
     { "name": "camel.main.virtualThreadsEnabled", "required": false, 
"description": "Whether to enable virtual threads when creating thread pools. 
When enabled, Camel will use virtual threads instead of platform threads for 
its thread pools. This can also be enabled via the JVM system property {code 
camel.threads.virtual.enabled=true} . This option must be read early during 
bootstrap, so it is set as a system property before thread pools are created.", 
"sourceType": "org.apache.camel.mai [...]
     { "name": "camel.main.yamlDslCompactNotationWarn", "required": false, 
"description": "Whether to log a WARN when YAML DSL routes use compact 
(shorthand) notation instead of the canonical (explicit\/normalized) form. The 
canonical style is recommended as it is more tooling and AI friendly. Use Camel 
CLI to normalize existing routes: camel validate normalize &lt;file&gt;", 
"sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": 
"boolean", "javaType": "boolean", "d [...]
+    { "name": "camel.aiObservability.enabled", "required": false, 
"description": "Enables GenAI observability for Camel AI producers 
(OpenTelemetry spans and Micrometer metrics when backends are present).", 
"sourceType": "org.apache.camel.main.AiObservabilityConfigurationProperties", 
"type": "boolean", "javaType": "boolean", "defaultValue": true, "secret": false 
},
     { "name": "camel.cluster.consul.aclToken", "required": false, 
"description": "Sets the ACL token to be used with Consul", "sourceType": 
"org.apache.camel.component.consul.cluster.ConsulClusterService", "type": 
"string", "javaType": "java.lang.String", "secret": true, "security": "secret" 
},
     { "name": "camel.cluster.consul.blockSeconds", "required": false, 
"description": "The second to wait for a watch event, default 10 seconds", 
"sourceType": "org.apache.camel.component.consul.cluster.ConsulClusterService", 
"type": "integer", "javaType": "int", "defaultValue": 10, "secret": false },
     { "name": "camel.cluster.consul.configuration", "required": false, 
"description": "To use an existing configuration", "sourceType": 
"org.apache.camel.component.consul.cluster.ConsulClusterService", "type": 
"object", "javaType": 
"org.apache.camel.component.consul.cluster.ConsulClusterConfiguration", 
"secret": false },
diff --git 
a/components/camel-ai/camel-ai-observability-api/src/main/java/org/apache/camel/component/ai/observability/GenAiModelResolver.java
 
b/components/camel-ai/camel-ai-observability-api/src/main/java/org/apache/camel/component/ai/observability/GenAiModelResolver.java
index 335e1c259c3c..9d81980fd54e 100644
--- 
a/components/camel-ai/camel-ai-observability-api/src/main/java/org/apache/camel/component/ai/observability/GenAiModelResolver.java
+++ 
b/components/camel-ai/camel-ai-observability-api/src/main/java/org/apache/camel/component/ai/observability/GenAiModelResolver.java
@@ -16,81 +16,372 @@
  */
 package org.apache.camel.component.ai.observability;
 
-import dev.langchain4j.model.ModelProvider;
-import dev.langchain4j.model.chat.ChatModel;
-import dev.langchain4j.model.chat.response.ChatResponse;
-import dev.langchain4j.model.embedding.EmbeddingModel;
+import java.lang.ref.SoftReference;
+import java.lang.reflect.Method;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.apache.camel.spi.ClassResolver;
 
 /**
- * Resolves GenAI provider and model metadata from LangChain4j model beans.
+ * Resolves GenAI provider and model metadata from LangChain4j and Spring AI 
model beans.
+ * <p/>
+ * LangChain4j types are resolved reflectively so callers such as {@code 
camel-spring-ai-chat} do not require
+ * {@code langchain4j-core} on the classpath.
+ * <p/>
+ * Reflective {@link Method} lookups are cached with {@link SoftReference} 
values keyed by declaring class name and
+ * method name (not {@link Class} instances) so recycled classloaders are not 
pinned by the cache keys.
  */
 public final class GenAiModelResolver {
 
     private static final String UNKNOWN = "unknown";
+    private static final int MAX_MODEL_UNWRAP_DEPTH = 4;
+
+    private static final String LANGCHAIN4J_CHAT_MODEL = 
"dev.langchain4j.model.chat.ChatModel";
+    private static final String LANGCHAIN4J_EMBEDDING_MODEL = 
"dev.langchain4j.model.embedding.EmbeddingModel";
+    private static final String LANGCHAIN4J_CHAT_RESPONSE = 
"dev.langchain4j.model.chat.response.ChatResponse";
+
+    /**
+     * LangChain4j {@code ModelProvider} enum constant names matched 
reflectively (no langchain4j dependency).
+     */
+    private static final String LC4J_PROVIDER_OPEN_AI = "OPEN_AI";
+    private static final String LC4J_PROVIDER_ANTHROPIC = "ANTHROPIC";
+    private static final String LC4J_PROVIDER_OLLAMA = "OLLAMA";
+    private static final String LC4J_PROVIDER_AZURE_OPEN_AI = "AZURE_OPEN_AI";
+    private static final String LC4J_PROVIDER_GOOGLE_VERTEX_AI_GEMINI = 
"GOOGLE_VERTEX_AI_GEMINI";
+    private static final String LC4J_PROVIDER_GOOGLE_VERTEX_AI_ANTHROPIC = 
"GOOGLE_VERTEX_AI_ANTHROPIC";
+    private static final String LC4J_PROVIDER_GOOGLE_AI_GEMINI = 
"GOOGLE_AI_GEMINI";
+    private static final String LC4J_PROVIDER_GOOGLE_GENAI = "GOOGLE_GENAI";
+    private static final String LC4J_PROVIDER_MISTRAL_AI = "MISTRAL_AI";
+    private static final String LC4J_PROVIDER_AMAZON_BEDROCK = 
"AMAZON_BEDROCK";
+
+    private static final ConcurrentMap<MethodKey, SoftReference<Method>> 
METHOD_CACHE = new ConcurrentHashMap<>();
 
     private GenAiModelResolver() {
     }
 
-    public static String resolveSystem(Object model) {
-        if (model == null) {
+    public static String resolveSystem(ClassResolver classResolver, Object 
model) {
+        return resolveSystem(classResolver, model, new HashSet<>(), 0);
+    }
+
+    public static String resolveModelName(ClassResolver classResolver, Object 
model) {
+        return resolveModelName(classResolver, model, new HashSet<>(), 0);
+    }
+
+    /**
+     * Resolves the response model from a LangChain4j or Spring AI chat 
response, falling back when absent.
+     */
+    public static String resolveResponseModelName(ClassResolver classResolver, 
Object chatResponse, String fallback) {
+        if (chatResponse == null) {
+            return fallback;
+        }
+        if (isLangChain4jPresent(classResolver, chatResponse)
+                && isInstanceOf(classResolver, chatResponse, 
LANGCHAIN4J_CHAT_RESPONSE)) {
+            String modelName = invokeToString(classResolver, chatResponse, 
"modelName");
+            if (modelName != null && !modelName.isBlank()) {
+                return modelName;
+            }
+        }
+        return resolveSpringAiResponseModelName(classResolver, chatResponse, 
fallback);
+    }
+
+    /**
+     * Resolves the response model from a Spring AI {@code ChatResponse}, 
falling back when absent.
+     */
+    public static String resolveSpringAiResponseModelName(ClassResolver 
classResolver, Object chatResponse, String fallback) {
+        if (chatResponse == null) {
+            return fallback;
+        }
+        try {
+            Object metadata = invokeNoArg(classResolver, chatResponse, 
"getMetadata");
+            if (metadata != null) {
+                Object model = invokeNoArg(classResolver, metadata, 
"getModel");
+                if (model != null && !model.toString().isBlank()) {
+                    return model.toString();
+                }
+            }
+        } catch (ReflectiveOperationException e) {
+            // ignore
+        }
+        return fallback;
+    }
+
+    private static String resolveSystem(ClassResolver classResolver, Object 
model, Set<Integer> visited, int depth) {
+        if (model == null || depth > MAX_MODEL_UNWRAP_DEPTH) {
             return UNKNOWN;
         }
-        if (model instanceof ChatModel chatModel) {
-            return mapProvider(chatModel.provider());
+        if (!markVisited(model, visited)) {
+            return UNKNOWN;
+        }
+        if (isLangChain4jPresent(classResolver, model) && 
isInstanceOf(classResolver, model, LANGCHAIN4J_CHAT_MODEL)) {
+            return mapLangChain4jProvider(invokeToString(classResolver, model, 
"provider"));
         }
-        if (model instanceof EmbeddingModel embeddingModel) {
-            return mapProvider(embeddingModel.provider());
+        if (isLangChain4jPresent(classResolver, model) && 
isInstanceOf(classResolver, model, LANGCHAIN4J_EMBEDDING_MODEL)) {
+            return mapLangChain4jProvider(invokeToString(classResolver, model, 
"provider"));
+        }
+        if (isSpringAiType(model)) {
+            String fromUnderlyingModel = 
resolveSystemFromSpringAiUnderlyingModel(classResolver, model, visited, depth + 
1);
+            if (!UNKNOWN.equals(fromUnderlyingModel)) {
+                return fromUnderlyingModel;
+            }
+            String fromPackage = 
resolveSystemFromSpringAiPackage(resolveSpringAiPackageName(model));
+            if (!UNKNOWN.equals(fromPackage)) {
+                return fromPackage;
+            }
         }
         return resolveSystemFromPackage(model.getClass().getPackageName());
     }
 
-    public static String resolveModelName(Object model) {
-        if (model == null) {
+    private static String resolveModelName(ClassResolver classResolver, Object 
model, Set<Integer> visited, int depth) {
+        if (model == null || depth > MAX_MODEL_UNWRAP_DEPTH) {
+            return UNKNOWN;
+        }
+        if (!markVisited(model, visited)) {
             return UNKNOWN;
         }
-        if (model instanceof ChatModel chatModel) {
-            String modelName = 
chatModel.defaultRequestParameters().modelName();
+        if (isLangChain4jPresent(classResolver, model) && 
isInstanceOf(classResolver, model, LANGCHAIN4J_CHAT_MODEL)) {
+            String modelName = resolveLangChain4jChatModelName(classResolver, 
model);
             if (modelName != null && !modelName.isBlank()) {
                 return modelName;
             }
         }
-        if (model instanceof EmbeddingModel embeddingModel) {
-            String modelName = embeddingModel.modelName();
+        if (isLangChain4jPresent(classResolver, model) && 
isInstanceOf(classResolver, model, LANGCHAIN4J_EMBEDDING_MODEL)) {
+            String modelName = 
resolveLangChain4jEmbeddingModelName(classResolver, model);
             if (modelName != null && !modelName.isBlank()) {
                 return modelName;
             }
         }
+        if (isSpringAiType(model)) {
+            return resolveSpringAiModelName(classResolver, model, visited, 
depth + 1);
+        }
         return UNKNOWN;
     }
 
-    /**
-     * Resolves the response model from a LangChain4j {@link ChatResponse}, 
falling back when absent.
-     */
-    public static String resolveResponseModelName(ChatResponse chatResponse, 
String fallback) {
-        if (chatResponse == null) {
-            return fallback;
+    private static String resolveLangChain4jChatModelName(ClassResolver 
classResolver, Object model) {
+        try {
+            Object parameters = invokeNoArg(classResolver, model, 
"defaultRequestParameters");
+            if (parameters != null) {
+                Object modelName = invokeNoArgOptional(classResolver, 
parameters, "modelName");
+                if (modelName != null && !modelName.toString().isBlank()) {
+                    return modelName.toString();
+                }
+            }
+        } catch (ReflectiveOperationException e) {
+            // ignore
+        }
+        return null;
+    }
+
+    private static String resolveLangChain4jEmbeddingModelName(ClassResolver 
classResolver, Object model) {
+        try {
+            Object parameters = invokeNoArgOptional(classResolver, model, 
"defaultRequestParameters");
+            if (parameters != null) {
+                Object modelName = invokeNoArgOptional(classResolver, 
parameters, "modelName");
+                if (modelName != null && !modelName.toString().isBlank()) {
+                    return modelName.toString();
+                }
+            }
+        } catch (ReflectiveOperationException e) {
+            // ignore
+        }
+        return invokeToString(classResolver, model, "modelName");
+    }
+
+    private static String resolveSystemFromSpringAiUnderlyingModel(
+            ClassResolver classResolver, Object model, Set<Integer> visited, 
int depth) {
+        try {
+            Object chatModel = invokeNoArgOptional(classResolver, model, 
"getChatModel");
+            if (chatModel != null && chatModel != model) {
+                return resolveSystem(classResolver, chatModel, visited, depth);
+            }
+        } catch (ReflectiveOperationException e) {
+            // ignore
         }
-        String modelName = chatResponse.modelName();
-        return modelName != null && !modelName.isBlank() ? modelName : 
fallback;
+        return UNKNOWN;
     }
 
-    private static String mapProvider(ModelProvider provider) {
-        if (provider == null) {
+    private static String resolveSpringAiModelName(ClassResolver 
classResolver, Object model, Set<Integer> visited, int depth) {
+        try {
+            Object chatModel = invokeNoArgOptional(classResolver, model, 
"getChatModel");
+            if (chatModel != null && chatModel != model) {
+                String resolved = resolveModelName(classResolver, chatModel, 
visited, depth);
+                if (!UNKNOWN.equals(resolved)) {
+                    return resolved;
+                }
+            }
+        } catch (ReflectiveOperationException e) {
+            // ignore
+        }
+        try {
+            Object options = invokeNoArgOptional(classResolver, model, 
"getDefaultOptions");
+            if (options == null) {
+                options = invokeNoArgOptional(classResolver, model, 
"getOptions");
+            }
+            if (options == null) {
+                options = invokeNoArgOptional(classResolver, model, 
"getDefaultChatOptions");
+            }
+            if (options != null) {
+                Object modelName = invokeNoArgOptional(classResolver, options, 
"getModel");
+                if (modelName != null && !modelName.toString().isBlank()) {
+                    return modelName.toString();
+                }
+            }
+        } catch (ReflectiveOperationException e) {
+            // ignore
+        }
+        return UNKNOWN;
+    }
+
+    private static boolean isSpringAiType(Object model) {
+        if (resolveSpringAiPackageName(model) != null) {
+            return true;
+        }
+        for (Class<?> iface : model.getClass().getInterfaces()) {
+            if (iface.getName().startsWith("org.springframework.ai.")) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static String resolveSpringAiPackageName(Object model) {
+        String packageName = model.getClass().getPackageName();
+        if (packageName.startsWith("org.springframework.ai.")) {
+            return packageName;
+        }
+        return null;
+    }
+
+    private static boolean markVisited(Object model, Set<Integer> visited) {
+        return visited.add(System.identityHashCode(model));
+    }
+
+    private static boolean isLangChain4jPresent(ClassResolver classResolver, 
Object model) {
+        if (classResolver == null) {
+            return false;
+        }
+        ClassLoader loader = model != null ? model.getClass().getClassLoader() 
: null;
+        if (loader != null) {
+            if (classResolver.resolveClass(LANGCHAIN4J_CHAT_MODEL, loader) != 
null) {
+                return true;
+            }
+            return classResolver.resolveClass(LANGCHAIN4J_EMBEDDING_MODEL, 
loader) != null;
+        }
+        return classResolver.resolveClass(LANGCHAIN4J_CHAT_MODEL) != null
+                || classResolver.resolveClass(LANGCHAIN4J_EMBEDDING_MODEL) != 
null;
+    }
+
+    private static boolean isInstanceOf(ClassResolver classResolver, Object 
model, String className) {
+        if (classResolver == null || model == null) {
+            return false;
+        }
+        Class<?> type = classResolver.resolveClass(className, 
model.getClass().getClassLoader());
+        if (type == null) {
+            type = classResolver.resolveClass(className);
+        }
+        return type != null && type.isInstance(model);
+    }
+
+    private static String invokeToString(ClassResolver classResolver, Object 
target, String methodName) {
+        try {
+            Object value = invokeNoArg(classResolver, target, methodName);
+            return value != null ? value.toString() : null;
+        } catch (ReflectiveOperationException e) {
+            return null;
+        }
+    }
+
+    private static Object invokeNoArgOptional(ClassResolver classResolver, 
Object target, String methodName)
+            throws ReflectiveOperationException {
+        try {
+            return invokeNoArg(classResolver, target, methodName);
+        } catch (NoSuchMethodException e) {
+            return null;
+        }
+    }
+
+    private static Object invokeNoArg(ClassResolver classResolver, Object 
target, String methodName)
+            throws ReflectiveOperationException {
+        Method method = resolveMethod(target.getClass(), methodName);
+        if (method == null) {
+            throw new NoSuchMethodException(target.getClass().getName() + "." 
+ methodName + "()");
+        }
+        if (!method.canAccess(target)) {
+            method.setAccessible(true);
+        }
+        return method.invoke(target);
+    }
+
+    private static Method resolveMethod(Class<?> clazz, String methodName) {
+        MethodKey key = new MethodKey(clazz.getName(), methodName);
+        SoftReference<Method> ref = METHOD_CACHE.get(key);
+        Method cached = ref != null ? ref.get() : null;
+        if (cached != null && cached.getDeclaringClass() == clazz) {
+            return cached;
+        }
+        try {
+            Method method = clazz.getMethod(methodName);
+            METHOD_CACHE.put(key, new SoftReference<>(method));
+            return method;
+        } catch (NoSuchMethodException e) {
+            return null;
+        }
+    }
+
+    private static String mapLangChain4jProvider(String provider) {
+        if (provider == null || provider.isBlank()) {
             return UNKNOWN;
         }
         return switch (provider) {
-            case OPEN_AI -> "openai";
-            case ANTHROPIC -> "anthropic";
-            case OLLAMA -> "ollama";
-            case AZURE_OPEN_AI -> "azure.ai.openai";
-            case GOOGLE_VERTEX_AI_GEMINI, GOOGLE_VERTEX_AI_ANTHROPIC -> 
"gcp.vertex_ai";
-            case GOOGLE_AI_GEMINI, GOOGLE_GENAI -> "google";
-            case MISTRAL_AI -> "mistral_ai";
-            case AMAZON_BEDROCK -> "aws.bedrock";
+            case LC4J_PROVIDER_OPEN_AI -> "openai";
+            case LC4J_PROVIDER_ANTHROPIC -> "anthropic";
+            case LC4J_PROVIDER_OLLAMA -> "ollama";
+            case LC4J_PROVIDER_AZURE_OPEN_AI -> "azure.ai.openai";
+            case LC4J_PROVIDER_GOOGLE_VERTEX_AI_GEMINI, 
LC4J_PROVIDER_GOOGLE_VERTEX_AI_ANTHROPIC -> "gcp.vertex_ai";
+            case LC4J_PROVIDER_GOOGLE_AI_GEMINI, LC4J_PROVIDER_GOOGLE_GENAI -> 
"google";
+            case LC4J_PROVIDER_MISTRAL_AI -> "mistral_ai";
+            case LC4J_PROVIDER_AMAZON_BEDROCK -> "aws.bedrock";
             default -> UNKNOWN;
         };
     }
 
+    private static String resolveSystemFromSpringAiPackage(String packageName) 
{
+        if (packageName == null) {
+            return UNKNOWN;
+        }
+        if (packageName.startsWith("org.springframework.ai.openai")) {
+            return "openai";
+        }
+        if (packageName.startsWith("org.springframework.ai.ollama")) {
+            return "ollama";
+        }
+        if (packageName.startsWith("org.springframework.ai.anthropic")) {
+            return "anthropic";
+        }
+        if (packageName.startsWith("org.springframework.ai.azure")) {
+            return "azure.ai.openai";
+        }
+        if (packageName.startsWith("org.springframework.ai.vertexai")) {
+            return "gcp.vertex_ai";
+        }
+        if (packageName.startsWith("org.springframework.ai.bedrock")) {
+            return "aws.bedrock";
+        }
+        if (packageName.startsWith("org.springframework.ai.mistralai")) {
+            return "mistral_ai";
+        }
+        if (packageName.startsWith("org.springframework.ai.deepseek")) {
+            return "deepseek";
+        }
+        if (packageName.startsWith("org.springframework.ai.chat.client")) {
+            return UNKNOWN;
+        }
+        return UNKNOWN;
+    }
+
     private static String resolveSystemFromPackage(String packageName) {
         if (packageName == null || packageName.isBlank()) {
             return UNKNOWN;
@@ -124,4 +415,11 @@ public final class GenAiModelResolver {
         }
         return UNKNOWN;
     }
+
+    private record MethodKey(String className, String methodName) {
+        private MethodKey {
+            Objects.requireNonNull(className, "className");
+            Objects.requireNonNull(methodName, "methodName");
+        }
+    }
 }
diff --git 
a/components/camel-ai/camel-ai-observability-api/src/main/java/org/apache/camel/component/ai/observability/GenAiObservabilityProperties.java
 
b/components/camel-ai/camel-ai-observability-api/src/main/java/org/apache/camel/component/ai/observability/GenAiObservabilityProperties.java
index 3f6634808220..661058b0b631 100644
--- 
a/components/camel-ai/camel-ai-observability-api/src/main/java/org/apache/camel/component/ai/observability/GenAiObservabilityProperties.java
+++ 
b/components/camel-ai/camel-ai-observability-api/src/main/java/org/apache/camel/component/ai/observability/GenAiObservabilityProperties.java
@@ -24,7 +24,7 @@ public final class GenAiObservabilityProperties {
     /**
      * Global property to enable or disable GenAI observability across all AI 
components. Default is {@code true}.
      */
-    public static final String ENABLED = "camel.ai.observability.enabled";
+    public static final String ENABLED = "camel.aiObservability.enabled";
 
     private GenAiObservabilityProperties() {
     }
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 04e3893b1427..2fe8750d7714 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
@@ -15,16 +15,25 @@ OpenTelemetry semantic conventions for GenAI (stable subset 
aligned with Spring
 AI producers depend on the lightweight `camel-ai-observability-api` module. 
Add `camel-ai-observability` to your
 application classpath together with a tracing or metrics backend to enable 
span and metric emission.
 
-When `camel-opentelemetry2` and/or `camel-micrometer` is on the classpath, 
langchain4j and OpenAI producers
-emit child spans and metrics per LLM call with attributes such as 
`gen_ai.operation.name`, `gen_ai.system`,
+When `camel-opentelemetry2` and/or `camel-micrometer` is on the classpath, 
langchain4j, OpenAI, and Spring AI chat
+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`.
 
-Global toggle: set `camel.ai.observability.enabled=false` to disable GenAI 
observability across all AI
-components (default is `true` when a tracing or metrics backend is present).
+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)`.
 
 Phase 1 coverage: `langchain4j-chat`, `langchain4j-tools`, 
`langchain4j-agent`, `langchain4j-embeddings`,
 and `openai`.
 
+Phase 3 coverage: `spring-ai-chat`.
+
+When using `spring-ai-chat` with only a pre-built `chatClient` (no `chatModel` 
on the endpoint), Camel attempts to read
+the backing Spring AI `ChatModel` via a private field on Spring AI's internal 
`ChatClientRequestSpec` implementation.
+That field is not public API and may change between Spring AI releases. If 
extraction fails, `gen_ai.system` and
+`gen_ai.request.model` span attributes may report `unknown`. Configure 
`chatModel` explicitly on the endpoint URI or
+component when GenAI observability is enabled so model metadata is always 
populated.
+
 New exchange headers for model identification on langchain4j components:
 
 * `CamelLangChain4jChatRequestModel` / `CamelLangChain4jChatResponseModel`
diff --git 
a/components/camel-ai/camel-ai-observability/src/test/java/org/apache/camel/component/ai/observability/GenAiModelResolverTest.java
 
b/components/camel-ai/camel-ai-observability/src/test/java/org/apache/camel/component/ai/observability/GenAiModelResolverTest.java
index 7e031883d8f6..f31a172680af 100644
--- 
a/components/camel-ai/camel-ai-observability/src/test/java/org/apache/camel/component/ai/observability/GenAiModelResolverTest.java
+++ 
b/components/camel-ai/camel-ai-observability/src/test/java/org/apache/camel/component/ai/observability/GenAiModelResolverTest.java
@@ -23,25 +23,42 @@ import 
dev.langchain4j.model.chat.request.ChatRequestParameters;
 import dev.langchain4j.model.chat.request.DefaultChatRequestParameters;
 import dev.langchain4j.model.chat.response.ChatResponse;
 import dev.langchain4j.model.embedding.EmbeddingModel;
+import org.apache.camel.spi.ClassResolver;
+import org.apache.camel.test.junit6.ExchangeTestSupport;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
-class GenAiModelResolverTest {
+class GenAiModelResolverTest extends ExchangeTestSupport {
+
+    private ClassResolver classResolver;
+
+    @BeforeEach
+    void setUpClassResolver() {
+        classResolver = context.getClassResolver();
+    }
+
+    @Test
+    void shouldResolveEmbeddingModelNameFromModelNameMethod() {
+        assertThat(GenAiModelResolver.resolveModelName(classResolver, new 
FakeEmbeddingModel()))
+                .isEqualTo("text-embedding-3-small");
+        assertThat(GenAiModelResolver.resolveSystem(classResolver, new 
FakeEmbeddingModel())).isEqualTo("openai");
+    }
 
     @Test
     void shouldResolveOpenAiProviderFromChatModel() {
-        assertThat(GenAiModelResolver.resolveSystem(new 
FakeOpenAiChatModel())).isEqualTo("openai");
+        assertThat(GenAiModelResolver.resolveSystem(classResolver, new 
FakeOpenAiChatModel())).isEqualTo("openai");
     }
 
     @Test
     void shouldResolveOllamaProviderFromChatModel() {
-        assertThat(GenAiModelResolver.resolveSystem(new 
FakeOllamaChatModel())).isEqualTo("ollama");
+        assertThat(GenAiModelResolver.resolveSystem(classResolver, new 
FakeOllamaChatModel())).isEqualTo("ollama");
     }
 
     @Test
     void shouldResolveModelNameFromChatModelDefaults() {
-        assertThat(GenAiModelResolver.resolveModelName(new 
FakeOpenAiChatModel())).isEqualTo("gpt-4o");
+        assertThat(GenAiModelResolver.resolveModelName(classResolver, new 
FakeOpenAiChatModel())).isEqualTo("gpt-4o");
     }
 
     @Test
@@ -50,32 +67,66 @@ class GenAiModelResolverTest {
                 .aiMessage(AiMessage.from("ok"))
                 .modelName("gpt-4o-mini")
                 .build();
-        assertThat(GenAiModelResolver.resolveResponseModelName(response, 
"gpt-4o")).isEqualTo("gpt-4o-mini");
+        assertThat(GenAiModelResolver.resolveResponseModelName(classResolver, 
response, "gpt-4o")).isEqualTo("gpt-4o-mini");
     }
 
     @Test
     void shouldNotMatchOpenAiFromUnrelatedPackageName() {
-        assertThat(GenAiModelResolver.resolveSystem(new 
UnrelatedPackageModel())).isEqualTo("unknown");
+        assertThat(GenAiModelResolver.resolveSystem(classResolver, new 
UnrelatedPackageModel())).isEqualTo("unknown");
     }
 
     @Test
     void shouldReturnUnknownForNullModel() {
-        
assertThat(GenAiModelResolver.resolveSystem(null)).isEqualTo("unknown");
-        
assertThat(GenAiModelResolver.resolveModelName(null)).isEqualTo("unknown");
+        assertThat(GenAiModelResolver.resolveSystem(classResolver, 
null)).isEqualTo("unknown");
+        assertThat(GenAiModelResolver.resolveModelName(classResolver, 
null)).isEqualTo("unknown");
     }
 
     @Test
     void shouldResolveGoogleProviderFromPackageName() {
-        assertThat(GenAiModelResolver.resolveSystem(new 
dev.langchain4j.model.google.FakeGoogleModel()))
+        assertThat(GenAiModelResolver.resolveSystem(classResolver, new 
dev.langchain4j.model.google.FakeGoogleModel()))
                 .isEqualTo("google");
     }
 
     @Test
     void shouldResolveVertexAiProviderFromPackageName() {
-        assertThat(GenAiModelResolver.resolveSystem(new 
dev.langchain4j.model.vertexai.FakeVertexAiModel()))
+        assertThat(GenAiModelResolver.resolveSystem(classResolver, new 
dev.langchain4j.model.vertexai.FakeVertexAiModel()))
                 .isEqualTo("gcp.vertex_ai");
     }
 
+    @Test
+    void shouldReturnUnknownForUnrecognizedLangChain4jProvider() {
+        assertThat(GenAiModelResolver.resolveSystem(classResolver, new 
FakeUnknownProviderChatModel())).isEqualTo("unknown");
+    }
+
+    @Test
+    void shouldMapAnthropicProviderFromChatModel() {
+        assertThat(GenAiModelResolver.resolveSystem(classResolver, new 
FakeAnthropicChatModel())).isEqualTo("anthropic");
+    }
+
+    static class FakeAnthropicChatModel implements ChatModel {
+        @Override
+        public ModelProvider provider() {
+            return ModelProvider.ANTHROPIC;
+        }
+
+        @Override
+        public ChatRequestParameters defaultRequestParameters() {
+            return 
DefaultChatRequestParameters.builder().modelName("claude-3").build();
+        }
+    }
+
+    static class FakeUnknownProviderChatModel implements ChatModel {
+        @Override
+        public ModelProvider provider() {
+            return ModelProvider.OTHER;
+        }
+
+        @Override
+        public ChatRequestParameters defaultRequestParameters() {
+            return 
DefaultChatRequestParameters.builder().modelName("custom").build();
+        }
+    }
+
     static class FakeOpenAiChatModel implements ChatModel {
         @Override
         public ModelProvider provider() {
@@ -100,6 +151,18 @@ class GenAiModelResolverTest {
         }
     }
 
+    static class FakeEmbeddingModel implements EmbeddingModel {
+        @Override
+        public ModelProvider provider() {
+            return ModelProvider.OPEN_AI;
+        }
+
+        @Override
+        public String modelName() {
+            return "text-embedding-3-small";
+        }
+    }
+
     static class UnrelatedPackageModel implements EmbeddingModel {
         @Override
         public ModelProvider provider() {
@@ -111,4 +174,47 @@ class GenAiModelResolverTest {
             return "custom";
         }
     }
+
+    @Test
+    void shouldResolveSpringAiProviderFromPackageName() {
+        assertThat(GenAiModelResolver.resolveSystem(classResolver, new 
org.springframework.ai.openai.FakeOpenAiChatModel()))
+                .isEqualTo("openai");
+    }
+
+    @Test
+    void shouldResolveSpringAiModelNameFromOptions() {
+        assertThat(GenAiModelResolver.resolveModelName(classResolver, new 
org.springframework.ai.openai.FakeOpenAiChatModel()))
+                .isEqualTo("gpt-4o");
+    }
+
+    @Test
+    void shouldResolveSpringAiResponseModelNameFromMetadata() {
+        Object response = new FakeSpringAiChatResponse("gpt-4o-mini");
+        
assertThat(GenAiModelResolver.resolveSpringAiResponseModelName(classResolver, 
response, "gpt-4o"))
+                .isEqualTo("gpt-4o-mini");
+    }
+
+    static class FakeSpringAiChatResponse {
+        private final FakeSpringAiResponseMetadata metadata;
+
+        FakeSpringAiChatResponse(String model) {
+            this.metadata = new FakeSpringAiResponseMetadata(model);
+        }
+
+        public FakeSpringAiResponseMetadata getMetadata() {
+            return metadata;
+        }
+    }
+
+    static class FakeSpringAiResponseMetadata {
+        private final String model;
+
+        FakeSpringAiResponseMetadata(String model) {
+            this.model = model;
+        }
+
+        public String getModel() {
+            return model;
+        }
+    }
 }
diff --git 
a/components/camel-ai/camel-ai-observability-api/src/main/java/org/apache/camel/component/ai/observability/GenAiObservabilityProperties.java
 
b/components/camel-ai/camel-ai-observability/src/test/java/org/springframework/ai/openai/FakeOpenAiChatModel.java
similarity index 62%
copy from 
components/camel-ai/camel-ai-observability-api/src/main/java/org/apache/camel/component/ai/observability/GenAiObservabilityProperties.java
copy to 
components/camel-ai/camel-ai-observability/src/test/java/org/springframework/ai/openai/FakeOpenAiChatModel.java
index 3f6634808220..fe36cb84ffe3 100644
--- 
a/components/camel-ai/camel-ai-observability-api/src/main/java/org/apache/camel/component/ai/observability/GenAiObservabilityProperties.java
+++ 
b/components/camel-ai/camel-ai-observability/src/test/java/org/springframework/ai/openai/FakeOpenAiChatModel.java
@@ -14,18 +14,26 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.camel.component.ai.observability;
+package org.springframework.ai.openai;
 
 /**
- * Global configuration for GenAI observability.
+ * Test stub placed in Spring AI package namespace for GenAiModelResolver 
tests.
  */
-public final class GenAiObservabilityProperties {
+public class FakeOpenAiChatModel {
 
-    /**
-     * Global property to enable or disable GenAI observability across all AI 
components. Default is {@code true}.
-     */
-    public static final String ENABLED = "camel.ai.observability.enabled";
+    public Object getDefaultOptions() {
+        return new FakeOpenAiOptions("gpt-4o");
+    }
+
+    public static final class FakeOpenAiOptions {
+        private final String model;
+
+        public FakeOpenAiOptions(String model) {
+            this.model = model;
+        }
 
-    private GenAiObservabilityProperties() {
+        public String getModel() {
+            return model;
+        }
     }
 }
diff --git 
a/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java
 
b/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java
index a021f007c1bd..07cdbaa69a47 100644
--- 
a/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java
+++ 
b/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java
@@ -170,8 +170,8 @@ public class LangChain4jAgentProducer extends 
DefaultProducer {
         Object chatModel = resolveChatModel(agent);
         GenAiObservationContext observationContext = 
GenAiObservationContext.builder()
                 .operationName(GenAiOperationName.GENERATE_CONTENT)
-                .system(GenAiModelResolver.resolveSystem(chatModel))
-                .requestModel(GenAiModelResolver.resolveModelName(chatModel))
+                
.system(GenAiModelResolver.resolveSystem(exchange.getContext().getClassResolver(),
 chatModel))
+                
.requestModel(GenAiModelResolver.resolveModelName(exchange.getContext().getClassResolver(),
 chatModel))
                 .componentScheme("langchain4j-agent")
                 .build();
         GenAiObservation observation = GenAiObservability.start(exchange, 
observationContext);
diff --git 
a/components/camel-ai/camel-langchain4j-chat/src/main/java/org/apache/camel/component/langchain4j/chat/LangChain4jChatProducer.java
 
b/components/camel-ai/camel-langchain4j-chat/src/main/java/org/apache/camel/component/langchain4j/chat/LangChain4jChatProducer.java
index 4e212db153b9..25b626d130fa 100644
--- 
a/components/camel-ai/camel-langchain4j-chat/src/main/java/org/apache/camel/component/langchain4j/chat/LangChain4jChatProducer.java
+++ 
b/components/camel-ai/camel-langchain4j-chat/src/main/java/org/apache/camel/component/langchain4j/chat/LangChain4jChatProducer.java
@@ -135,7 +135,8 @@ public class LangChain4jChatProducer extends 
DefaultProducer {
             message.setHeader(LangChain4jChatHeaders.TOTAL_TOKEN_COUNT, 
chatResponse.tokenUsage().totalTokenCount());
         }
 
-        String responseModel = 
GenAiModelResolver.resolveResponseModelName(chatResponse, requestModel);
+        String responseModel = GenAiModelResolver.resolveResponseModelName(
+                exchange.getContext().getClassResolver(), chatResponse, 
requestModel);
         if (responseModel != null) {
             message.setHeader(LangChain4jChatHeaders.RESPONSE_MODEL, 
responseModel);
         }
@@ -158,7 +159,8 @@ public class LangChain4jChatProducer extends 
DefaultProducer {
                     chatResponse.tokenUsage() != null ? 
chatResponse.tokenUsage().inputTokenCount() : null,
                     chatResponse.tokenUsage() != null ? 
chatResponse.tokenUsage().outputTokenCount() : null,
                     chatResponse.finishReason(),
-                    GenAiModelResolver.resolveResponseModelName(chatResponse, 
observationContext.requestModel())));
+                    GenAiModelResolver.resolveResponseModelName(
+                            exchange.getContext().getClassResolver(), 
chatResponse, observationContext.requestModel())));
             return extractAiResponse(chatResponse.aiMessage());
         } catch (RuntimeException e) {
             observation.recordError(e);
@@ -219,7 +221,8 @@ public class LangChain4jChatProducer extends 
DefaultProducer {
                     chatResponse.tokenUsage() != null ? 
chatResponse.tokenUsage().inputTokenCount() : null,
                     chatResponse.tokenUsage() != null ? 
chatResponse.tokenUsage().outputTokenCount() : null,
                     chatResponse.finishReason(),
-                    GenAiModelResolver.resolveResponseModelName(chatResponse, 
observationContext.requestModel())));
+                    GenAiModelResolver.resolveResponseModelName(
+                            exchange.getContext().getClassResolver(), 
chatResponse, observationContext.requestModel())));
             response = chatResponse.aiMessage();
             return extractAiResponse(response);
         } catch (RuntimeException e) {
@@ -231,10 +234,11 @@ public class LangChain4jChatProducer extends 
DefaultProducer {
     }
 
     private GenAiObservationContext buildObservationContext() {
-        String requestModel = GenAiModelResolver.resolveModelName(chatModel);
+        String requestModel
+                = 
GenAiModelResolver.resolveModelName(getEndpoint().getCamelContext().getClassResolver(),
 chatModel);
         return GenAiObservationContext.builder()
                 .operationName(GenAiOperationName.CHAT)
-                .system(GenAiModelResolver.resolveSystem(chatModel))
+                
.system(GenAiModelResolver.resolveSystem(getEndpoint().getCamelContext().getClassResolver(),
 chatModel))
                 .requestModel(requestModel)
                 .componentScheme("langchain4j-chat")
                 .build();
diff --git 
a/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsProducer.java
 
b/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsProducer.java
index 632b2f1fd776..7bb7da5c02d3 100644
--- 
a/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsProducer.java
+++ 
b/components/camel-ai/camel-langchain4j-embeddings/src/main/java/org/apache/camel/component/langchain4j/embeddings/LangChain4jEmbeddingsProducer.java
@@ -46,8 +46,8 @@ public class LangChain4jEmbeddingsProducer extends 
DefaultProducer {
         final EmbeddingModel model = 
getEndpoint().getConfiguration().getEmbeddingModel();
         GenAiObservationContext observationContext = 
GenAiObservationContext.builder()
                 .operationName(GenAiOperationName.EMBEDDINGS)
-                .system(GenAiModelResolver.resolveSystem(model))
-                .requestModel(GenAiModelResolver.resolveModelName(model))
+                
.system(GenAiModelResolver.resolveSystem(exchange.getContext().getClassResolver(),
 model))
+                
.requestModel(GenAiModelResolver.resolveModelName(exchange.getContext().getClassResolver(),
 model))
                 .componentScheme("langchain4j-embeddings")
                 .build();
         GenAiObservation observation = GenAiObservability.start(exchange, 
observationContext);
diff --git 
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/pom.xml 
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/pom.xml
index eb8face459c6..47551c3c736b 100644
--- 
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/pom.xml
+++ 
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/pom.xml
@@ -40,6 +40,12 @@
             <artifactId>camel-support</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-ai-observability-api</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
         <!-- Camel AI Tool for unified AiToolRegistry -->
         <dependency>
             <groupId>org.apache.camel</groupId>
@@ -88,6 +94,17 @@
             <artifactId>camel-test-junit6</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-ai-observability</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-telemetry</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.apache.camel</groupId>
             <artifactId>camel-test-spring-junit6</artifactId>
diff --git 
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/main/docs/spring-ai-chat-component.adoc
 
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/main/docs/spring-ai-chat-component.adoc
index eb6b2d7d4ce4..d2fd642a768a 100644
--- 
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/main/docs/spring-ai-chat-component.adoc
+++ 
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/main/docs/spring-ai-chat-component.adoc
@@ -1381,6 +1381,23 @@ is on the classpath.
 Refer to the 
https://docs.spring.io/spring-ai/reference/observability/index.html[Spring AI 
Observability documentation]
 for details on available metrics, tracing, and prompt/completion logging 
configuration.
 
+==== Camel GenAI observability
+
+When `camel-ai-observability` and a tracing backend (for example 
`camel-opentelemetry2`) are on the classpath, the
+producer emits OpenTelemetry spans with `gen_ai.*` attributes per chat call. 
Enable or disable globally with
+`camel.aiObservability.enabled` (see xref:others:ai-observability.adoc[AI 
Observability]).
+
+If the endpoint is configured with `chatClient` only (no `chatModel`), Camel 
tries to resolve the backing `ChatModel`
+for span metadata by reading a private field on Spring AI's internal 
`ChatClientRequestSpec` implementation. That is not
+public Spring AI API and may break on Spring AI upgrades. When GenAI 
observability is enabled, configure `chatModel`
+explicitly alongside `chatClient` so `gen_ai.system` and 
`gen_ai.request.model` attributes are reliably populated:
+
+[source,java]
+----
+from("direct:start")
+    .to("spring-ai-chat:chat?chatClient=#chatClient&chatModel=#chatModel");
+----
+
 === Request/Response Logging
 
 The component automatically adds Spring AI's `SimpleLoggerAdvisor` to log 
requests and responses for debugging. Enable it by setting the logger 
`org.springframework.ai.chat.client.advisor` to DEBUG level:
diff --git 
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/main/java/org/apache/camel/component/springai/chat/SpringAiChatProducer.java
 
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/main/java/org/apache/camel/component/springai/chat/SpringAiChatProducer.java
index 17089f36df31..a88ad48fd972 100644
--- 
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/main/java/org/apache/camel/component/springai/chat/SpringAiChatProducer.java
+++ 
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/main/java/org/apache/camel/component/springai/chat/SpringAiChatProducer.java
@@ -33,6 +33,12 @@ import org.apache.camel.Exchange;
 import org.apache.camel.InvalidPayloadException;
 import org.apache.camel.NoSuchHeaderException;
 import org.apache.camel.WrappedFile;
+import org.apache.camel.component.ai.observability.GenAiModelResolver;
+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.component.ai.tool.AiToolParameterHelper;
 import org.apache.camel.component.ai.tool.AiToolRegistry;
 import org.apache.camel.component.ai.tool.AiToolSpec;
@@ -41,6 +47,7 @@ import org.apache.camel.support.DefaultProducer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.chat.client.ResponseEntity;
 import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
 import org.springframework.ai.chat.client.advisor.SafeGuardAdvisor;
 import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
@@ -77,8 +84,10 @@ import org.springframework.util.MimeType;
 public class SpringAiChatProducer extends DefaultProducer {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(SpringAiChatProducer.class);
+    private static volatile boolean chatModelExtractionWarnLogged;
 
     private ChatClient chatClient;
+    private ChatModel observabilityChatModel;
     private SpringAiChatMcpManager mcpManager;
     private Advisor chatMemoryAdvisor;
 
@@ -97,16 +106,24 @@ public class SpringAiChatProducer extends DefaultProducer {
 
         // Initialize ChatClient
         ChatClient configuredClient = 
getEndpoint().getConfiguration().getChatClient();
+        ChatModel configuredChatModel = 
getEndpoint().getConfiguration().getChatModel();
         if (configuredClient != null) {
             // Use the provided ChatClient
             this.chatClient = configuredClient;
+            if (configuredChatModel != null) {
+                this.observabilityChatModel = configuredChatModel;
+            } else {
+                this.observabilityChatModel = 
extractChatModelFromClient(configuredClient);
+                warnWhenObservabilityNeedsExplicitChatModel();
+            }
         } else {
             // Create ChatClient from ChatModel
-            ChatModel chatModel = 
getEndpoint().getConfiguration().getChatModel();
+            ChatModel chatModel = configuredChatModel;
             if (chatModel == null) {
                 throw new IllegalArgumentException(
                         "Either ChatClient or ChatModel must be configured");
             }
+            this.observabilityChatModel = chatModel;
 
             ChatClient.Builder builder = ChatClient.builder(chatModel);
 
@@ -852,13 +869,23 @@ public class SpringAiChatProducer extends DefaultProducer 
{
      */
     private <T> void processEntityRequest(
             ChatClient.ChatClientRequestSpec request, Exchange exchange, 
Class<T> entityClass) {
-        // Execute the request and convert to entity
-        T entity = request.call().entity(entityClass);
-
-        // Set the entity as the body
-        exchange.getMessage().setBody(entity);
-
-        LOG.debug("Converted response to entity of type: {}", 
entityClass.getName());
+        GenAiObservationContext observationContext = buildObservationContext();
+        GenAiObservation observation = GenAiObservability.start(exchange, 
observationContext);
+        try {
+            ResponseEntity<ChatResponse, T> responseEntity = 
request.call().responseEntity(entityClass);
+            ChatResponse chatResponse = responseEntity.response();
+            T entity = responseEntity.entity();
+            recordObservationSuccess(observation, chatResponse, 
observationContext.requestModel());
+            populateTokenUsage(chatResponse, exchange);
+            populateResponseMetadata(chatResponse, exchange);
+            exchange.getMessage().setBody(entity);
+            LOG.debug("Converted response to entity of type: {}", 
entityClass.getName());
+        } catch (RuntimeException e) {
+            observation.recordError(e);
+            throw e;
+        } finally {
+            observation.close();
+        }
     }
 
     private StructuredOutputConverter<?> getStructuredOutputConverter(Exchange 
exchange) {
@@ -950,8 +977,8 @@ public class SpringAiChatProducer extends DefaultProducer {
         // Append format instructions to the request
         request.user(u -> u.text(format));
 
-        // Execute the request
-        ChatResponse response = request.call().chatResponse();
+        ChatResponse response = callWithObservability(request, exchange);
+        populateTokenUsage(response, exchange);
 
         // Get the raw response text
         String responseText = response.getResult().getOutput().getText();
@@ -965,8 +992,6 @@ public class SpringAiChatProducer extends DefaultProducer {
         // Also set headers
         exchange.getMessage().setHeader(SpringAiChatConstants.CHAT_RESPONSE, 
responseText);
         
exchange.getMessage().setHeader(SpringAiChatConstants.STRUCTURED_OUTPUT, 
structuredOutput);
-
-        populateTokenUsage(response, exchange);
     }
 
     /**
@@ -1202,11 +1227,103 @@ public class SpringAiChatProducer extends 
DefaultProducer {
         } else if (converter != null) {
             processStructuredOutputRequest(request, exchange, converter);
         } else {
-            ChatResponse response = request.call().chatResponse();
+            ChatResponse response = callWithObservability(request, exchange);
             populateResponse(response, exchange);
         }
     }
 
+    private ChatResponse 
callWithObservability(ChatClient.ChatClientRequestSpec request, Exchange 
exchange) {
+        GenAiObservationContext observationContext = buildObservationContext();
+        GenAiObservation observation = GenAiObservability.start(exchange, 
observationContext);
+        try {
+            ChatResponse response = request.call().chatResponse();
+            recordObservationSuccess(observation, response, 
observationContext.requestModel());
+            return response;
+        } catch (RuntimeException e) {
+            observation.recordError(e);
+            throw e;
+        } finally {
+            observation.close();
+        }
+    }
+
+    private void recordObservationSuccess(GenAiObservation observation, 
ChatResponse response, String requestModel) {
+        Integer inputTokens = null;
+        Integer outputTokens = null;
+        String finishReason = null;
+        String responseModel = requestModel;
+        if (response.getMetadata() != null) {
+            if (response.getMetadata().getUsage() != null) {
+                var usage = response.getMetadata().getUsage();
+                inputTokens = usage.getPromptTokens();
+                outputTokens = usage.getCompletionTokens();
+            }
+            if (response.getMetadata().getModel() != null) {
+                responseModel = response.getMetadata().getModel();
+            }
+        }
+        if (response.getResult() != null && response.getResult().getMetadata() 
!= null) {
+            finishReason = 
response.getResult().getMetadata().getFinishReason();
+        }
+        observation.recordSuccess(GenAiUsage.of(inputTokens, outputTokens, 
finishReason, responseModel));
+    }
+
+    private GenAiObservationContext buildObservationContext() {
+        Object modelSource = resolveObservabilityModelSource();
+        String requestModel = GenAiModelResolver.resolveModelName(
+                getEndpoint().getCamelContext().getClassResolver(), 
modelSource);
+        return GenAiObservationContext.builder()
+                .operationName(GenAiOperationName.CHAT)
+                
.system(GenAiModelResolver.resolveSystem(getEndpoint().getCamelContext().getClassResolver(),
 modelSource))
+                .requestModel(requestModel)
+                .componentScheme("spring-ai-chat")
+                .build();
+    }
+
+    private Object resolveObservabilityModelSource() {
+        if (observabilityChatModel != null) {
+            return observabilityChatModel;
+        }
+        ChatModel chatModel = getEndpoint().getConfiguration().getChatModel();
+        if (chatModel != null) {
+            return chatModel;
+        }
+        return chatClient;
+    }
+
+    /**
+     * Attempts to read the backing {@link ChatModel} from Spring AI's 
internal {@code ChatClientRequestSpec}
+     * implementation. This relies on a private {@code chatModel} field that 
is not part of the public Spring AI API and
+     * may change between Spring AI releases.
+     */
+    private ChatModel extractChatModelFromClient(ChatClient client) {
+        try {
+            ChatClient.ChatClientRequestSpec requestSpec = client.prompt();
+            var field = requestSpec.getClass().getDeclaredField("chatModel");
+            field.setAccessible(true);
+            return (ChatModel) field.get(requestSpec);
+        } catch (ReflectiveOperationException e) {
+            if (LOG.isDebugEnabled()) {
+                LOG.debug("Could not extract ChatModel from ChatClient for 
observability: {}", e.getMessage());
+            }
+            return null;
+        }
+    }
+
+    private void warnWhenObservabilityNeedsExplicitChatModel() {
+        if (observabilityChatModel != null || 
!GenAiObservability.isEnabled(getEndpoint().getCamelContext())) {
+            return;
+        }
+        if (!chatModelExtractionWarnLogged) {
+            chatModelExtractionWarnLogged = true;
+            LOG.warn(
+                    "GenAI observability is enabled but ChatModel could not be 
resolved from the configured ChatClient. "
+                     + "Spring AI does not expose the backing ChatModel 
publicly, so Camel reads a private field "
+                     + "that may change between releases. Configure chatModel 
explicitly on the endpoint URI or "
+                     + "component so gen_ai.system and gen_ai.request.model 
span attributes are populated.");
+        }
+    }
+
     /**
      * Build the list of default advisors based on endpoint configuration.
      * <p>
diff --git 
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/test/java/org/apache/camel/component/springai/chat/SpringAiChatObservabilityTest.java
 
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/test/java/org/apache/camel/component/springai/chat/SpringAiChatObservabilityTest.java
new file mode 100644
index 000000000000..6c04834558ae
--- /dev/null
+++ 
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/test/java/org/apache/camel/component/springai/chat/SpringAiChatObservabilityTest.java
@@ -0,0 +1,291 @@
+/*
+ * 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.springai.chat;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.CamelContextAware;
+import org.apache.camel.RoutesBuilder;
+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.component.mock.MockEndpoint;
+import org.apache.camel.spi.Registry;
+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;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.chat.model.ChatModel;
+import org.springframework.ai.openai.EntityJsonStubOpenAiChatModel;
+import org.springframework.ai.openai.StubOpenAiChatModel;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class SpringAiChatObservabilityTest extends CamelTestSupport {
+
+    private RecordingTracer tracer;
+    private ChatModel chatModel;
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext context = super.createCamelContext();
+        tracer = new RecordingTracer();
+        CamelContextAware.trySetCamelContext(tracer, context);
+        tracer.init(context);
+        return context;
+    }
+
+    @Override
+    protected void bindToRegistry(Registry registry) {
+        chatModel = new StubOpenAiChatModel();
+        registry.bind("chatModel", chatModel);
+    }
+
+    @Override
+    protected RoutesBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                SpringAiChatComponent component = new SpringAiChatComponent();
+                component.setChatModel(chatModel);
+                context.addComponent("spring-ai-chat", component);
+
+                from("direct:start")
+                        .to("spring-ai-chat:test")
+                        .to("mock:result");
+            }
+        };
+    }
+
+    @Test
+    void shouldEmitGenAiSpanAndModelHeadersFromProducer() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedMessageCount(1);
+
+        template.sendBody("direct:start", "Hello");
+
+        mock.assertIsSatisfied(10, TimeUnit.SECONDS);
+
+        
assertThat(mock.getExchanges().get(0).getMessage().getBody(String.class)).isEqualTo("Hello
 back");
+        
assertThat(mock.getExchanges().get(0).getMessage().getHeader(SpringAiChatConstants.MODEL_NAME))
+                .isEqualTo("gpt-4o-mini");
+        
assertThat(mock.getExchanges().get(0).getMessage().getHeader(SpringAiChatConstants.INPUT_TOKEN_COUNT))
+                .isEqualTo(3);
+        
assertThat(mock.getExchanges().get(0).getMessage().getHeader(SpringAiChatConstants.OUTPUT_TOKEN_COUNT))
+                .isEqualTo(2);
+
+        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.REQUEST_MODEL)).isEqualTo("gpt-4o");
+        
assertThat(tags.get(GenAiAttributes.RESPONSE_MODEL)).isEqualTo("gpt-4o-mini");
+        assertThat(tags.get(GenAiAttributes.INPUT_TOKENS)).isEqualTo("3");
+        assertThat(tags.get(GenAiAttributes.OUTPUT_TOKENS)).isEqualTo("2");
+        
assertThat(tags.get(GenAiAttributes.CAMEL_COMPONENT)).isEqualTo("spring-ai-chat");
+        assertThat(tags.get(GenAiAttributes.SYSTEM)).isEqualTo("openai");
+    }
+
+    @Test
+    void shouldResolveModelFromChatClientOnlyConfiguration() throws Exception {
+        CamelContext chatClientContext = createCamelContext();
+
+        ChatModel stubModel = new StubOpenAiChatModel();
+        ChatClient chatClient = ChatClient.builder(stubModel).build();
+        chatClientContext.getRegistry().bind("chatClient", chatClient);
+
+        chatClientContext.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:chatClientOnly")
+                        .to("spring-ai-chat:test?chatClient=#chatClient")
+                        .to("mock:chatClientResult");
+            }
+        });
+
+        chatClientContext.start();
+        try {
+            MockEndpoint mock = 
chatClientContext.getEndpoint("mock:chatClientResult", MockEndpoint.class);
+            mock.expectedMessageCount(1);
+            
chatClientContext.createProducerTemplate().sendBody("direct:chatClientOnly", 
"Hello");
+            mock.assertIsSatisfied(10, TimeUnit.SECONDS);
+
+            assertThat(tracer.genAiSpans()).hasSize(1);
+            Map<String, String> tags = tracer.genAiSpans().get(0).tags();
+            
assertThat(tags.get(GenAiAttributes.REQUEST_MODEL)).isEqualTo("gpt-4o");
+            assertThat(tags.get(GenAiAttributes.SYSTEM)).isEqualTo("openai");
+        } finally {
+            chatClientContext.stop();
+        }
+    }
+
+    @Test
+    void shouldEmitGenAiSpanForEntityConversion() throws Exception {
+        CamelContext entityContext = createCamelContext();
+
+        ChatModel entityModel = new EntityJsonStubOpenAiChatModel();
+        SpringAiChatComponent component = new SpringAiChatComponent();
+        component.setChatModel(entityModel);
+        entityContext.addComponent("spring-ai-chat", component);
+
+        entityContext.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:entity")
+                        .setHeader(SpringAiChatConstants.ENTITY_CLASS, 
constant(Person.class))
+                        .to("spring-ai-chat:test")
+                        .to("mock:entityResult");
+            }
+        });
+
+        entityContext.start();
+        try {
+            MockEndpoint mock = entityContext.getEndpoint("mock:entityResult", 
MockEndpoint.class);
+            mock.expectedMessageCount(1);
+            entityContext.createProducerTemplate().sendBody("direct:entity", 
"Who?");
+            mock.assertIsSatisfied(10, TimeUnit.SECONDS);
+
+            
assertThat(mock.getExchanges().get(0).getMessage().getBody(Person.class).name()).isEqualTo("Alice");
+            assertThat(tracer.genAiSpans()).hasSize(1);
+            Map<String, String> tags = tracer.genAiSpans().get(0).tags();
+            
assertThat(tags.get(GenAiAttributes.REQUEST_MODEL)).isEqualTo("gpt-4o");
+            
assertThat(tags.get(GenAiAttributes.RESPONSE_MODEL)).isEqualTo("gpt-4o-mini");
+            assertThat(tags.get(GenAiAttributes.INPUT_TOKENS)).isEqualTo("4");
+            assertThat(tags.get(GenAiAttributes.OUTPUT_TOKENS)).isEqualTo("3");
+        } finally {
+            entityContext.stop();
+        }
+    }
+
+    @Test
+    void shouldNotEmitGenAiSpanWhenObservabilityDisabled() throws Exception {
+        CamelContext disabledContext = createCamelContext();
+        Properties properties = new Properties();
+        properties.setProperty(GenAiObservabilityProperties.ENABLED, "false");
+        
disabledContext.getPropertiesComponent().setOverrideProperties(properties);
+
+        ChatModel stubModel = new StubOpenAiChatModel();
+        SpringAiChatComponent component = new SpringAiChatComponent();
+        component.setChatModel(stubModel);
+        disabledContext.addComponent("spring-ai-chat", component);
+
+        disabledContext.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:disabled")
+                        .to("spring-ai-chat:test")
+                        .to("mock:disabledResult");
+            }
+        });
+
+        disabledContext.start();
+        try {
+            MockEndpoint mock = 
disabledContext.getEndpoint("mock:disabledResult", MockEndpoint.class);
+            mock.expectedMessageCount(1);
+            
disabledContext.createProducerTemplate().sendBody("direct:disabled", "Hello");
+            mock.assertIsSatisfied(10, TimeUnit.SECONDS);
+            assertThat(tracer.genAiSpans()).isEmpty();
+        } finally {
+            disabledContext.stop();
+        }
+    }
+
+    record Person(String name) {
+    }
+
+    private 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();
+            }
+
+            @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
+            }
+        }
+    }
+
+    private static final class RecordingSpan implements Span {
+
+        private final Map<String, String> tags = new HashMap<>();
+
+        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-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/test/java/org/springframework/ai/openai/EntityJsonStubOpenAiChatModel.java
 
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/test/java/org/springframework/ai/openai/EntityJsonStubOpenAiChatModel.java
new file mode 100644
index 000000000000..db32b2255a74
--- /dev/null
+++ 
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/test/java/org/springframework/ai/openai/EntityJsonStubOpenAiChatModel.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.springframework.ai.openai;
+
+import java.util.List;
+
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
+import org.springframework.ai.chat.metadata.ChatResponseMetadata;
+import org.springframework.ai.chat.metadata.Usage;
+import org.springframework.ai.chat.model.ChatModel;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.model.Generation;
+import org.springframework.ai.chat.prompt.ChatOptions;
+import org.springframework.ai.chat.prompt.Prompt;
+
+/**
+ * Test stub that returns JSON suitable for Spring AI entity conversion.
+ */
+public class EntityJsonStubOpenAiChatModel implements ChatModel {
+
+    private final ChatOptions options = 
ChatOptions.builder().model("gpt-4o").build();
+
+    @Override
+    public ChatOptions getDefaultOptions() {
+        return options;
+    }
+
+    @Override
+    public ChatOptions getOptions() {
+        return options;
+    }
+
+    @Override
+    public ChatResponse call(Prompt prompt) {
+        Usage usage = new Usage() {
+            @Override
+            public Integer getPromptTokens() {
+                return 4;
+            }
+
+            @Override
+            public Integer getCompletionTokens() {
+                return 3;
+            }
+
+            @Override
+            public Integer getTotalTokens() {
+                return 7;
+            }
+
+            @Override
+            public Object getNativeUsage() {
+                return null;
+            }
+        };
+        ChatResponseMetadata metadata = ChatResponseMetadata.builder()
+                .model("gpt-4o-mini")
+                .usage(usage)
+                .build();
+        ChatGenerationMetadata generationMetadata = 
ChatGenerationMetadata.builder()
+                .finishReason("STOP")
+                .build();
+        AssistantMessage assistantMessage = new 
AssistantMessage("{\"name\":\"Alice\"}");
+        Generation generation = new Generation(assistantMessage, 
generationMetadata);
+        return new ChatResponse(List.of(generation), metadata);
+    }
+}
diff --git 
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/test/java/org/springframework/ai/openai/StubOpenAiChatModel.java
 
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/test/java/org/springframework/ai/openai/StubOpenAiChatModel.java
new file mode 100644
index 000000000000..63c81f02fa38
--- /dev/null
+++ 
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/test/java/org/springframework/ai/openai/StubOpenAiChatModel.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.springframework.ai.openai;
+
+import java.util.List;
+
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.metadata.ChatGenerationMetadata;
+import org.springframework.ai.chat.metadata.ChatResponseMetadata;
+import org.springframework.ai.chat.metadata.Usage;
+import org.springframework.ai.chat.model.ChatModel;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.model.Generation;
+import org.springframework.ai.chat.prompt.ChatOptions;
+import org.springframework.ai.chat.prompt.Prompt;
+
+/**
+ * Test stub placed in Spring AI package namespace for observability tests.
+ */
+public class StubOpenAiChatModel implements ChatModel {
+
+    private final ChatOptions options = 
ChatOptions.builder().model("gpt-4o").build();
+
+    @Override
+    public ChatOptions getDefaultOptions() {
+        return options;
+    }
+
+    @Override
+    public ChatOptions getOptions() {
+        return options;
+    }
+
+    @Override
+    public ChatResponse call(Prompt prompt) {
+        Usage usage = new Usage() {
+            @Override
+            public Integer getPromptTokens() {
+                return 3;
+            }
+
+            @Override
+            public Integer getCompletionTokens() {
+                return 2;
+            }
+
+            @Override
+            public Integer getTotalTokens() {
+                return 5;
+            }
+
+            @Override
+            public Object getNativeUsage() {
+                return null;
+            }
+        };
+        ChatResponseMetadata metadata = ChatResponseMetadata.builder()
+                .model("gpt-4o-mini")
+                .usage(usage)
+                .build();
+        ChatGenerationMetadata generationMetadata = 
ChatGenerationMetadata.builder()
+                .finishReason("STOP")
+                .build();
+        AssistantMessage assistantMessage = new AssistantMessage("Hello back");
+        Generation generation = new Generation(assistantMessage, 
generationMetadata);
+        return new ChatResponse(List.of(generation), metadata);
+    }
+}
diff --git a/core/camel-main/pom.xml b/core/camel-main/pom.xml
index f5f10f9b0c4c..634af52046be 100644
--- a/core/camel-main/pom.xml
+++ b/core/camel-main/pom.xml
@@ -83,6 +83,12 @@
             <artifactId>camel-test-junit6</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-ai-observability-api</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.apache.camel</groupId>
             <artifactId>camel-core</artifactId>
diff --git 
a/core/camel-main/src/generated/java/org/apache/camel/main/AiObservabilityConfigurationPropertiesConfigurer.java
 
b/core/camel-main/src/generated/java/org/apache/camel/main/AiObservabilityConfigurationPropertiesConfigurer.java
new file mode 100644
index 000000000000..6cf3c6d400d7
--- /dev/null
+++ 
b/core/camel-main/src/generated/java/org/apache/camel/main/AiObservabilityConfigurationPropertiesConfigurer.java
@@ -0,0 +1,60 @@
+/* Generated by camel build tools - do NOT edit this file! */
+package org.apache.camel.main;
+
+import javax.annotation.processing.Generated;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.spi.ExtendedPropertyConfigurerGetter;
+import org.apache.camel.spi.PropertyConfigurerGetter;
+import org.apache.camel.spi.ConfigurerStrategy;
+import org.apache.camel.spi.GeneratedPropertyConfigurer;
+import org.apache.camel.util.CaseInsensitiveMap;
+import org.apache.camel.main.AiObservabilityConfigurationProperties;
+
+/**
+ * Generated by camel build tools - do NOT edit this file!
+ */
+@Generated("org.apache.camel.maven.packaging.GenerateConfigurerMojo")
+@SuppressWarnings("unchecked")
+public class AiObservabilityConfigurationPropertiesConfigurer extends 
org.apache.camel.support.component.PropertyConfigurerSupport implements 
GeneratedPropertyConfigurer, ExtendedPropertyConfigurerGetter {
+
+    private static final Map<String, Object> ALL_OPTIONS;
+    static {
+        Map<String, Object> map = new CaseInsensitiveMap();
+        map.put("Enabled", boolean.class);
+        ALL_OPTIONS = map;
+    }
+
+    @Override
+    public boolean configure(CamelContext camelContext, Object obj, String 
name, Object value, boolean ignoreCase) {
+        org.apache.camel.main.AiObservabilityConfigurationProperties target = 
(org.apache.camel.main.AiObservabilityConfigurationProperties) obj;
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "enabled": target.setEnabled(property(camelContext, 
boolean.class, value)); return true;
+        default: return false;
+        }
+    }
+
+    @Override
+    public Map<String, Object> getAllOptions(Object target) {
+        return ALL_OPTIONS;
+    }
+
+    @Override
+    public Class<?> getOptionType(String name, boolean ignoreCase) {
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "enabled": return boolean.class;
+        default: return null;
+        }
+    }
+
+    @Override
+    public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
+        org.apache.camel.main.AiObservabilityConfigurationProperties target = 
(org.apache.camel.main.AiObservabilityConfigurationProperties) obj;
+        switch (ignoreCase ? name.toLowerCase() : name) {
+        case "enabled": return target.isEnabled();
+        default: return null;
+        }
+    }
+}
+
diff --git 
a/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
 
b/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
index fc0b05196272..68fd96c319ab 100644
--- 
a/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
+++ 
b/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
@@ -29,6 +29,7 @@
     { "name": "camel.resilience4j", "description": "Resilience4j EIP Circuit 
Breaker configurations", "sourceType": 
"org.apache.camel.main.Resilience4jConfigurationProperties" },
     { "name": "camel.lra", "description": "Camel Saga EIP (Long Running 
Actions) configurations", "sourceType": 
"org.apache.camel.main.LraConfigurationProperties" },
     { "name": "camel.errorRegistry", "description": "Camel Error Registry 
configurations", "sourceType": 
"org.apache.camel.main.ErrorRegistryConfigurationProperties" },
+    { "name": "camel.aiObservability", "description": "Camel GenAI 
Observability configurations", "sourceType": 
"org.apache.camel.main.AiObservabilityConfigurationProperties" },
     { "name": "camel.cluster.file", "description": "Camel File Cluster Service 
configurations", "sourceType": 
"org.apache.camel.component.file.cluster.FileLockClusterService" },
     { "name": "camel.cluster.kubernetes", "description": "Camel Kubernetes 
Cluster Service configurations", "sourceType": 
"org.apache.camel.component.kubernetes.cluster.KubernetesClusterService" },
     { "name": "camel.cluster.zookeeper", "description": "Camel ZooKeeper 
Cluster Service configurations", "sourceType": 
"org.apache.camel.component.zookeeper.cluster.ZooKeeperClusterService" },
@@ -167,6 +168,7 @@
     { "name": "camel.main.uuidGenerator", "required": false, "description": 
"UUID generator to use. default (32 bytes), short (16 bytes), classic (32 bytes 
or longer), simple (long incrementing counter), off (turned off for exchanges - 
only intended for performance profiling)", "sourceType": 
"org.apache.camel.main.DefaultConfigurationProperties", "type": "enum", 
"javaType": "java.lang.String", "defaultValue": "default", "secret": false, 
"enum": [ "classic", "default", "short", "simple",  [...]
     { "name": "camel.main.virtualThreadsEnabled", "required": false, 
"description": "Whether to enable virtual threads when creating thread pools. 
When enabled, Camel will use virtual threads instead of platform threads for 
its thread pools. This can also be enabled via the JVM system property {code 
camel.threads.virtual.enabled=true} . This option must be read early during 
bootstrap, so it is set as a system property before thread pools are created.", 
"sourceType": "org.apache.camel.mai [...]
     { "name": "camel.main.yamlDslCompactNotationWarn", "required": false, 
"description": "Whether to log a WARN when YAML DSL routes use compact 
(shorthand) notation instead of the canonical (explicit\/normalized) form. The 
canonical style is recommended as it is more tooling and AI friendly. Use Camel 
CLI to normalize existing routes: camel validate normalize &lt;file&gt;", 
"sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": 
"boolean", "javaType": "boolean", "d [...]
+    { "name": "camel.aiObservability.enabled", "required": false, 
"description": "Enables GenAI observability for Camel AI producers 
(OpenTelemetry spans and Micrometer metrics when backends are present).", 
"sourceType": "org.apache.camel.main.AiObservabilityConfigurationProperties", 
"type": "boolean", "javaType": "boolean", "defaultValue": true, "secret": false 
},
     { "name": "camel.cluster.consul.aclToken", "required": false, 
"description": "Sets the ACL token to be used with Consul", "sourceType": 
"org.apache.camel.component.consul.cluster.ConsulClusterService", "type": 
"string", "javaType": "java.lang.String", "secret": true, "security": "secret" 
},
     { "name": "camel.cluster.consul.blockSeconds", "required": false, 
"description": "The second to wait for a watch event, default 10 seconds", 
"sourceType": "org.apache.camel.component.consul.cluster.ConsulClusterService", 
"type": "integer", "javaType": "int", "defaultValue": 10, "secret": false },
     { "name": "camel.cluster.consul.configuration", "required": false, 
"description": "To use an existing configuration", "sourceType": 
"org.apache.camel.component.consul.cluster.ConsulClusterService", "type": 
"object", "javaType": 
"org.apache.camel.component.consul.cluster.ConsulClusterConfiguration", 
"secret": false },
diff --git 
a/core/camel-main/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.main.AiObservabilityConfigurationProperties
 
b/core/camel-main/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.main.AiObservabilityConfigurationProperties
new file mode 100644
index 000000000000..a8372800a3e7
--- /dev/null
+++ 
b/core/camel-main/src/generated/resources/META-INF/services/org/apache/camel/configurer/org.apache.camel.main.AiObservabilityConfigurationProperties
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.main.AiObservabilityConfigurationPropertiesConfigurer
diff --git a/core/camel-main/src/main/docs/main.adoc 
b/core/camel-main/src/main/docs/main.adoc
index 869f38ff8ff3..3cb8a5dd8622 100644
--- a/core/camel-main/src/main/docs/main.adoc
+++ b/core/camel-main/src/main/docs/main.adoc
@@ -761,6 +761,16 @@ The camel.errorRegistry supports 8 options, which are 
listed below.
 |===
 
 
+=== Camel GenAI Observability configurations
+The camel.aiObservability supports 1 options, which are listed below.
+
+[width="100%",cols="2,5,^1,2",options="header"]
+|===
+| Name | Description | Default | Type
+| *camel.aiObservability.enabled* | Enables GenAI observability for Camel AI 
producers (OpenTelemetry spans and Micrometer metrics when backends are 
present). | true | boolean
+|===
+
+
 === Camel File Cluster Service configurations
 The camel.cluster.file supports 10 options, which are listed below.
 
diff --git 
a/core/camel-main/src/main/java/org/apache/camel/main/AiObservabilityConfigurationProperties.java
 
b/core/camel-main/src/main/java/org/apache/camel/main/AiObservabilityConfigurationProperties.java
new file mode 100644
index 000000000000..1cfe5b8fbd45
--- /dev/null
+++ 
b/core/camel-main/src/main/java/org/apache/camel/main/AiObservabilityConfigurationProperties.java
@@ -0,0 +1,67 @@
+/*
+ * 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.main;
+
+import org.apache.camel.spi.BootstrapCloseable;
+import org.apache.camel.spi.Configurer;
+import org.apache.camel.spi.Metadata;
+
+/**
+ * Global configuration for GenAI observability in Camel AI components.
+ */
+@Configurer(extended = true)
+public class AiObservabilityConfigurationProperties implements 
BootstrapCloseable {
+
+    private MainConfigurationProperties parent;
+
+    @Metadata(defaultValue = "true")
+    private boolean enabled = true;
+
+    public AiObservabilityConfigurationProperties(MainConfigurationProperties 
parent) {
+        this.parent = parent;
+    }
+
+    public MainConfigurationProperties end() {
+        return parent;
+    }
+
+    @Override
+    public void close() {
+        parent = null;
+    }
+
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    /**
+     * Enables GenAI observability for Camel AI producers (OpenTelemetry spans 
and Micrometer metrics when backends are
+     * present).
+     */
+    public void setEnabled(boolean enabled) {
+        this.enabled = enabled;
+    }
+
+    /**
+     * Enables GenAI observability for Camel AI producers (OpenTelemetry spans 
and Micrometer metrics when backends are
+     * present).
+     */
+    public AiObservabilityConfigurationProperties withEnabled(boolean enabled) 
{
+        this.enabled = enabled;
+        return this;
+    }
+}
diff --git 
a/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java 
b/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java
index b9205a0a73b0..b3ac04de54e1 100644
--- a/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java
+++ b/core/camel-main/src/main/java/org/apache/camel/main/BaseMainSupport.java
@@ -150,6 +150,7 @@ public abstract class BaseMainSupport extends BaseService {
     private static final String PREFIX_TRACE = "camel.trace.";
     private static final String PREFIX_ROUTE_CONTROLLER = 
"camel.routeController.";
     private static final String PREFIX_ERROR_REGISTRY = "camel.errorRegistry.";
+    private static final String PREFIX_AI_OBSERVABILITY = 
"camel.aiObservability.";
 
     private static final String[] GROUP_PREFIXES = new String[] {
             "camel.context.", "camel.resilience4j.", "camel.faulttolerance.",
@@ -158,7 +159,7 @@ public abstract class BaseMainSupport extends BaseService {
             "camel.telemetryDev.", "camel.management.", "camel.mdc.", 
"camel.metrics.", "camel.routeTemplate",
             "camel.devConsole.", "camel.variable.", "camel.beans.", 
"camel.globalOptions.",
             PREFIX_SERVER, PREFIX_SSL, PREFIX_SECURITY, PREFIX_DEBUG, 
PREFIX_TRACE,
-            PREFIX_ROUTE_CONTROLLER, PREFIX_ERROR_REGISTRY };
+            PREFIX_ROUTE_CONTROLLER, PREFIX_ERROR_REGISTRY, 
PREFIX_AI_OBSERVABILITY };
 
     protected final List<MainListener> listeners = new ArrayList<>();
     protected volatile CamelContext camelContext;
@@ -1421,6 +1422,7 @@ public abstract class BaseMainSupport extends BaseService 
{
         OrderedLocationProperties tracerProperties = new 
OrderedLocationProperties();
         OrderedLocationProperties routeControllerProperties = new 
OrderedLocationProperties();
         OrderedLocationProperties errorRegistryProperties = new 
OrderedLocationProperties();
+        OrderedLocationProperties aiObservabilityProperties = new 
OrderedLocationProperties();
 
         for (String key : prop.stringPropertyNames()) {
             String loc = prop.getLocation(key);
@@ -1580,6 +1582,12 @@ public abstract class BaseMainSupport extends 
BaseService {
                 String option = key.substring(20);
                 validateOptionAndValue(key, option, value);
                 errorRegistryProperties.put(loc, optionKey(option), value);
+            } else if (startsWithIgnoreCase(key, PREFIX_AI_OBSERVABILITY)) {
+                // grab the value
+                String value = prop.getProperty(key);
+                String option = 
key.substring(PREFIX_AI_OBSERVABILITY.length());
+                validateOptionAndValue(key, option, value);
+                aiObservabilityProperties.put(loc, optionKey(option), value);
             }
         }
 
@@ -1734,6 +1742,12 @@ public abstract class BaseMainSupport extends 
BaseService {
                     mainConfigurationProperties.isAutoConfigurationFailFast(),
                     autoConfiguredProperties);
         }
+        if (!aiObservabilityProperties.isEmpty() || 
mainConfigurationProperties.hasAiObservabilityConfiguration()) {
+            LOG.debug("Auto-configuring GenAI observability from loaded 
properties: {}", aiObservabilityProperties.size());
+            setAiObservabilityProperties(camelContext, 
aiObservabilityProperties,
+                    mainConfigurationProperties.isAutoConfigurationFailFast(),
+                    autoConfiguredProperties);
+        }
 
         // configure which requires access to the model
         MainSupportModelConfigurer.configureModelCamelContext(camelContext, 
mainConfigurationProperties,
@@ -2606,6 +2620,26 @@ public abstract class BaseMainSupport extends 
BaseService {
         
registry.setIncludeExchangeVariables(config.isIncludeExchangeVariables());
     }
 
+    private void setAiObservabilityProperties(
+            CamelContext camelContext, OrderedLocationProperties properties,
+            boolean failIfNotSet, OrderedLocationProperties 
autoConfiguredProperties)
+            throws Exception {
+
+        AiObservabilityConfigurationProperties config = 
mainConfigurationProperties.aiObservability();
+        setPropertiesOnTarget(camelContext, config, properties, 
PREFIX_AI_OBSERVABILITY,
+                failIfNotSet, true, autoConfiguredProperties);
+
+        if (mainConfigurationProperties.hasAiObservabilityConfiguration() || 
!properties.isEmpty()) {
+            PropertiesComponent pc = camelContext.getPropertiesComponent();
+            Properties local = pc.getLocalProperties();
+            if (local == null) {
+                local = new Properties();
+                pc.setLocalProperties(local);
+            }
+            local.setProperty("camel.aiObservability.enabled", 
Boolean.toString(config.isEnabled()));
+        }
+    }
+
     private void bindBeansToRegistry(
             CamelContext camelContext, OrderedLocationProperties properties,
             String optionPrefix, boolean failIfNotSet, boolean logSummary, 
boolean ignoreCase,
diff --git 
a/core/camel-main/src/main/java/org/apache/camel/main/MainConfigurationProperties.java
 
b/core/camel-main/src/main/java/org/apache/camel/main/MainConfigurationProperties.java
index 7502b3702ed4..12fba7360b41 100644
--- 
a/core/camel-main/src/main/java/org/apache/camel/main/MainConfigurationProperties.java
+++ 
b/core/camel-main/src/main/java/org/apache/camel/main/MainConfigurationProperties.java
@@ -75,6 +75,7 @@ public class MainConfigurationProperties extends 
DefaultConfigurationProperties<
     private TracerConfigurationProperties tracerConfigurationProperties;
     private RouteControllerConfigurationProperties 
routeControllerConfigurationProperties;
     private ErrorRegistryConfigurationProperties 
errorRegistryConfigurationProperties;
+    private AiObservabilityConfigurationProperties 
aiObservabilityConfigurationProperties;
 
     @Override
     public void close() {
@@ -158,6 +159,10 @@ public class MainConfigurationProperties extends 
DefaultConfigurationProperties<
             errorRegistryConfigurationProperties.close();
             errorRegistryConfigurationProperties = null;
         }
+        if (aiObservabilityConfigurationProperties != null) {
+            aiObservabilityConfigurationProperties.close();
+            aiObservabilityConfigurationProperties = null;
+        }
         if (routesBuilders != null) {
             routesBuilders.clear();
             routesBuilders = null;
@@ -397,6 +402,23 @@ public class MainConfigurationProperties extends 
DefaultConfigurationProperties<
         return errorRegistryConfigurationProperties != null;
     }
 
+    /**
+     * To configure GenAI observability.
+     */
+    public AiObservabilityConfigurationProperties aiObservability() {
+        if (aiObservabilityConfigurationProperties == null) {
+            aiObservabilityConfigurationProperties = new 
AiObservabilityConfigurationProperties(this);
+        }
+        return aiObservabilityConfigurationProperties;
+    }
+
+    /**
+     * Whether there has been any GenAI observability configuration specified.
+     */
+    public boolean hasAiObservabilityConfiguration() {
+        return aiObservabilityConfigurationProperties != null;
+    }
+
     /**
      * To configure Route Controller.
      */
diff --git 
a/core/camel-main/src/test/java/org/apache/camel/main/AiObservabilityConfigurationPropertiesTest.java
 
b/core/camel-main/src/test/java/org/apache/camel/main/AiObservabilityConfigurationPropertiesTest.java
new file mode 100644
index 000000000000..9af3f22c4e9a
--- /dev/null
+++ 
b/core/camel-main/src/test/java/org/apache/camel/main/AiObservabilityConfigurationPropertiesTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.main;
+
+import org.apache.camel.component.ai.observability.GenAiObservability;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class AiObservabilityConfigurationPropertiesTest {
+
+    @Test
+    void shouldDisableGenAiObservabilityViaMainConfiguration() throws 
Exception {
+        Main main = new Main();
+        main.configure().aiObservability().withEnabled(false);
+
+        main.start();
+
+        try {
+            
assertThat(GenAiObservability.isEnabled(main.getCamelContext())).isFalse();
+        } finally {
+            main.stop();
+        }
+    }
+
+    @Test
+    void shouldDisableGenAiObservabilityViaApplicationProperties() throws 
Exception {
+        Main main = new Main();
+        
main.setDefaultPropertyPlaceholderLocation("classpath:ai-observability.properties");
+
+        main.start();
+
+        try {
+            
assertThat(GenAiObservability.isEnabled(main.getCamelContext())).isFalse();
+        } finally {
+            main.stop();
+        }
+    }
+
+    @Test
+    void shouldEnableGenAiObservabilityByDefault() throws Exception {
+        Main main = new Main();
+        main.start();
+
+        try {
+            
assertThat(GenAiObservability.isEnabled(main.getCamelContext())).isTrue();
+        } finally {
+            main.stop();
+        }
+    }
+}
diff --git a/core/camel-main/src/test/resources/ai-observability.properties 
b/core/camel-main/src/test/resources/ai-observability.properties
new file mode 100644
index 000000000000..e4967a71c807
--- /dev/null
+++ b/core/camel-main/src/test/resources/ai-observability.properties
@@ -0,0 +1,17 @@
+## ---------------------------------------------------------------------------
+## 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.
+## ---------------------------------------------------------------------------
+camel.aiObservability.enabled = false
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 3cfd929a64b8..960ff47f0062 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
@@ -166,7 +166,10 @@ unchanged and still does not permit any of the ranges 
above.
 
 LangChain4j and OpenAI producers now emit GenAI observability data 
(OpenTelemetry span attributes and
 Micrometer metrics) when `camel-opentelemetry2` and/or `camel-micrometer` is 
on the classpath.
-Disable globally with `camel.ai.observability.enabled=false` (default is 
enabled).
+Disable globally with `camel.aiObservability.enabled=false` (default is 
enabled). Camel Main also
+exposes the same setting via 
`main.configure().aiObservability().withEnabled(false)`.
+
+Spring AI chat (`spring-ai-chat`) producers emit the same GenAI spans and 
metrics in Phase 3.
 
 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.
diff --git 
a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCamelMainMojo.java
 
b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCamelMainMojo.java
index 17f4e00f5131..05f4e1423cf0 100644
--- 
a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCamelMainMojo.java
+++ 
b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCamelMainMojo.java
@@ -333,6 +333,8 @@ public class PrepareCamelMainMojo extends 
AbstractGeneratorMojo {
                     prefix = "camel.trace.";
                 } else if 
(file.getName().contains("RouteControllerConfigurationProperties")) {
                     prefix = "camel.routecontroller.";
+                } else if 
(file.getName().contains("AiObservabilityConfigurationProperties")) {
+                    prefix = "camel.aiObservability.";
                 } else {
                     prefix = "camel.main.";
                 }
@@ -594,6 +596,9 @@ public class PrepareCamelMainMojo extends 
AbstractGeneratorMojo {
             model.getGroups().add(new MainGroupModel(
                     "camel.errorRegistry", "Camel Error Registry 
configurations",
                     
"org.apache.camel.main.ErrorRegistryConfigurationProperties"));
+            model.getGroups().add(new MainGroupModel(
+                    "camel.aiObservability", "Camel GenAI Observability 
configurations",
+                    
"org.apache.camel.main.AiObservabilityConfigurationProperties"));
             model.getGroups().add(new MainGroupModel(
                     "camel.cluster.file", "Camel File Cluster Service 
configurations",
                     
"org.apache.camel.component.file.cluster.FileLockClusterService"));

Reply via email to