Spring Boot auto-configuration for the ChatClient API in Spring AI applications
Spring Boot auto-configuration for the ChatClient API in Spring AI applications. This module automatically configures ChatClient.Builder beans with prototype scope, ensuring each injection point receives a newly cloned builder instance with integrated observability support.
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-client</artifactId>
<version>1.1.2</version>
</dependency>Prerequisites:
Transitive Dependencies:
org.springframework.ai:spring-ai-client-chat - Provides the core ChatClient interfaceorg.springframework.boot:spring-boot-autoconfigure - Spring Boot auto-configuration supportio.micrometer:micrometer-observation - Observability support (optional)io.micrometer:micrometer-tracing - Distributed tracing support (optional)This is an auto-configuration module that works through Spring Boot's auto-configuration mechanism. No direct imports are needed for basic usage. The beans are created automatically when the module is on the classpath.
For extension points and customization:
// Core ChatClient API
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.ChatClient.Builder;
// Customization interface
import org.springframework.ai.chat.client.ChatClientCustomizer;
// Configuration properties
import org.springframework.ai.model.chat.client.autoconfigure.ChatClientBuilderProperties;
// Advanced: Builder configuration utility
import org.springframework.ai.model.chat.client.autoconfigure.ChatClientBuilderConfigurer;
// Observability (optional)
import org.springframework.ai.chat.client.observation.ChatClientObservationConvention;
import org.springframework.ai.chat.client.observation.ChatClientObservationContext;
import org.springframework.ai.chat.client.observation.ChatClientPromptContentObservationHandler;
import org.springframework.ai.chat.client.observation.ChatClientCompletionObservationHandler;
// Auto-configuration class (for exclusion scenarios)
import org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration;The auto-configuration automatically creates a ChatClient.Builder bean when the module is present. Simply inject it where needed:
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class MyAIService {
private final ChatClient.Builder chatClientBuilder;
@Autowired
public MyAIService(ChatClient.Builder chatClientBuilder) {
this.chatClientBuilder = chatClientBuilder;
}
public String askQuestion(String question) {
ChatClient chatClient = chatClientBuilder.build();
return chatClient.prompt()
.user(question)
.call()
.content();
}
}Important: The ChatClient.Builder bean has prototype scope, meaning each injection point receives a newly cloned instance. This prevents shared state between different parts of your application.
Bean Scope Behavior:
ChatClient.Builder multiple times (even in the same class), each injection receives a separate instanceChatClientCustomizer beans are applied to each cloned instanceChatClient instances in your applicationAutomatic configuration of ChatClient.Builder beans with integrated observability support.
The auto-configuration activates when:
ChatClient.class is on the classpathChatModel bean is available in the Spring contextspring.ai.chat.client.enabled is true (default)Key Behaviors:
@Autowired injection of ChatClient.Builder receives a newly cloned instanceChatClientCustomizer beans during builder creationExecution Order:
The auto-configuration runs after Spring Boot's ObservationAutoConfiguration, ensuring observation infrastructure is available before ChatClient beans are created.
Auto-Configuration Discovery:
This auto-configuration is registered in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports and is automatically discovered by Spring Boot's auto-configuration mechanism.
Disabling Auto-Configuration:
Method 1 - Via properties:
spring:
ai:
chat:
client:
enabled: falseMethod 2 - Exclude the auto-configuration class:
@SpringBootApplication(exclude = {ChatClientAutoConfiguration.class})
public class MyApplication {
// ...
}Method 3 - In application.properties:
spring.autoconfigure.exclude=org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfigurationWhat Happens When Disabled:
ChatClient.Builder bean is created automaticallyChatClientCustomizer beans will not be applied automaticallyConfigure the ChatClient auto-configuration behavior through application properties.
/**
* Configuration properties for ChatClient builder.
* Property prefix: spring.ai.chat.client
* Package: org.springframework.ai.model.chat.client.autoconfigure
*/
@ConfigurationProperties("spring.ai.chat.client")
public class ChatClientBuilderProperties {
/**
* Configuration prefix constant.
* Value: "spring.ai.chat.client"
*/
public static final String CONFIG_PREFIX = "spring.ai.chat.client";
/**
* Enable or disable the ChatClient builder auto-configuration.
* When disabled, no ChatClient.Builder bean will be created.
* Default: true
*/
private boolean enabled = true;
/**
* Observability configuration for ChatClient operations.
* Controls logging of prompts and completions.
*/
private final Observations observations = new Observations();
/**
* Check if auto-configuration is enabled.
* @return true if enabled, false otherwise
*/
public boolean isEnabled();
/**
* Set whether auto-configuration is enabled.
* @param enabled true to enable, false to disable
*/
public void setEnabled(boolean enabled);
/**
* Get the observations configuration.
* @return the Observations configuration object (never null)
*/
public Observations getObservations();
/**
* Nested configuration for observability settings.
*/
public static class Observations {
/**
* Whether to log the prompt content in observations.
* WARNING: Enabling may expose sensitive or private information.
* When enabled, a warning message is logged at application startup.
* Default: false
* Since: 1.0.0
*/
private boolean logPrompt = false;
/**
* Whether to log the completion content in observations.
* WARNING: Enabling may expose sensitive or private information.
* When enabled, a warning message is logged at application startup.
* Default: false
* Since: 1.1.0
*/
private boolean logCompletion = false;
/**
* Check if prompt logging is enabled.
* @return true if prompt logging is enabled
*/
public boolean isLogPrompt();
/**
* Enable or disable prompt content logging.
* @param logPrompt true to enable, false to disable
*/
public void setLogPrompt(boolean logPrompt);
/**
* Check if completion logging is enabled.
* @return true if completion logging is enabled
*/
public boolean isLogCompletion();
/**
* Enable or disable completion content logging.
* @param logCompletion true to enable, false to disable
*/
public void setLogCompletion(boolean logCompletion);
}
}Property Reference:
| Property | Type | Default | Description |
|---|---|---|---|
spring.ai.chat.client.enabled | boolean | true | Enable/disable ChatClient builder auto-configuration |
spring.ai.chat.client.observations.log-prompt | boolean | false | Enable logging of prompt content in observations |
spring.ai.chat.client.observations.log-completion | boolean | false | Enable logging of completion content in observations |
Configuration Example:
spring:
ai:
chat:
client:
enabled: true
observations:
log-prompt: false
log-completion: falseConfiguration in Java:
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ChatClientPropertiesConfig {
@Bean
@ConfigurationProperties("spring.ai.chat.client")
public ChatClientBuilderProperties chatClientBuilderProperties() {
ChatClientBuilderProperties props = new ChatClientBuilderProperties();
props.setEnabled(true);
props.getObservations().setLogPrompt(false);
props.getObservations().setLogCompletion(false);
return props;
}
}Security Warning:
Enabling log-prompt or log-completion will log a warning message at startup:
log-prompt: "You have enabled logging out the ChatClient prompt content with the risk of exposing sensitive or private information. Please, be careful!"log-completion: "You have enabled logging out the ChatClient completion content with the risk of exposing sensitive or private information. Please, be careful!"These warnings are logged at WARN level using the logger for ChatClientAutoConfiguration class.
Impact of Enabling Observation Logging:
Additional configuration properties for controlling which AI models are auto-configured in the Spring AI ecosystem.
These properties are defined in the module's configuration metadata and control which model auto-configurations are enabled:
| Property | Type | Default | Description |
|---|---|---|---|
spring.ai.model.chat | String | "" | Specify the primary ChatModel to autoconfigure. If not set, each ChatModel auto-configuration is enabled by default |
spring.ai.model.embedding | String | "" | Specify the primary EmbeddingModel to autoconfigure |
spring.ai.model.embedding.text | String | "" | Specify the primary EmbeddingModel for text embeddings |
spring.ai.model.embedding.multimodal | String | "" | Specify the primary EmbeddingModel for multimodal embeddings |
spring.ai.model.image | String | "" | Specify the primary ImageModel to autoconfigure |
spring.ai.model.audio.transcription | String | "" | Specify the primary TranscriptionModel to autoconfigure |
spring.ai.model.audio.speech | String | "" | Specify the primary SpeechModel to autoconfigure |
spring.ai.model.moderation | String | "" | Specify the primary ModerationModel to autoconfigure |
Valid Model Provider Values:
openai - OpenAI models (GPT-4, GPT-3.5, etc.)anthropic - Anthropic models (Claude)ollama - Ollama local modelsazure - Azure OpenAI modelsvertexai - Google Vertex AI modelsbedrock - AWS Bedrock modelshuggingface - HuggingFace modelsstabilityai - Stability AI modelsmistralai - Mistral AI modelsConfiguration Example:
spring:
ai:
model:
chat: openai # Use OpenAI as primary chat model
embedding:
text: openai # Use OpenAI for text embeddings
multimodal: ollama # Use Ollama for multimodal embeddings
image: stabilityai # Use Stability AI for image generation
audio:
transcription: openai # Use OpenAI Whisper for transcription
speech: openai # Use OpenAI TTS for speech synthesis
moderation: openai # Use OpenAI for content moderationBehavior When Multiple Models Are Available:
Note: These properties help control which model providers are activated when multiple Spring AI model auto-configurations are available on the classpath. This is particularly useful in multi-model scenarios where you want to explicitly control which provider handles which type of operation.
Register global customizations that apply to all ChatClient.Builder instances through ChatClientCustomizer beans.
/**
* Functional interface for customizing ChatClient.Builder instances.
* Implementations should be registered as Spring beans to be automatically applied.
* Multiple customizers can be registered and will be applied in order.
*
* Package: org.springframework.ai.chat.client
*/
@FunctionalInterface
public interface ChatClientCustomizer {
/**
* Customize the ChatClient.Builder.
* This method is called for each newly created builder instance (prototype scope).
*
* @param builder the builder to customize (never null)
*/
void customize(ChatClient.Builder builder);
}Usage Example:
import org.springframework.ai.chat.client.ChatClientCustomizer;
import org.springframework.ai.chat.model.ChatOptions;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ChatClientConfig {
@Bean
public ChatClientCustomizer defaultSystemPromptCustomizer() {
return builder -> builder
.defaultSystem("You are a helpful AI assistant.");
}
@Bean
public ChatClientCustomizer defaultOptionsCustomizer() {
return builder -> builder
.defaultOptions(ChatOptions.builder()
.withTemperature(0.7)
.withMaxTokens(500)
.build());
}
}Customizer Application:
ChatClientCustomizer beans are collected from the Spring context at startup@Order annotation or implement Ordered interface to control orderingChatClient.Builder instance (due to prototype scope)Ordering Example:
import org.springframework.core.annotation.Order;
@Configuration
public class OrderedCustomizers {
@Bean
@Order(1) // Applied first
public ChatClientCustomizer firstCustomizer() {
return builder -> {
// First customization
};
}
@Bean
@Order(2) // Applied second
public ChatClientCustomizer secondCustomizer() {
return builder -> {
// Second customization
};
}
}Customization Scope:
Customizations set defaults that can be overridden per-request:
ChatClient chatClient = chatClientBuilder
.defaultSystem("Default system prompt") // From customizer
.build();
// Override on a per-request basis
String response = chatClient.prompt()
.system("Different system prompt for this request")
.user("Hello")
.call()
.content();Available Customization Methods on Builder:
/**
* ChatClient.Builder customization methods.
* These methods are commonly used in ChatClientCustomizer implementations.
*/
interface Builder {
/**
* Set the default system prompt for all requests.
* Can be overridden per-request.
* @param text the system prompt text
* @return this builder
*/
Builder defaultSystem(String text);
/**
* Set the default system prompt using a resource.
* @param text the resource containing the system prompt
* @return this builder
*/
Builder defaultSystem(Resource text);
/**
* Set default chat options (temperature, max tokens, etc.).
* Can be overridden per-request.
* @param chatOptions the chat options
* @return this builder
*/
Builder defaultOptions(ChatOptions chatOptions);
/**
* Set default function callbacks for function calling.
* @param functionCallbacks the function callbacks
* @return this builder
*/
Builder defaultFunctions(String... functionCallbacks);
/**
* Set default advisors for request/response processing.
* Advisors can modify requests before sending and responses after receiving.
* @param advisors the request/response advisors
* @return this builder
*/
Builder defaultAdvisors(RequestResponseAdvisor... advisors);
/**
* Set default advisors using a list.
* @param advisors the list of advisors
* @return this builder
*/
Builder defaultAdvisors(List<RequestResponseAdvisor> advisors);
/**
* Build the final ChatClient instance.
* @return a new ChatClient instance
*/
ChatClient build();
}Common Customization Patterns:
@Bean
public ChatClientCustomizer systemPromptCustomizer() {
return builder -> builder.defaultSystem(
"You are an AI assistant for Acme Corp. " +
"Always provide accurate, professional responses."
);
}@Bean
public ChatClientCustomizer modelOptionsCustomizer() {
return builder -> builder.defaultOptions(
ChatOptions.builder()
.withTemperature(0.7)
.withMaxTokens(1000)
.withTopP(0.9)
.withFrequencyPenalty(0.0)
.withPresencePenalty(0.0)
.build()
);
}@Bean
public ChatClientCustomizer memoryAdvisorCustomizer(ChatMemory chatMemory) {
return builder -> builder.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory)
);
}@Bean
public ChatClientCustomizer environmentSpecificCustomizer(Environment env) {
return builder -> {
if (env.matchesProfiles("dev")) {
builder.defaultOptions(
ChatOptions.builder()
.withTemperature(0.0) // Deterministic for dev
.build()
);
} else if (env.matchesProfiles("prod")) {
builder.defaultOptions(
ChatOptions.builder()
.withTemperature(0.7) // Creative for prod
.build()
);
}
};
}Error Handling in Customizers:
Customizers should handle exceptions gracefully. If a customizer throws an exception, the application context will fail to start:
@Bean
public ChatClientCustomizer safeCustomizer() {
return builder -> {
try {
// Customization logic that might fail
builder.defaultSystem(loadSystemPromptFromFile());
} catch (Exception e) {
// Handle error or use fallback
builder.defaultSystem("Default fallback prompt");
// Optionally log the error
}
};
}Configure observation handlers for monitoring and tracing ChatClient operations.
ObservationRegistry Integration:
The auto-configuration automatically integrates with Spring Boot's Micrometer observability infrastructure:
/**
* Creates a ChatClient.Builder bean with integrated observability.
* This is the main bean method in ChatClientAutoConfiguration.
*
* @param chatClientBuilderConfigurer Applies registered ChatClientCustomizer beans (never null)
* @param chatModel The ChatModel to use for chat operations (required, must exist in context)
* @param observationRegistry Optional ObservationRegistry wrapped in ObjectProvider (defaults to NOOP if not available)
* @param chatClientObservationConvention Optional custom convention for ChatClient observations wrapped in ObjectProvider
* @param advisorObservationConvention Optional custom convention for Advisor observations wrapped in ObjectProvider
* @return A configured ChatClient.Builder with prototype scope
*/
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
public ChatClient.Builder chatClientBuilder(
ChatClientBuilderConfigurer chatClientBuilderConfigurer,
ChatModel chatModel,
ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<ChatClientObservationConvention> chatClientObservationConvention,
ObjectProvider<AdvisorObservationConvention> advisorObservationConvention
) {
// Implementation creates builder with observability integration
// observationRegistry.getIfAvailable(ObservationRegistry::noop) is used
// Custom conventions are applied if available
ChatClient.Builder builder = ChatClient.builder(chatModel);
// Apply observation registry
ObservationRegistry registry = observationRegistry.getIfAvailable(ObservationRegistry::noop);
builder.observationRegistry(registry);
// Apply custom conventions if present
chatClientObservationConvention.ifAvailable(builder::observationConvention);
advisorObservationConvention.ifAvailable(builder::advisorObservationConvention);
// Apply customizers
chatClientBuilderConfigurer.configure(builder);
return builder;
}ObjectProvider Usage:
ObjectProvider<T> is a Spring interface that provides lazy, optional dependency injection:
.getIfAvailable() method to retrieve the bean or null.getIfAvailable(Supplier<T>) method to retrieve the bean or a default value.ifAvailable(Consumer<T>) method to execute an action if the bean is availableObservation Registry Behavior:
ObservationRegistry bean is available, it's automatically usedObservationRegistry.NOOP (no-op implementation that does nothing)ObservationAutoConfigurationCustom Observation Conventions:
Register custom conventions to control observation naming and tagging:
import org.springframework.ai.chat.client.observation.ChatClientObservationConvention;
import org.springframework.ai.chat.client.observation.ChatClientObservationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.micrometer.common.KeyValues;
@Configuration
public class ObservationConfig {
@Bean
public ChatClientObservationConvention customChatClientConvention() {
return new ChatClientObservationConvention() {
@Override
public String getName() {
return "my.custom.chat.client";
}
@Override
public KeyValues getLowCardinalityKeyValues(ChatClientObservationContext context) {
return KeyValues.of(
"model.provider", context.getRequest().getModel().getProvider(),
"application.name", "my-app",
"environment", System.getenv("ENVIRONMENT")
);
}
@Override
public KeyValues getHighCardinalityKeyValues(ChatClientObservationContext context) {
return KeyValues.of(
"model.name", context.getRequest().getModel().getName(),
"user.id", context.getRequest().getUserId()
);
}
};
}
}Available Conventions:
/**
* Observation convention for ChatClient operations.
* Controls observation naming and key-value tags.
* Package: org.springframework.ai.chat.client.observation
*/
public interface ChatClientObservationConvention extends ObservationConvention<ChatClientObservationContext> {
/**
* Get the name of the observation.
* Default implementation returns "spring.ai.chat.client".
* @return the observation name
*/
String getName();
/**
* Get low-cardinality key-values for the observation.
* Low-cardinality values have a limited set of possible values (e.g., model provider, operation type).
* Used for metrics that can be aggregated.
* @param context the observation context
* @return key-values with low cardinality
*/
KeyValues getLowCardinalityKeyValues(ChatClientObservationContext context);
/**
* Get high-cardinality key-values for the observation.
* High-cardinality values have many possible values (e.g., user ID, request ID).
* Used for tracing but typically excluded from metrics.
* @param context the observation context
* @return key-values with high cardinality
*/
KeyValues getHighCardinalityKeyValues(ChatClientObservationContext context);
/**
* Check if this convention supports the given context.
* @param context the context to check
* @return true if supported
*/
default boolean supportsContext(Observation.Context context) {
return context instanceof ChatClientObservationContext;
}
}
/**
* Observation convention for Advisor operations.
* Advisors can intercept and modify ChatClient requests and responses.
* Package: org.springframework.ai.chat.client.advisor.observation
*/
public interface AdvisorObservationConvention extends ObservationConvention<AdvisorObservationContext> {
/**
* Get the name of the observation.
* Default implementation returns "spring.ai.advisor".
* @return the observation name
*/
String getName();
/**
* Get low-cardinality key-values for the observation.
* @param context the observation context
* @return key-values with low cardinality
*/
KeyValues getLowCardinalityKeyValues(AdvisorObservationContext context);
/**
* Get high-cardinality key-values for the observation.
* @param context the observation context
* @return key-values with high cardinality
*/
KeyValues getHighCardinalityKeyValues(AdvisorObservationContext context);
/**
* Check if this convention supports the given context.
* @param context the context to check
* @return true if supported
*/
default boolean supportsContext(Observation.Context context) {
return context instanceof AdvisorObservationContext;
}
}Observation Context Information:
/**
* Context object for ChatClient observations.
* Contains information about the chat request and response.
* Package: org.springframework.ai.chat.client.observation
*/
public class ChatClientObservationContext extends Observation.Context {
/**
* Get the chat request.
* @return the request object (never null during observation)
*/
public ChatRequest getRequest();
/**
* Get the chat response.
* @return the response object (may be null during onStart)
*/
public ChatResponse getResponse();
/**
* Get the prompt text.
* @return the prompt text
*/
public String getPrompt();
/**
* Get the completion text.
* @return the completion text (may be null if not yet available)
*/
public String getCompletion();
}
/**
* Context object for Advisor observations.
* Contains information about advisor execution.
* Package: org.springframework.ai.chat.client.advisor.observation
*/
public class AdvisorObservationContext extends Observation.Context {
/**
* Get the advisor name.
* @return the advisor name
*/
public String getAdvisorName();
/**
* Get the advisor type (REQUEST or RESPONSE).
* @return the advisor type
*/
public AdvisorType getAdvisorType();
}Default Observation Names and Tags:
When using default conventions, the following observations are created:
spring.ai.chat.clientspring.ai.kind = "chat_client"spring.ai.operation.name = "chat_client"spring.ai.provider = provider name (e.g., "openai", "anthropic")spring.ai.chat.client.model.name = model name (e.g., "gpt-4", "claude-3")spring.ai.chat.client.request.id = unique request IDConfigure specialized observation handlers for logging prompt and completion content.
With Micrometer Tracing (io.micrometer:micrometer-tracing):
When the Tracer class is available and a Tracer bean is configured, tracing-aware handlers are created:
/**
* Creates tracing-aware observation handler for prompt content logging.
* This handler logs prompt content with trace context integration.
*
* Only created when:
* - io.micrometer.tracing.Tracer class is on classpath
* - Tracer bean exists in application context
* - spring.ai.chat.client.observations.log-prompt=true
* - No custom handler bean with name "chatClientPromptContentObservationHandler" exists
*
* @param tracer The Micrometer Tracer instance (never null)
* @return TracingAwareLoggingObservationHandler wrapping ChatClientPromptContentObservationHandler
*/
@Bean
@ConditionalOnMissingBean(
value = ChatClientPromptContentObservationHandler.class,
name = "chatClientPromptContentObservationHandler"
)
@ConditionalOnProperty(
prefix = "spring.ai.chat.client.observations",
name = "log-prompt",
havingValue = "true"
)
@ConditionalOnClass(name = "io.micrometer.tracing.Tracer")
@ConditionalOnBean(Tracer.class)
public TracingAwareLoggingObservationHandler<ChatClientObservationContext>
chatClientPromptContentObservationHandler(Tracer tracer) {
return new TracingAwareLoggingObservationHandler<>(
new ChatClientPromptContentObservationHandler(),
tracer
);
}
/**
* Creates tracing-aware observation handler for completion content logging.
* This handler logs completion content with trace context integration.
*
* Only created when:
* - io.micrometer.tracing.Tracer class is on classpath
* - Tracer bean exists in application context
* - spring.ai.chat.client.observations.log-completion=true
* - No custom handler bean with name "chatClientCompletionObservationHandler" exists
*
* @param tracer The Micrometer Tracer instance (never null)
* @return TracingAwareLoggingObservationHandler wrapping ChatClientCompletionObservationHandler
*/
@Bean
@ConditionalOnMissingBean(
value = ChatClientCompletionObservationHandler.class,
name = "chatClientCompletionObservationHandler"
)
@ConditionalOnProperty(
prefix = "spring.ai.chat.client.observations",
name = "log-completion",
havingValue = "true"
)
@ConditionalOnClass(name = "io.micrometer.tracing.Tracer")
@ConditionalOnBean(Tracer.class)
public TracingAwareLoggingObservationHandler<ChatClientObservationContext>
chatClientCompletionObservationHandler(Tracer tracer) {
return new TracingAwareLoggingObservationHandler<>(
new ChatClientCompletionObservationHandler(),
tracer
);
}Without Micrometer Tracing:
When the Tracer class is not available, basic observation handlers are created:
/**
* Creates basic observation handler for prompt content logging.
* This handler logs prompt content without trace context integration.
*
* Only created when:
* - io.micrometer.tracing.Tracer class is NOT on classpath
* - spring.ai.chat.client.observations.log-prompt=true
* - No custom handler bean exists
*
* @return ChatClientPromptContentObservationHandler
*/
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(
prefix = "spring.ai.chat.client.observations",
name = "log-prompt",
havingValue = "true"
)
@ConditionalOnMissingClass("io.micrometer.tracing.Tracer")
public ChatClientPromptContentObservationHandler chatClientPromptContentObservationHandler() {
return new ChatClientPromptContentObservationHandler();
}
/**
* Creates basic observation handler for completion content logging.
* This handler logs completion content without trace context integration.
*
* Only created when:
* - io.micrometer.tracing.Tracer class is NOT on classpath
* - spring.ai.chat.client.observations.log-completion=true
* - No custom handler bean exists
*
* @return ChatClientCompletionObservationHandler
*/
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(
prefix = "spring.ai.chat.client.observations",
name = "log-completion",
havingValue = "true"
)
@ConditionalOnMissingClass("io.micrometer.tracing.Tracer")
public ChatClientCompletionObservationHandler chatClientCompletionObservationHandler() {
return new ChatClientCompletionObservationHandler();
}Configuration Example:
spring:
ai:
chat:
client:
observations:
log-prompt: true # Enable prompt logging
log-completion: true # Enable completion loggingCustom Handler Override:
Register custom observation handlers to replace the default implementations:
import org.springframework.ai.chat.client.observation.ChatClientPromptContentObservationHandler;
import org.springframework.ai.chat.client.observation.ChatClientObservationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.micrometer.observation.Observation;
@Configuration
public class CustomObservationConfig {
@Bean
public ChatClientPromptContentObservationHandler chatClientPromptContentObservationHandler() {
return new ChatClientPromptContentObservationHandler() {
@Override
public void onStart(ChatClientObservationContext context) {
// Custom logging logic with sanitization
String prompt = context.getPrompt();
String sanitized = sanitize(prompt);
System.out.println("Sanitized Prompt: " + sanitized);
}
@Override
public void onStop(ChatClientObservationContext context) {
// Log additional metadata
System.out.println("Request completed for model: " +
context.getRequest().getModel().getName());
}
private String sanitize(String content) {
// Custom sanitization logic to remove sensitive data
return content
.replaceAll("password=\\w+", "password=***")
.replaceAll("api[_-]?key=\\w+", "api_key=***")
.replaceAll("\\b\\d{4}-\\d{4}-\\d{4}-\\d{4}\\b", "****-****-****-****");
}
};
}
}Handler Types:
/**
* Observation handler for logging ChatClient prompt content.
* Logs the prompt text when the observation stops.
* Package: org.springframework.ai.chat.client.observation
*/
public class ChatClientPromptContentObservationHandler
implements ObservationHandler<ChatClientObservationContext> {
/**
* Called when the observation stops (after the request is sent).
* Logs the prompt content at DEBUG level.
* @param context the observation context containing prompt information
*/
@Override
public void onStop(ChatClientObservationContext context) {
// Logs prompt content to logger at DEBUG level
// Logger name: "org.springframework.ai.chat.client.observation.ChatClientPromptContentObservationHandler"
// Message format: "Prompt: {prompt content}"
}
/**
* Check if this handler supports the given context.
* @param context the context to check
* @return true if context is ChatClientObservationContext
*/
@Override
public boolean supportsContext(Observation.Context context) {
return context instanceof ChatClientObservationContext;
}
}
/**
* Observation handler for logging ChatClient completion content.
* Logs the completion text when the observation stops.
* Package: org.springframework.ai.chat.client.observation
*/
public class ChatClientCompletionObservationHandler
implements ObservationHandler<ChatClientObservationContext> {
/**
* Called when the observation stops (after the response is received).
* Logs the completion content at DEBUG level.
* @param context the observation context containing completion information
*/
@Override
public void onStop(ChatClientObservationContext context) {
// Logs completion content to logger at DEBUG level
// Logger name: "org.springframework.ai.chat.client.observation.ChatClientCompletionObservationHandler"
// Message format: "Completion: {completion content}"
}
/**
* Check if this handler supports the given context.
* @param context the context to check
* @return true if context is ChatClientObservationContext
*/
@Override
public boolean supportsContext(Observation.Context context) {
return context instanceof ChatClientObservationContext;
}
}
/**
* Wraps observation handlers with tracing awareness.
* Ensures trace context is properly propagated to logging.
* When a trace is active, the trace ID and span ID are included in logs.
* Package: org.springframework.ai.chat.client.observation
*/
public class TracingAwareLoggingObservationHandler<T extends Observation.Context>
implements ObservationHandler<T> {
/**
* Create a tracing-aware handler.
* @param delegate the actual observation handler to wrap
* @param tracer the Micrometer tracer for trace context
*/
public TracingAwareLoggingObservationHandler(
ObservationHandler<T> delegate,
Tracer tracer
) {
// Stores delegate and tracer for use during observation callbacks
}
/**
* Called when observation starts.
* Delegates to wrapped handler within trace context.
*/
@Override
public void onStart(T context) {
// Sets up trace context before delegating
}
/**
* Called when observation stops.
* Delegates to wrapped handler within trace context.
*/
@Override
public void onStop(T context) {
// Ensures trace context is active before delegating
// Trace ID and Span ID are available in MDC for logging
}
/**
* Called when observation has an error.
* Delegates to wrapped handler within trace context.
*/
@Override
public void onError(T context) {
// Handles errors within trace context
}
/**
* Check if this handler supports the given context.
* Delegates to wrapped handler.
*/
@Override
public boolean supportsContext(Observation.Context context) {
return delegate.supportsContext(context);
}
}Logging Configuration for Observation Handlers:
To see the logged prompt and completion content, configure logging levels:
logging:
level:
org.springframework.ai.chat.client.observation: DEBUGOr in logback-spring.xml:
<logger name="org.springframework.ai.chat.client.observation" level="DEBUG"/>When Handlers Are Created:
| Scenario | Tracer Available | log-prompt | log-completion | Handlers Created |
|---|---|---|---|---|
| 1 | Yes | true | true | TracingAware handlers for both |
| 2 | Yes | true | false | TracingAware handler for prompt only |
| 3 | Yes | false | true | TracingAware handler for completion only |
| 4 | Yes | false | false | No handlers |
| 5 | No | true | true | Basic handlers for both |
| 6 | No | true | false | Basic handler for prompt only |
| 7 | No | false | true | Basic handler for completion only |
| 8 | No | false | false | No handlers |
Internal configuration classes that conditionally create observation handlers based on classpath dependencies.
/**
* Configuration for observation handlers when Micrometer Tracer is present.
* This nested static class is defined within ChatClientAutoConfiguration.
* Provides tracing-aware observation handlers that integrate with distributed tracing.
*
* Package: org.springframework.ai.model.chat.client.autoconfigure
*
* Activation Conditions:
* - io.micrometer.tracing.Tracer class must be on classpath
* - Tracer bean must exist in the application context
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Tracer.class)
@ConditionalOnBean(Tracer.class)
public static class TracerPresentObservationConfiguration {
/**
* Bean methods for tracing-aware observation handlers.
* See "Observation Handler Configuration" section for details.
*
* Beans created:
* - chatClientPromptContentObservationHandler (conditional)
* - chatClientCompletionObservationHandler (conditional)
*/
}
/**
* Configuration for observation handlers when Micrometer Tracer is NOT present.
* This nested static class is defined within ChatClientAutoConfiguration.
* Provides basic observation handlers without tracing integration.
*
* Package: org.springframework.ai.model.chat.client.autoconfigure
*
* Activation Conditions:
* - io.micrometer.tracing.Tracer class must NOT be on classpath
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingClass("io.micrometer.tracing.Tracer")
public static class TracerNotPresentObservationConfiguration {
/**
* Bean methods for basic observation handlers.
* See "Observation Handler Configuration" section for details.
*
* Beans created:
* - chatClientPromptContentObservationHandler (conditional)
* - chatClientCompletionObservationHandler (conditional)
*/
}Purpose:
These nested configuration classes provide conditional bean creation for observation handlers:
TracingAwareLoggingObservationHandler instances that integrate with distributed tracing systems (Zipkin, Jaeger, etc.)ChatClientPromptContentObservationHandler and ChatClientCompletionObservationHandler instances that log without trace contextWhy Nested Configurations:
Spring Boot uses these nested static configuration classes to:
Configuration Evaluation Order:
ChatClientAutoConfiguration is evaluated first (outer class)TracerPresentObservationConfiguration is evaluated if Tracer is presentTracerNotPresentObservationConfiguration is evaluated if Tracer is absentThe split ensures the module works correctly whether or not Micrometer Tracing is on the classpath, without requiring the tracing library as a mandatory dependency.
Internal configuration utility for applying customizers to ChatClient.Builder instances.
/**
* Configures ChatClient.Builder instances by applying registered ChatClientCustomizer beans.
* This is primarily an internal component used by the auto-configuration,
* but can be used directly for advanced customization scenarios.
*
* Package: org.springframework.ai.model.chat.client.autoconfigure
*/
public class ChatClientBuilderConfigurer {
private final List<ChatClientCustomizer> customizers;
/**
* Create a ChatClientBuilderConfigurer with the given customizers.
* @param customizers the list of customizers to apply (never null, may be empty)
*/
public ChatClientBuilderConfigurer(List<ChatClientCustomizer> customizers) {
this.customizers = customizers;
}
/**
* Configure the specified ChatClient.Builder by applying all registered customizers.
* The builder can be further tuned and default settings can be overridden after this call.
* Customizers are applied in the order they appear in the list.
*
* @param builder The ChatClient.Builder instance to configure (never null)
* @return The configured builder (same instance, modified in-place)
*/
public ChatClient.Builder configure(ChatClient.Builder builder) {
// Applies each customizer in order
for (ChatClientCustomizer customizer : customizers) {
customizer.customize(builder);
}
return builder;
}
}Bean Creation:
/**
* Creates ChatClientBuilderConfigurer bean with all registered customizers.
* This bean is created automatically by the auto-configuration.
*
* @param customizerProvider Provider for all ChatClientCustomizer beans wrapped in ObjectProvider
* @return Configured ChatClientBuilderConfigurer (never null)
*/
@Bean
@ConditionalOnMissingBean
public ChatClientBuilderConfigurer chatClientBuilderConfigurer(
ObjectProvider<ChatClientCustomizer> customizerProvider
) {
// Collects all ChatClientCustomizer beans into a list
// If no customizers exist, creates configurer with empty list
// Respects bean ordering (@Order annotation, Ordered interface)
List<ChatClientCustomizer> customizers = customizerProvider.orderedStream()
.collect(Collectors.toList());
return new ChatClientBuilderConfigurer(customizers);
}Note: The ObjectProvider<ChatClientCustomizer> allows the configurer to collect all registered ChatClientCustomizer beans from the application context, even if none are present. The orderedStream() method ensures customizers are applied in the correct order based on @Order annotations or Ordered interface implementations.
Usage (Advanced):
Most users don't need to interact with this class directly, as it's used internally by the auto-configuration. However, for advanced scenarios where you need to programmatically create ChatClient instances with customizations:
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.model.chat.client.autoconfigure.ChatClientBuilderConfigurer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class AdvancedChatService {
@Autowired
private ChatClientBuilderConfigurer configurer;
@Autowired
private ChatModel chatModel;
public ChatClient createCustomClient() {
// Create a new builder from scratch
ChatClient.Builder builder = ChatClient.builder(chatModel);
// Apply all registered customizers
configurer.configure(builder);
// Additional custom configuration on top of customizers
builder.defaultSystem("Special configuration for this specific client");
builder.defaultOptions(ChatOptions.builder()
.withTemperature(0.9)
.build());
return builder.build();
}
public ChatClient createMinimalClient() {
// Create a client without customizers
return ChatClient.builder(chatModel).build();
}
}When to Use ChatClientBuilderConfigurer Directly:
Customizer Application Guarantees:
/**
* Auto-configuration for ChatClient.
* Produces ChatClient.Builder beans with prototype scope and configures observability.
*
* Package: org.springframework.ai.model.chat.client.autoconfigure
* Fully Qualified Name: org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration
*
* Activation Conditions:
* - ChatClient.class must be on classpath
* - spring.ai.chat.client.enabled=true (default: true, matchIfMissing: true)
* - Runs after ObservationAutoConfiguration to ensure observability infrastructure is ready
*
* Beans Created:
* - ChatClient.Builder (prototype scope)
* - ChatClientBuilderConfigurer
* - ChatClientPromptContentObservationHandler (conditional)
* - ChatClientCompletionObservationHandler (conditional)
*
* Nested Configuration Classes:
* - TracerPresentObservationConfiguration
* - TracerNotPresentObservationConfiguration
*/
@AutoConfiguration(
afterName = "org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration"
)
@ConditionalOnClass(ChatClient.class)
@EnableConfigurationProperties(ChatClientBuilderProperties.class)
@ConditionalOnProperty(
prefix = "spring.ai.chat.client",
name = "enabled",
havingValue = "true",
matchIfMissing = true
)
public class ChatClientAutoConfiguration {
/**
* Bean methods documented in Capabilities sections above:
* - chatClientBuilder(...)
* - chatClientBuilderConfigurer(...)
*
* Nested classes:
* - TracerPresentObservationConfiguration
* - TracerNotPresentObservationConfiguration
*
* See respective sections for detailed documentation.
*/
/**
* Logger for this auto-configuration.
* Used to log warnings when observation logging is enabled.
*/
private static final Logger logger = LoggerFactory.getLogger(ChatClientAutoConfiguration.class);
}Auto-Configuration Loading:
This auto-configuration is registered in:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importsWith the entry:
org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfigurationConditional Evaluation Details:
The auto-configuration is only loaded when ALL of the following are true:
@ConditionalOnClass(ChatClient.class): The ChatClient class must be on the classpath. This is satisfied when spring-ai-client-chat dependency is present.
@ConditionalOnProperty: The property spring.ai.chat.client.enabled must be true (or missing, as matchIfMissing = true).
@AutoConfiguration(afterName = "..."): This ensures the auto-configuration runs AFTER ObservationAutoConfiguration, guaranteeing that ObservationRegistry beans are available if Actuator is present.
What Happens if Conditions Are Not Met:
ChatClient.class is not found: Auto-configuration is skipped entirely (no error)spring.ai.chat.client.enabled=false: Auto-configuration is skipped (no beans created)ChatModel bean exists: Application context fails to start with error "No qualifying bean of type ChatModel available"Debugging Auto-Configuration:
To see if this auto-configuration is applied, enable debug logging:
logging:
level:
org.springframework.boot.autoconfigure: DEBUGLook for lines like:
ChatClientAutoConfiguration matched:
- @ConditionalOnClass found required class 'org.springframework.ai.chat.client.ChatClient'
- @ConditionalOnProperty (spring.ai.chat.client.enabled=true) matchedOr use Spring Boot's conditions report:
java -jar myapp.jar --debug/**
* Configuration properties for the chat client builder.
* Bound to properties under "spring.ai.chat.client" prefix.
* Automatically enabled via @EnableConfigurationProperties in ChatClientAutoConfiguration.
*
* Package: org.springframework.ai.model.chat.client.autoconfigure
* Fully Qualified Name: org.springframework.ai.model.chat.client.autoconfigure.ChatClientBuilderProperties
*/
@ConfigurationProperties("spring.ai.chat.client")
public class ChatClientBuilderProperties {
/**
* Configuration prefix constant.
* Value: "spring.ai.chat.client"
*/
public static final String CONFIG_PREFIX = "spring.ai.chat.client";
/**
* Enable chat client builder auto-configuration.
* When false, no ChatClient.Builder bean will be created.
* Default: true
*/
private boolean enabled = true;
/**
* Observability configuration.
* Controls logging of prompts and completions.
* Never null - initialized in constructor.
*/
private final Observations observations = new Observations();
/**
* Check if auto-configuration is enabled.
* @return true if enabled, false otherwise
*/
public boolean isEnabled() {
return this.enabled;
}
/**
* Set whether auto-configuration is enabled.
* @param enabled true to enable, false to disable
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
/**
* Get the observations configuration.
* @return the Observations configuration object (never null)
*/
public Observations getObservations() {
return this.observations;
}
/**
* Nested configuration for observability settings.
* Controls whether to log prompt and completion content.
*/
public static class Observations {
/**
* Whether to log the prompt content in observations.
* WARNING: May expose sensitive information.
* When enabled, triggers a WARN level log message at startup.
* Default: false
* Since: 1.0.0
*/
private boolean logPrompt = false;
/**
* Whether to log the completion content in observations.
* WARNING: May expose sensitive information.
* When enabled, triggers a WARN level log message at startup.
* Default: false
* Since: 1.1.0
*/
private boolean logCompletion = false;
/**
* Check if prompt logging is enabled.
* @return true if prompt logging is enabled
*/
public boolean isLogPrompt() {
return this.logPrompt;
}
/**
* Enable or disable prompt content logging.
* @param logPrompt true to enable, false to disable
*/
public void setLogPrompt(boolean logPrompt) {
this.logPrompt = logPrompt;
}
/**
* Check if completion logging is enabled.
* @return true if completion logging is enabled
*/
public boolean isLogCompletion() {
return this.logCompletion;
}
/**
* Enable or disable completion content logging.
* @param logCompletion true to enable, false to disable
*/
public void setLogCompletion(boolean logCompletion) {
this.logCompletion = logCompletion;
}
}
}Property Binding:
Properties are bound using Spring Boot's relaxed binding rules. All of these are equivalent:
# Recommended style
spring.ai.chat.client.enabled: true
# Also works
spring.ai.chat.client.enabled: true
# Environment variable
SPRING_AI_CHAT_CLIENT_ENABLED=true
# System property
-Dspring.ai.chat.client.enabled=trueDefault Values:
If no properties are specified, these defaults are used:
spring.ai.chat.client.enabled = truespring.ai.chat.client.observations.log-prompt = falsespring.ai.chat.client.observations.log-completion = falseProperty Validation:
Spring Boot validates these properties at startup. Invalid values will cause the application to fail to start:
spring:
ai:
chat:
client:
enabled: "not-a-boolean" # ERROR: Cannot convert String to booleanThese types are referenced by the auto-configuration but provided by external Spring AI dependencies:
/**
* Main ChatClient interface from spring-ai-client-chat module.
* Provides fluent API for chat operations with AI models.
* Package: org.springframework.ai.chat.client
*/
public interface ChatClient {
/**
* Start building a chat prompt.
* Returns a fluent API for constructing the prompt and calling the model.
* @return a PromptSpec for building the chat request
*/
PromptSpec prompt();
/**
* Builder for creating ChatClient instances.
* Provided as a prototype-scoped bean by this auto-configuration.
* Package: org.springframework.ai.chat.client.ChatClient
*/
interface Builder {
/**
* Set the default system prompt for all requests.
* Can be overridden per-request.
* @param text the system prompt text
* @return this builder for method chaining
*/
Builder defaultSystem(String text);
/**
* Set the default system prompt from a Spring Resource.
* Useful for loading prompts from files.
* @param text the resource containing the system prompt
* @return this builder for method chaining
*/
Builder defaultSystem(Resource text);
/**
* Set default chat options (temperature, max tokens, etc.).
* Can be overridden per-request.
* @param chatOptions the chat options
* @return this builder for method chaining
*/
Builder defaultOptions(ChatOptions chatOptions);
/**
* Set default function callbacks for function calling.
* Functions can be invoked by the AI model during the conversation.
* @param functionCallbacks the function callback bean names
* @return this builder for method chaining
*/
Builder defaultFunctions(String... functionCallbacks);
/**
* Set default advisors for request/response processing.
* Advisors can modify requests before sending and responses after receiving.
* @param advisors the request/response advisors
* @return this builder for method chaining
*/
Builder defaultAdvisors(RequestResponseAdvisor... advisors);
/**
* Set default advisors using a list.
* @param advisors the list of advisors
* @return this builder for method chaining
*/
Builder defaultAdvisors(List<RequestResponseAdvisor> advisors);
/**
* Set the observation registry for metrics and tracing.
* @param observationRegistry the observation registry
* @return this builder for method chaining
*/
Builder observationRegistry(ObservationRegistry observationRegistry);
/**
* Set a custom observation convention for ChatClient operations.
* @param observationConvention the custom convention
* @return this builder for method chaining
*/
Builder observationConvention(ChatClientObservationConvention observationConvention);
/**
* Set a custom observation convention for Advisor operations.
* @param observationConvention the custom convention
* @return this builder for method chaining
*/
Builder advisorObservationConvention(AdvisorObservationConvention observationConvention);
/**
* Build the final ChatClient instance.
* Creates an immutable client with all configured defaults.
* @return a new ChatClient instance (never null)
*/
ChatClient build();
/**
* Clone this builder.
* Creates a new builder with the same configuration.
* Used internally for prototype scope implementation.
* @return a new Builder instance with copied configuration
*/
Builder clone();
}
/**
* Create a new ChatClient builder.
* @param chatModel the underlying chat model to use (required, never null)
* @return a new Builder instance
*/
static Builder builder(ChatModel chatModel) {
// Creates new builder instance
}
}
/**
* Base model interface for chat operations.
* Must be provided by other Spring AI auto-configurations.
* Implementations: OpenAI, Anthropic, Ollama, Azure, Vertex AI, Bedrock, etc.
* Package: org.springframework.ai.chat.model
*/
public interface ChatModel extends Model<Prompt, ChatResponse> {
/**
* Synchronous call to the chat model.
* Blocks until the response is received.
* @param prompt the prompt containing messages and options
* @return the chat response with generated content
*/
ChatResponse call(Prompt prompt);
/**
* Streaming call to the chat model.
* Returns a reactive stream of response chunks.
* @param prompt the prompt containing messages and options
* @return a Flux of ChatResponse objects (streaming)
*/
Flux<ChatResponse> stream(Prompt prompt);
/**
* Get the default options for this chat model.
* @return the default ChatOptions (may be null)
*/
default ChatOptions getDefaultOptions() {
return null;
}
}
/**
* Functional interface for customizing ChatClient.Builder.
* Implement and register as bean to apply global customizations.
* Package: org.springframework.ai.chat.client
*/
@FunctionalInterface
public interface ChatClientCustomizer {
/**
* Customize the ChatClient.Builder.
* @param builder the builder to customize (never null)
*/
void customize(ChatClient.Builder builder);
}
/**
* Fluent API for building chat prompts.
* Returned by ChatClient.prompt().
* Package: org.springframework.ai.chat.client
*/
public interface PromptSpec {
/**
* Set the system prompt for this request.
* Overrides any default system prompt.
* @param text the system prompt text
* @return this PromptSpec for method chaining
*/
PromptSpec system(String text);
/**
* Set the user message for this request.
* @param text the user message text
* @return this PromptSpec for method chaining
*/
PromptSpec user(String text);
/**
* Set chat options for this request.
* Overrides any default options.
* @param chatOptions the chat options
* @return this PromptSpec for method chaining
*/
PromptSpec options(ChatOptions chatOptions);
/**
* Add advisors for this request.
* Added to any default advisors.
* @param advisors the advisors to add
* @return this PromptSpec for method chaining
*/
PromptSpec advisors(RequestResponseAdvisor... advisors);
/**
* Execute the chat request synchronously.
* @return a CallResponseSpec for accessing the response
*/
CallResponseSpec call();
/**
* Execute the chat request as a stream.
* @return a StreamResponseSpec for accessing the streaming response
*/
StreamResponseSpec stream();
}
/**
* Options for configuring chat model behavior.
* Package: org.springframework.ai.chat.model
*/
public interface ChatOptions {
/**
* Get the temperature setting (0.0 to 2.0).
* Controls randomness in the output.
* @return the temperature value
*/
Double getTemperature();
/**
* Get the maximum number of tokens to generate.
* @return the max tokens value
*/
Integer getMaxTokens();
/**
* Get the top-p sampling value (0.0 to 1.0).
* Controls diversity via nucleus sampling.
* @return the top-p value
*/
Double getTopP();
/**
* Get the frequency penalty (-2.0 to 2.0).
* Penalizes frequent tokens.
* @return the frequency penalty value
*/
Double getFrequencyPenalty();
/**
* Get the presence penalty (-2.0 to 2.0).
* Penalizes tokens that have appeared.
* @return the presence penalty value
*/
Double getPresencePenalty();
/**
* Create a new builder for ChatOptions.
* @return a new ChatOptions.Builder
*/
static Builder builder() {
// Creates new builder
}
/**
* Builder for creating ChatOptions instances.
*/
interface Builder {
Builder withTemperature(Double temperature);
Builder withMaxTokens(Integer maxTokens);
Builder withTopP(Double topP);
Builder withFrequencyPenalty(Double frequencyPenalty);
Builder withPresencePenalty(Double presencePenalty);
ChatOptions build();
}
}Dependency Hierarchy:
spring-ai-autoconfigure-model-chat-client (this module)
├── spring-ai-client-chat (provides ChatClient interface)
│ ├── spring-ai-core (core abstractions)
│ └── io.micrometer:micrometer-observation (optional)
└── spring-boot-autoconfigure (Spring Boot infrastructure)
Required runtime dependencies (one of):
├── spring-ai-openai (OpenAI models)
├── spring-ai-anthropic (Claude models)
├── spring-ai-ollama (Local models)
├── spring-ai-azure-openai (Azure OpenAI)
└── ... (other model providers)Import Order for Usage:
// 1. Core ChatClient API
import org.springframework.ai.chat.client.ChatClient;
// 2. Model interface (usually not imported directly, provided by auto-config)
import org.springframework.ai.chat.model.ChatModel;
// 3. Options and configuration
import org.springframework.ai.chat.model.ChatOptions;
// 4. Customization
import org.springframework.ai.chat.client.ChatClientCustomizer;
// 5. Spring Framework
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class ChatService {
private final ChatClient.Builder chatClientBuilder;
public ChatService(ChatClient.Builder chatClientBuilder) {
this.chatClientBuilder = chatClientBuilder;
}
public String chat(String userMessage) {
ChatClient client = chatClientBuilder.build();
return client.prompt()
.user(userMessage)
.call()
.content();
}
}Key Points:
ChatClient.Builder, not ChatClient directly.build() to create a ChatClient instancechatClientBuilder.build() creates a new ChatClientEach injection point receives a separate builder instance due to prototype scope:
@Service
public class MultiClientService {
private final ChatClient formalClient;
private final ChatClient casualClient;
public MultiClientService(ChatClient.Builder builder1, ChatClient.Builder builder2) {
// builder1 and builder2 are DIFFERENT instances (prototype scope)
this.formalClient = builder1
.defaultSystem("You are a formal business assistant.")
.build();
this.casualClient = builder2
.defaultSystem("You are a friendly, casual assistant.")
.build();
}
public String getFormalResponse(String question) {
return formalClient.prompt().user(question).call().content();
}
public String getCasualResponse(String question) {
return casualClient.prompt().user(question).call().content();
}
}Why This Works:
builder1 and builder2 are separate instances (prototype scope)builder1 don't affect builder2Alternative Pattern - Single Builder:
@Service
public class MultiClientService {
private final ChatClient formalClient;
private final ChatClient casualClient;
public MultiClientService(ChatClient.Builder builder) {
// Use the same builder but configure differently
this.formalClient = builder
.defaultSystem("You are a formal business assistant.")
.build();
this.casualClient = builder
.defaultSystem("You are a friendly, casual assistant.")
.build();
}
}Warning: In this alternative, both clients share the same builder instance. Configurations are applied sequentially, so casualClient will have both system prompts applied (with the second overriding the first in its own context).
import org.springframework.ai.chat.client.ChatClientCustomizer;
import org.springframework.ai.chat.model.ChatOptions;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
@Configuration
public class ChatCustomizations {
@Bean
@Order(1) // Applied first
public ChatClientCustomizer systemPromptCustomizer() {
return builder -> builder.defaultSystem("You are a helpful assistant.");
}
@Bean
@Order(2) // Applied second
public ChatClientCustomizer optionsCustomizer() {
return builder -> builder.defaultOptions(
ChatOptions.builder()
.withTemperature(0.7)
.withMaxTokens(1000)
.build()
);
}
@Bean
@Order(3) // Applied third
public ChatClientCustomizer advisorsCustomizer() {
return builder -> builder.defaultAdvisors(
new MessageChatMemoryAdvisor(new InMemoryChatMemory())
);
}
}Ordering Guidelines:
@Order values are applied first (default is Integer.MAX_VALUE)Without @Order:
If no @Order is specified, customizers are applied in an undefined order (based on bean discovery order, which is not guaranteed).
import org.springframework.ai.chat.client.observation.ChatClientObservationConvention;
import org.springframework.ai.chat.client.observation.ChatClientObservationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import io.micrometer.common.KeyValues;
import io.micrometer.common.KeyValue;
@Configuration
public class ObservabilityConfig {
@Bean
public ChatClientObservationConvention customConvention(Environment env) {
return new ChatClientObservationConvention() {
@Override
public String getName() {
return "ai.chat.client.call";
}
@Override
public KeyValues getLowCardinalityKeyValues(ChatClientObservationContext context) {
// Low-cardinality: limited set of values, good for metrics
return KeyValues.of(
"model.provider", getProvider(context),
"application.name", "my-app",
"environment", env.getProperty("spring.profiles.active", "default"),
"operation", "chat"
);
}
@Override
public KeyValues getHighCardinalityKeyValues(ChatClientObservationContext context) {
// High-cardinality: many possible values, good for traces
return KeyValues.of(
"model.name", getModelName(context),
"request.id", context.getRequest().getRequestId(),
"user.id", getUserId(context)
);
}
private String getProvider(ChatClientObservationContext context) {
// Extract provider from context
return context.getRequest().getModel().getProvider();
}
private String getModelName(ChatClientObservationContext context) {
return context.getRequest().getModel().getName();
}
private String getUserId(ChatClientObservationContext context) {
// Extract from request metadata or return "anonymous"
return context.getRequest().getMetadata()
.getOrDefault("user.id", "anonymous");
}
};
}
}Cardinality Guidelines:
Low Cardinality (for metrics aggregation):
High Cardinality (for traces only):
Why It Matters:
# application-dev.yml
spring:
ai:
chat:
client:
enabled: true
observations:
log-prompt: true # Enable in dev for debugging
log-completion: true
logging:
level:
org.springframework.ai: DEBUG
---
# application-prod.yml
spring:
ai:
chat:
client:
enabled: true
observations:
log-prompt: false # Disable in prod for security
log-completion: false
logging:
level:
org.springframework.ai: INFOJava-Based Conditional Configuration:
import org.springframework.ai.chat.client.ChatClientCustomizer;
import org.springframework.ai.chat.model.ChatOptions;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Configuration
public class EnvironmentSpecificConfig {
@Bean
@Profile("dev")
public ChatClientCustomizer devCustomizer() {
return builder -> builder.defaultOptions(
ChatOptions.builder()
.withTemperature(0.0) // Deterministic for dev/testing
.withMaxTokens(500) // Smaller responses for faster testing
.build()
);
}
@Bean
@Profile("prod")
public ChatClientCustomizer prodCustomizer() {
return builder -> builder.defaultOptions(
ChatOptions.builder()
.withTemperature(0.7) // More creative for prod
.withMaxTokens(2000) // Longer responses allowed
.build()
);
}
@Bean
@Profile("!prod") // Any profile except prod
public ChatClientCustomizer nonProdCustomizer() {
return builder -> builder.defaultSystem(
"You are in development mode. Provide detailed explanations."
);
}
}import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import reactor.core.publisher.Flux;
@TestConfiguration
public class TestChatConfig {
@Bean
@Primary // Override the real ChatModel bean
public ChatModel mockChatModel() {
return new MockChatModel();
}
@Bean
public ChatClientCustomizer testCustomizer() {
return builder -> builder
.defaultSystem("Test mode")
.defaultOptions(ChatOptions.builder()
.withTemperature(0.0) // Deterministic for testing
.build());
}
/**
* Simple mock implementation for testing.
*/
private static class MockChatModel implements ChatModel {
@Override
public ChatResponse call(Prompt prompt) {
// Return mock response
return new ChatResponse(List.of(
new Generation("Mock response to: " + prompt.getContents())
));
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
// Return mock streaming response
return Flux.just(call(prompt));
}
}
}Usage in Tests:
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@Import(TestChatConfig.class)
class ChatServiceTest {
@Autowired
private ChatService chatService;
@Test
void testChat() {
String response = chatService.chat("Hello");
assertThat(response).contains("Mock response");
}
}Alternative: Mockito Approach:
import org.mockito.Mockito;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
@TestConfiguration
public class MockitoTestConfig {
@MockBean // Spring Boot's @MockBean automatically replaces the real bean
private ChatModel chatModel;
@Bean
public ChatClient.Builder testChatClientBuilder() {
// Configure mock behavior
Mockito.when(chatModel.call(Mockito.any(Prompt.class)))
.thenReturn(new ChatResponse(List.of(
new Generation("Mocked response")
)));
return ChatClient.builder(chatModel);
}
}import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatOptions;
import org.springframework.ai.model.chat.client.autoconfigure.ChatClientBuilderConfigurer;
import org.springframework.stereotype.Service;
@Service
public class DynamicChatClientFactory {
private final ChatModel chatModel;
private final ChatClientBuilderConfigurer configurer;
public DynamicChatClientFactory(
ChatModel chatModel,
ChatClientBuilderConfigurer configurer
) {
this.chatModel = chatModel;
this.configurer = configurer;
}
public ChatClient createClientForRole(String role, double temperature) {
ChatClient.Builder builder = ChatClient.builder(chatModel);
// Apply all registered customizers
configurer.configure(builder);
// Add role-specific configuration
switch (role.toLowerCase()) {
case "analyst":
builder.defaultSystem("You are a data analyst. Provide detailed analysis with numbers.");
builder.defaultOptions(ChatOptions.builder()
.withTemperature(temperature)
.withMaxTokens(2000)
.build());
break;
case "creative":
builder.defaultSystem("You are a creative writer. Be imaginative and engaging.");
builder.defaultOptions(ChatOptions.builder()
.withTemperature(Math.max(temperature, 0.8)) // At least 0.8 for creativity
.withMaxTokens(3000)
.build());
break;
case "technical":
builder.defaultSystem("You are a technical expert. Be precise and accurate.");
builder.defaultOptions(ChatOptions.builder()
.withTemperature(Math.min(temperature, 0.3)) // At most 0.3 for accuracy
.withMaxTokens(1500)
.build());
break;
default:
builder.defaultSystem("You are a helpful assistant.");
builder.defaultOptions(ChatOptions.builder()
.withTemperature(temperature)
.build());
}
return builder.build();
}
public ChatClient createClientWithMemory(ChatMemory memory) {
ChatClient.Builder builder = ChatClient.builder(chatModel);
configurer.configure(builder);
builder.defaultAdvisors(new MessageChatMemoryAdvisor(memory));
return builder.build();
}
}Usage:
@RestController
public class ChatController {
@Autowired
private DynamicChatClientFactory factory;
@PostMapping("/chat/{role}")
public String chat(@PathVariable String role, @RequestBody String message) {
ChatClient client = factory.createClientForRole(role, 0.7);
return client.prompt().user(message).call().content();
}
}import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.model.chat.client.autoconfigure.ChatClientBuilderConfigurer;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
@Component
public class ChatClientPool {
private final ChatModel chatModel;
private final ChatClientBuilderConfigurer configurer;
private final Map<String, ChatClient> clientPool = new ConcurrentHashMap<>();
public ChatClientPool(ChatModel chatModel, ChatClientBuilderConfigurer configurer) {
this.chatModel = chatModel;
this.configurer = configurer;
}
public ChatClient getOrCreateClient(String clientId, double temperature) {
return clientPool.computeIfAbsent(clientId, id -> {
ChatClient.Builder builder = ChatClient.builder(chatModel);
configurer.configure(builder);
builder.defaultOptions(ChatOptions.builder()
.withTemperature(temperature)
.build());
return builder.build();
});
}
public void removeClient(String clientId) {
clientPool.remove(clientId);
}
public void clear() {
clientPool.clear();
}
}Usage with Session-Based Clients:
@RestController
public class SessionChatController {
@Autowired
private ChatClientPool clientPool;
@PostMapping("/session/{sessionId}/chat")
public String chat(
@PathVariable String sessionId,
@RequestBody String message,
@RequestParam(defaultValue = "0.7") double temperature
) {
ChatClient client = clientPool.getOrCreateClient(sessionId, temperature);
return client.prompt().user(message).call().content();
}
@DeleteMapping("/session/{sessionId}")
public void endSession(@PathVariable String sessionId) {
clientPool.removeClient(sessionId);
}
}Design Principles:
Prototype Scope for Isolation: Each injection point receives an independent ChatClient.Builder instance, preventing unintended state sharing across the application. This is critical for multi-tenant applications or scenarios where different parts of the application need different configurations.
Convention Over Configuration: Works automatically when dependencies are present, with sensible defaults that can be overridden. Zero configuration required for basic usage - just add the dependency and inject the builder.
Extensibility Through Beans: All customization happens through Spring beans (customizers, conventions, handlers), maintaining clean separation of concerns. No need to extend classes or implement complex interfaces.
Conditional Activation: Uses Spring Boot's conditional annotations (@ConditionalOnClass, @ConditionalOnBean, @ConditionalOnProperty, @ConditionalOnMissingClass) to gracefully handle optional dependencies and feature toggling. The module works whether or not observability dependencies are present.
Security by Default: Sensitive content logging is disabled by default and issues warnings when enabled. Prevents accidental exposure of user data, API keys, or other sensitive information.
Observability Integration: First-class support for Micrometer observability, with automatic integration when infrastructure is available. Seamlessly integrates with distributed tracing systems (Zipkin, Jaeger) and metrics systems (Prometheus, CloudWatch).
Fail-Fast Validation: Missing required dependencies (like ChatModel bean) cause immediate application startup failure with clear error messages, rather than runtime errors.
Component Relationships:
ChatClientAutoConfiguration (main orchestrator)
├── Enables: ChatClientBuilderProperties
├── Creates: ChatClientBuilderConfigurer
│ └── Collects: List<ChatClientCustomizer>
│ ├── Applied in order (@Order)
│ └── Customizes each builder instance
├── Creates: ChatClient.Builder (prototype scope)
│ ├── Requires: ChatModel (from other auto-configs)
│ ├── Optionally uses: ObservationRegistry
│ ├── Optionally uses: ChatClientObservationConvention
│ ├── Optionally uses: AdvisorObservationConvention
│ └── Configured by: ChatClientBuilderConfigurer
└── Nested: Observation Handler Configurations
├── TracerPresentObservationConfiguration
│ └── Creates tracing-aware handlers when Tracer available
└── TracerNotPresentObservationConfiguration
└── Creates basic handlers when Tracer not availableBean Creation Flow:
Startup Phase:
ChatClientAutoConfiguration via auto-configuration imports@ConditionalOnClass(ChatClient.class) - checks if ChatClient is on classpath@ConditionalOnProperty - checks if auto-configuration is enabledProperty Binding:
@EnableConfigurationProperties(ChatClientBuilderProperties.class) triggers property bindingspring.ai.chat.client are bound to ChatClientBuilderPropertiesBean Registration:
ChatClientBuilderConfigurer bean is created first
ChatClientCustomizer beans via ObjectProviderTracerPresentObservationConfiguration or TracerNotPresentObservationConfiguration activatesChatClient.Builder bean definition is registered with prototype scope
Injection Time:
ChatClient.Builder is injected somewhere:
ChatClient.builder(chatModel)ObservationRegistry (if available)ChatClientBuilderConfigurer.configure(builder) to apply all customizersThreading Considerations:
Builder Thread-Safety: Each ChatClient.Builder instance is NOT thread-safe. It should be used to build a client and then discarded. Don't share builder instances across threads.
Client Thread-Safety: Once built, ChatClient instances are immutable and thread-safe. Multiple threads can safely call the same client instance.
Customizer Thread-Safety: ChatClientCustomizer.customize() is called during builder creation, which happens in the requesting thread. Customizers should be stateless or thread-safe.
Observation Handler Thread-Safety: Observation handlers must be thread-safe as they can be called from multiple threads concurrently when multiple requests are processed in parallel.
Memory Considerations:
Prototype Scope Overhead: Each injection creates a new builder instance. In applications with many injection points, this can create many builder objects. However, builders are lightweight and eligible for garbage collection after the client is built.
Client Pooling: If creating many clients dynamically, consider implementing a pooling strategy (see usage patterns) to avoid excessive object creation.
Observation Data: When observation logging is enabled, prompt and completion content is held in memory during observation processing. For large prompts/completions, this can increase memory usage.
Observability Architecture:
ChatClient.Builder
└── with ObservationRegistry
├── Creates observations for each chat request
│ ├── Applies ChatClientObservationConvention (if present)
│ └── Applies AdvisorObservationConvention (if present)
├── Observation handlers process observations
│ ├── ChatClientPromptContentObservationHandler (if enabled)
│ ├── ChatClientCompletionObservationHandler (if enabled)
│ └── Other handlers from ObservationRegistry
└── Observation data flows to:
├── Metrics systems (Prometheus, CloudWatch, etc.)
├── Tracing systems (Zipkin, Jaeger, etc.)
└── Logging systems (via observation handlers)Error Handling:
Missing ChatModel: If no ChatModel bean is found, application startup fails with:
Parameter 1 of method chatClientBuilder in o.s.a.m.c.c.a.ChatClientAutoConfiguration required a bean of type 'o.s.a.c.m.ChatModel' that could not be found.Customizer Exceptions: If a ChatClientCustomizer throws an exception during customize(), the application context fails to start. Customizers should handle exceptions gracefully or use try-catch blocks.
Builder Configuration Errors: Invalid builder configurations (e.g., null values) typically throw NullPointerException or IllegalArgumentException during builder method calls.
Runtime Chat Errors: Errors during actual chat requests (network issues, API errors, etc.) are thrown by the ChatClient and must be handled by the calling code.
Backwards Compatibility:
Property Changes: Property names are considered public API and changes would be breaking. New properties are added with backwards-compatible defaults.
Bean Names: Bean names (when explicitly specified) are part of the public API. The prototype-scoped ChatClient.Builder bean doesn't have an explicit name to avoid conflicts.
Customizer API: The ChatClientCustomizer interface is stable. New methods would be default methods to maintain backwards compatibility.
Observation Conventions: Convention interfaces can add new default methods without breaking existing implementations.
Performance Characteristics:
Startup Time: Minimal impact. Bean creation is fast, and prototype beans are created lazily on first injection.
Builder Creation: Very fast (microseconds). Creating a builder involves cloning configuration and applying customizers.
Client Creation: Fast (milliseconds). Building a client from a builder is lightweight.
Observation Overhead: When observability is enabled, there's a small overhead (typically < 1% of request time) for creating and processing observations.
Best Practices:
Inject Builder, Not Client: Always inject ChatClient.Builder and call .build() when needed. This gives you the flexibility to customize per-instance.
Reuse Clients: Once built, reuse ChatClient instances. Don't build a new client for every request unless you need different configurations.
Order Customizers: Use @Order on customizers to ensure predictable configuration order.
Secure Observation Logging: Only enable log-prompt and log-completion in development environments. Never enable in production without sanitization.
Use Low-Cardinality Tags: When creating custom observation conventions, use low-cardinality tags for metrics and high-cardinality tags for traces only.
Test With Mocks: In tests, provide a mock ChatModel to avoid making real API calls.
Handle Errors: Always handle exceptions when calling ChatClient methods, as network errors, API errors, and rate limits can occur.
Configure Timeouts: Use chat options to configure appropriate timeouts for your use case.
Monitor Observability: Set up dashboards and alerts based on ChatClient observations to monitor usage, errors, and performance.
Document Custom Conventions: If you create custom observation conventions, document the tags and their meanings for operations teams.
tessl i tessl/maven-org-springframework-ai--spring-ai-autoconfigure-model-chat-client@1.1.1