davsclaus commented on code in PR #25507:
URL: https://github.com/apache/camel/pull/25507#discussion_r3842428037


##########
components/camel-ai/camel-ai-observability-api/src/main/java/org/apache/camel/component/ai/observability/GenAiObservabilityProperties.java:
##########
@@ -24,7 +24,12 @@ 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";
+
+    /**
+     * Dash-style alias for {@link #ENABLED}, accepted in 
application.properties and resolved by Camel Main.
+     */
+    public static final String ENABLED_DASH = "camel.ai-observability.enabled";

Review Comment:
   This dash-style alias is read directly via 
`PropertiesComponent.resolveProperty()` (see `GenAiObservability.isEnabled()`), 
bypassing Camel Main's `setPropertiesOnTarget`/`validateOptionAndValue` path 
that the camelCase `ENABLED` key goes through. That means a typo in this 
spelling is silently ignored instead of failing fast, and there's no precedent 
for a dash-case alias elsewhere in `camel-main`'s config groups. Please drop 
this alias or add explicit unknown-option validation for it.



##########
components/camel-ai/camel-ai-observability-api/src/main/java/org/apache/camel/component/ai/observability/GenAiObservability.java:
##########
@@ -52,6 +52,9 @@ public static boolean isEnabled(CamelContext camelContext) {
         }
         Optional<String> property
                 = 
camelContext.getPropertiesComponent().resolveProperty(GenAiObservabilityProperties.ENABLED);
+        if (property.isEmpty()) {
+            property = 
camelContext.getPropertiesComponent().resolveProperty(GenAiObservabilityProperties.ENABLED_DASH);

Review Comment:
   Same concern as the `ENABLED_DASH` definition: this fallback lookup has no 
typo/unknown-key protection since it's a raw property resolution outside Main's 
configurer-validation path.



##########
components/camel-ai/camel-ai-observability-api/src/main/java/org/apache/camel/component/ai/observability/GenAiModelResolver.java:
##########
@@ -16,81 +16,301 @@
  */
 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.util.HashSet;
+import java.util.Set;
 
 /**
- * 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.
  */
 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";
+
+    private static volatile Boolean langChain4jPresent;
 
     private GenAiModelResolver() {
     }
 
     public static String resolveSystem(Object model) {
-        if (model == null) {
+        return resolveSystem(model, new HashSet<>(), 0);
+    }
+
+    public static String resolveModelName(Object model) {
+        return resolveModelName(model, new HashSet<>(), 0);
+    }
+
+    /**
+     * Resolves the response model from a LangChain4j or Spring AI chat 
response, falling back when absent.
+     */
+    public static String resolveResponseModelName(Object chatResponse, String 
fallback) {
+        if (chatResponse == null) {
+            return fallback;
+        }
+        if (isLangChain4jPresent() && isInstanceOf(chatResponse, 
LANGCHAIN4J_CHAT_RESPONSE)) {
+            String modelName = invokeToString(chatResponse, "modelName");
+            if (modelName != null && !modelName.isBlank()) {
+                return modelName;
+            }
+        }
+        return resolveSpringAiResponseModelName(chatResponse, fallback);
+    }
+
+    /**
+     * Resolves the response model from a Spring AI {@code ChatResponse}, 
falling back when absent.
+     */
+    public static String resolveSpringAiResponseModelName(Object chatResponse, 
String fallback) {
+        if (chatResponse == null) {
+            return fallback;
+        }
+        try {
+            Object metadata = 
chatResponse.getClass().getMethod("getMetadata").invoke(chatResponse);
+            if (metadata != null) {
+                Object model = 
metadata.getClass().getMethod("getModel").invoke(metadata);
+                if (model != null && !model.toString().isBlank()) {
+                    return model.toString();
+                }
+            }
+        } catch (ReflectiveOperationException e) {
+            // ignore
+        }
+        return fallback;
+    }
+
+    private static String resolveSystem(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) {
-            return mapProvider(chatModel.provider());
+        if (isLangChain4jPresent() && isInstanceOf(model, 
LANGCHAIN4J_CHAT_MODEL)) {
+            return mapLangChain4jProvider(invokeToString(model, "provider"));
         }
-        if (model instanceof EmbeddingModel embeddingModel) {
-            return mapProvider(embeddingModel.provider());
+        if (isLangChain4jPresent() && isInstanceOf(model, 
LANGCHAIN4J_EMBEDDING_MODEL)) {
+            return mapLangChain4jProvider(invokeToString(model, "provider"));
+        }
+        if (isSpringAiType(model)) {
+            String fromUnderlyingModel = 
resolveSystemFromSpringAiUnderlyingModel(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(Object model, Set<Integer> visited, 
int depth) {
+        if (model == null || depth > MAX_MODEL_UNWRAP_DEPTH) {
             return UNKNOWN;
         }
-        if (model instanceof ChatModel chatModel) {
-            String modelName = 
chatModel.defaultRequestParameters().modelName();
+        if (!markVisited(model, visited)) {
+            return UNKNOWN;
+        }
+        if (isLangChain4jPresent() && isInstanceOf(model, 
LANGCHAIN4J_CHAT_MODEL)) {
+            String modelName = resolveLangChain4jChatModelName(model);
             if (modelName != null && !modelName.isBlank()) {
                 return modelName;
             }
         }
-        if (model instanceof EmbeddingModel embeddingModel) {
-            String modelName = embeddingModel.modelName();
+        if (isLangChain4jPresent() && isInstanceOf(model, 
LANGCHAIN4J_EMBEDDING_MODEL)) {
+            String modelName = invokeToString(model, "modelName");
             if (modelName != null && !modelName.isBlank()) {
                 return modelName;
             }
         }
+        if (isSpringAiType(model)) {
+            return resolveSpringAiModelName(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(Object model) {
+        try {
+            Object parameters = invokeNoArg(model, "defaultRequestParameters");
+            if (parameters != null) {
+                Object modelName = invokeNoArgOptional(parameters, 
"modelName");
+                if (modelName != null && !modelName.toString().isBlank()) {
+                    return modelName.toString();
+                }
+            }
+        } catch (ReflectiveOperationException e) {
+            // ignore
+        }
+        return null;
+    }
+
+    private static String resolveSystemFromSpringAiUnderlyingModel(Object 
model, Set<Integer> visited, int depth) {
+        try {
+            Object chatModel = invokeNoArgOptional(model, "getChatModel");
+            if (chatModel != null && chatModel != model) {
+                return resolveSystem(chatModel, visited, depth);
+            }
+        } catch (ReflectiveOperationException e) {
+            // ignore
+        }
+        return UNKNOWN;
+    }
+
+    private static String resolveSpringAiModelName(Object model, Set<Integer> 
visited, int depth) {
+        try {
+            Object chatModel = invokeNoArgOptional(model, "getChatModel");
+            if (chatModel != null && chatModel != model) {
+                String resolved = resolveModelName(chatModel, visited, depth);
+                if (!UNKNOWN.equals(resolved)) {
+                    return resolved;
+                }
+            }
+        } catch (ReflectiveOperationException e) {
+            // ignore
+        }
+        try {
+            Object options = invokeNoArgOptional(model, "getDefaultOptions");
+            if (options == null) {
+                options = invokeNoArgOptional(model, "getOptions");
+            }
+            if (options == null) {
+                options = invokeNoArgOptional(model, "getDefaultChatOptions");
+            }
+            if (options != null) {
+                Object modelName = invokeNoArgOptional(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() {

Review Comment:
   `Class.forName` is called here using the resolver's own classloader rather 
than Camel's `ClassResolver` SPI (`camelContext.getClassResolver()`), which is 
the convention used elsewhere in the codebase for classloading that needs to 
work under OSGi/modular runtimes. The result is also cached in a single 
`volatile Boolean`, which works for this one presence check, but the pattern 
below (`isInstanceOf`/`invokeNoArg`) repeats uncached 
`Class.forName`/reflective method lookups on every call.



##########
components/camel-ai/camel-ai-observability-api/src/main/java/org/apache/camel/component/ai/observability/GenAiModelResolver.java:
##########
@@ -16,81 +16,301 @@
  */
 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.util.HashSet;
+import java.util.Set;
 
 /**
- * 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.
  */
 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";
+
+    private static volatile Boolean langChain4jPresent;
 
     private GenAiModelResolver() {
     }
 
     public static String resolveSystem(Object model) {
-        if (model == null) {
+        return resolveSystem(model, new HashSet<>(), 0);
+    }
+
+    public static String resolveModelName(Object model) {
+        return resolveModelName(model, new HashSet<>(), 0);
+    }
+
+    /**
+     * Resolves the response model from a LangChain4j or Spring AI chat 
response, falling back when absent.
+     */
+    public static String resolveResponseModelName(Object chatResponse, String 
fallback) {
+        if (chatResponse == null) {
+            return fallback;
+        }
+        if (isLangChain4jPresent() && isInstanceOf(chatResponse, 
LANGCHAIN4J_CHAT_RESPONSE)) {
+            String modelName = invokeToString(chatResponse, "modelName");
+            if (modelName != null && !modelName.isBlank()) {
+                return modelName;
+            }
+        }
+        return resolveSpringAiResponseModelName(chatResponse, fallback);
+    }
+
+    /**
+     * Resolves the response model from a Spring AI {@code ChatResponse}, 
falling back when absent.
+     */
+    public static String resolveSpringAiResponseModelName(Object chatResponse, 
String fallback) {
+        if (chatResponse == null) {
+            return fallback;
+        }
+        try {
+            Object metadata = 
chatResponse.getClass().getMethod("getMetadata").invoke(chatResponse);
+            if (metadata != null) {
+                Object model = 
metadata.getClass().getMethod("getModel").invoke(metadata);
+                if (model != null && !model.toString().isBlank()) {
+                    return model.toString();
+                }
+            }
+        } catch (ReflectiveOperationException e) {
+            // ignore
+        }
+        return fallback;
+    }
+
+    private static String resolveSystem(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) {
-            return mapProvider(chatModel.provider());
+        if (isLangChain4jPresent() && isInstanceOf(model, 
LANGCHAIN4J_CHAT_MODEL)) {
+            return mapLangChain4jProvider(invokeToString(model, "provider"));
         }
-        if (model instanceof EmbeddingModel embeddingModel) {
-            return mapProvider(embeddingModel.provider());
+        if (isLangChain4jPresent() && isInstanceOf(model, 
LANGCHAIN4J_EMBEDDING_MODEL)) {
+            return mapLangChain4jProvider(invokeToString(model, "provider"));
+        }
+        if (isSpringAiType(model)) {
+            String fromUnderlyingModel = 
resolveSystemFromSpringAiUnderlyingModel(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(Object model, Set<Integer> visited, 
int depth) {
+        if (model == null || depth > MAX_MODEL_UNWRAP_DEPTH) {
             return UNKNOWN;
         }
-        if (model instanceof ChatModel chatModel) {
-            String modelName = 
chatModel.defaultRequestParameters().modelName();
+        if (!markVisited(model, visited)) {
+            return UNKNOWN;
+        }
+        if (isLangChain4jPresent() && isInstanceOf(model, 
LANGCHAIN4J_CHAT_MODEL)) {
+            String modelName = resolveLangChain4jChatModelName(model);
             if (modelName != null && !modelName.isBlank()) {
                 return modelName;
             }
         }
-        if (model instanceof EmbeddingModel embeddingModel) {
-            String modelName = embeddingModel.modelName();
+        if (isLangChain4jPresent() && isInstanceOf(model, 
LANGCHAIN4J_EMBEDDING_MODEL)) {
+            String modelName = invokeToString(model, "modelName");
             if (modelName != null && !modelName.isBlank()) {
                 return modelName;
             }
         }
+        if (isSpringAiType(model)) {
+            return resolveSpringAiModelName(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(Object model) {
+        try {
+            Object parameters = invokeNoArg(model, "defaultRequestParameters");
+            if (parameters != null) {
+                Object modelName = invokeNoArgOptional(parameters, 
"modelName");
+                if (modelName != null && !modelName.toString().isBlank()) {
+                    return modelName.toString();
+                }
+            }
+        } catch (ReflectiveOperationException e) {
+            // ignore
+        }
+        return null;
+    }
+
+    private static String resolveSystemFromSpringAiUnderlyingModel(Object 
model, Set<Integer> visited, int depth) {
+        try {
+            Object chatModel = invokeNoArgOptional(model, "getChatModel");
+            if (chatModel != null && chatModel != model) {
+                return resolveSystem(chatModel, visited, depth);
+            }
+        } catch (ReflectiveOperationException e) {
+            // ignore
+        }
+        return UNKNOWN;
+    }
+
+    private static String resolveSpringAiModelName(Object model, Set<Integer> 
visited, int depth) {
+        try {
+            Object chatModel = invokeNoArgOptional(model, "getChatModel");
+            if (chatModel != null && chatModel != model) {
+                String resolved = resolveModelName(chatModel, visited, depth);
+                if (!UNKNOWN.equals(resolved)) {
+                    return resolved;
+                }
+            }
+        } catch (ReflectiveOperationException e) {
+            // ignore
+        }
+        try {
+            Object options = invokeNoArgOptional(model, "getDefaultOptions");
+            if (options == null) {
+                options = invokeNoArgOptional(model, "getOptions");
+            }
+            if (options == null) {
+                options = invokeNoArgOptional(model, "getDefaultChatOptions");
+            }
+            if (options != null) {
+                Object modelName = invokeNoArgOptional(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() {
+        if (langChain4jPresent == null) {
+            try {
+                Class.forName(LANGCHAIN4J_CHAT_MODEL, false, 
GenAiModelResolver.class.getClassLoader());
+                langChain4jPresent = true;
+            } catch (ClassNotFoundException e) {
+                langChain4jPresent = false;
+            }
         }
-        String modelName = chatResponse.modelName();
-        return modelName != null && !modelName.isBlank() ? modelName : 
fallback;
+        return langChain4jPresent;
     }
 
-    private static String mapProvider(ModelProvider provider) {
-        if (provider == null) {
+    private static boolean isInstanceOf(Object model, String className) {
+        try {
+            Class<?> type = Class.forName(className, false, 
model.getClass().getClassLoader());
+            return type.isInstance(model);
+        } catch (ClassNotFoundException e) {
+            return false;
+        }
+    }
+
+    private static String invokeToString(Object target, String methodName) {
+        try {
+            Object value = invokeNoArg(target, methodName);
+            return value != null ? value.toString() : null;
+        } catch (ReflectiveOperationException e) {
+            return null;
+        }
+    }
+
+    private static Object invokeNoArgOptional(Object target, String 
methodName) throws ReflectiveOperationException {
+        try {
+            return invokeNoArg(target, methodName);
+        } catch (NoSuchMethodException e) {
+            return null;
+        }
+    }
+
+    private static Object invokeNoArg(Object target, String methodName) throws 
ReflectiveOperationException {

Review Comment:
   `getClass().getMethod(methodName).invoke(target)` is looked up fresh on 
every call with no caching of the resolved `Method`. Since this runs on every 
chat/embedding call (a hot path for high-throughput AI routes), please cache 
resolved `Method` objects (e.g., keyed by `Class<?>` + method name) rather than 
re-resolving via reflection each time.



##########
components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/main/java/org/apache/camel/component/springai/chat/SpringAiChatProducer.java:
##########
@@ -1202,11 +1225,81 @@ private void 
executeRequest(ChatClient.ChatClientRequestSpec request, Exchange e
         } 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(modelSource);
+        return GenAiObservationContext.builder()
+                .operationName(GenAiOperationName.CHAT)
+                .system(GenAiModelResolver.resolveSystem(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;
+    }
+
+    private ChatModel extractChatModelFromClient(ChatClient client) {

Review Comment:
   Non-blocking: this reaches into `ChatClient.ChatClientRequestSpec`'s private 
`chatModel` field via reflection, which isn't public Spring AI API. If a future 
Spring AI version renames/restructures this field, observability silently 
degrades to `system=unknown` for chatClient-only configurations -- the failure 
is only logged at DEBUG. Consider a one-time WARN so operators notice after a 
Spring AI upgrade.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to