Skip to content

Commit 51b8d6b

Browse files
committed
ChatClientAutoConfiguration should back off if there are multiple ChatModels
Signed-off-by: Filip Hrisafov <[email protected]>
1 parent 88e03cd commit 51b8d6b

File tree

15 files changed

+211
-24
lines changed

15 files changed

+211
-24
lines changed

auto-configurations/models/chat/client/spring-ai-autoconfigure-model-chat-client/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientAutoConfiguration.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
3939
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
4040
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
41+
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
4142
import org.springframework.boot.context.properties.EnableConfigurationProperties;
4243
import org.springframework.context.annotation.Bean;
4344
import org.springframework.context.annotation.Configuration;
@@ -89,6 +90,7 @@ ChatClientBuilderConfigurer chatClientBuilderConfigurer(ObjectProvider<ChatClien
8990
@Bean
9091
@Scope("prototype")
9192
@ConditionalOnMissingBean
93+
@ConditionalOnSingleCandidate(ChatModel.class)
9294
ChatClient.Builder chatClientBuilder(ChatClientBuilderConfigurer chatClientBuilderConfigurer, ChatModel chatModel,
9395
ObjectProvider<ObservationRegistry> observationRegistry,
9496
ObjectProvider<ChatClientObservationConvention> chatClientObservationConvention,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ai.model.chat.client.autoconfigure;
18+
19+
import java.util.List;
20+
21+
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.api.extension.ExtendWith;
23+
import org.mockito.ArgumentCaptor;
24+
25+
import org.springframework.ai.chat.client.ChatClient;
26+
import org.springframework.ai.chat.client.ChatClientCustomizer;
27+
import org.springframework.ai.chat.messages.AssistantMessage;
28+
import org.springframework.ai.chat.messages.Message;
29+
import org.springframework.ai.chat.messages.MessageType;
30+
import org.springframework.ai.chat.model.ChatModel;
31+
import org.springframework.ai.chat.model.ChatResponse;
32+
import org.springframework.ai.chat.model.Generation;
33+
import org.springframework.ai.chat.prompt.Prompt;
34+
import org.springframework.ai.content.Content;
35+
import org.springframework.boot.autoconfigure.AutoConfigurations;
36+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
37+
import org.springframework.boot.test.system.OutputCaptureExtension;
38+
import org.springframework.context.annotation.Bean;
39+
import org.springframework.context.annotation.Configuration;
40+
41+
import static org.assertj.core.api.Assertions.assertThat;
42+
import static org.assertj.core.api.Assertions.tuple;
43+
import static org.mockito.ArgumentMatchers.any;
44+
import static org.mockito.Mockito.mock;
45+
import static org.mockito.Mockito.verify;
46+
import static org.mockito.Mockito.when;
47+
48+
/**
49+
* @author Filip Hrisafov
50+
*/
51+
@ExtendWith(OutputCaptureExtension.class)
52+
class ChatClientAutoConfigurationTests {
53+
54+
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
55+
.withConfiguration(AutoConfigurations.of(ChatClientAutoConfiguration.class))
56+
.withUserConfiguration(MockConfig.class);
57+
58+
@Test
59+
void implicitlyEnabled() {
60+
this.contextRunner.run(context -> assertThat(context.getBeansOfType(ChatClient.Builder.class)).isNotEmpty());
61+
}
62+
63+
@Test
64+
void explicitlyEnabled() {
65+
this.contextRunner.withPropertyValues("spring.ai.chat.client.enabled=true")
66+
.run(context -> assertThat(context.getBeansOfType(ChatClient.Builder.class)).isNotEmpty());
67+
}
68+
69+
@Test
70+
void explicitlyDisabled() {
71+
this.contextRunner.withPropertyValues("spring.ai.chat.client.enabled=false")
72+
.run(context -> assertThat(context.getBeansOfType(ChatClient.Builder.class)).isEmpty());
73+
}
74+
75+
@Test
76+
void generate() {
77+
this.contextRunner.run(context -> {
78+
ChatClient.Builder builder = context.getBean(ChatClient.Builder.class);
79+
80+
assertThat(builder).isNotNull();
81+
82+
ChatClient chatClient = builder.build();
83+
ChatModel chatModel = context.getBean(ChatModel.class);
84+
85+
ChatResponse response = ChatResponse.builder()
86+
.generations(List.of(new Generation(new AssistantMessage("Test"))))
87+
.build();
88+
when(chatModel.call(any(Prompt.class))).thenReturn(response);
89+
90+
ChatResponse chatResponse = chatClient.prompt().user("Hello").call().chatResponse();
91+
assertThat(chatResponse).isSameAs(response);
92+
});
93+
}
94+
95+
@Test
96+
void testChatClientCustomizers() {
97+
this.contextRunner.withUserConfiguration(Config.class).run(context -> {
98+
99+
ChatClient.Builder builder = context.getBean(ChatClient.Builder.class);
100+
101+
ChatClient chatClient = builder.build();
102+
103+
assertThat(chatClient).isNotNull();
104+
105+
ChatModel chatModel = context.getBean(ChatModel.class);
106+
107+
ChatResponse response = ChatResponse.builder()
108+
.generations(List.of(new Generation(new AssistantMessage("Test"))))
109+
.build();
110+
when(chatModel.call(any(Prompt.class))).thenReturn(response);
111+
chatClient.prompt().user(u -> u.param("actor", "Tom Hanks")).call().chatResponse();
112+
113+
ArgumentCaptor<Prompt> promptArgument = ArgumentCaptor.forClass(Prompt.class);
114+
115+
verify(chatModel).call(promptArgument.capture());
116+
117+
Prompt prompt = promptArgument.getValue();
118+
assertThat(prompt.getInstructions()).extracting(Message::getMessageType, Content::getText)
119+
.containsExactly(tuple(MessageType.SYSTEM, "You are a movie expert."),
120+
tuple(MessageType.USER, "Generate the filmography of 5 movies for Tom Hanks."));
121+
});
122+
}
123+
124+
@Test
125+
void withMultipleChatModels() {
126+
this.contextRunner.withUserConfiguration(SecondChatModelConfig.class).run(context -> {
127+
assertThat(context).hasNotFailed();
128+
assertThat(context.getBeansOfType(ChatClient.Builder.class)).isEmpty();
129+
});
130+
}
131+
132+
record ActorsFilms(String actor, List<String> movies) {
133+
134+
}
135+
136+
@Configuration
137+
static class MockConfig {
138+
139+
@Bean
140+
ChatModel chatModel() {
141+
return mock(ChatModel.class);
142+
}
143+
144+
}
145+
146+
@Configuration
147+
static class SecondChatModelConfig {
148+
149+
@Bean
150+
ChatModel secondChatModel() {
151+
return mock(ChatModel.class);
152+
}
153+
154+
}
155+
156+
@Configuration
157+
static class Config {
158+
159+
@Bean
160+
public ChatClientCustomizer chatClientCustomizer() {
161+
return b -> b.defaultSystem("You are a movie expert.")
162+
.defaultUser("Generate the filmography of 5 movies for {actor}.");
163+
}
164+
165+
}
166+
167+
}

auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicChatAutoConfiguration.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,10 @@
5252
* @author Hyoseop Song
5353
* @since 1.0.0
5454
*/
55-
@AutoConfiguration(after = { RestClientAutoConfiguration.class, WebClientAutoConfiguration.class,
56-
ToolCallingAutoConfiguration.class, SpringAiRetryAutoConfiguration.class })
55+
@AutoConfiguration(
56+
after = { RestClientAutoConfiguration.class, WebClientAutoConfiguration.class,
57+
ToolCallingAutoConfiguration.class, SpringAiRetryAutoConfiguration.class },
58+
beforeName = "org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration")
5759
@EnableConfigurationProperties({ AnthropicChatProperties.class, AnthropicConnectionProperties.class })
5860
@ConditionalOnClass(AnthropicApi.class)
5961
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.ANTHROPIC,

auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiChatAutoConfiguration.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@
4444
* @author Manuel Andreo Garcia
4545
* @author Ilayaperumal Gopinathan
4646
*/
47-
@AutoConfiguration(after = ToolCallingAutoConfiguration.class)
47+
@AutoConfiguration(after = ToolCallingAutoConfiguration.class,
48+
beforeName = "org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration")
4849
@ConditionalOnClass(AzureOpenAiChatModel.class)
4950
@EnableConfigurationProperties(AzureOpenAiChatProperties.class)
5051
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.AZURE_OPENAI,

auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/converse/autoconfigure/BedrockConverseProxyChatAutoConfiguration.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2024-2024 the original author or authors.
2+
* Copyright 2024-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -51,7 +51,8 @@
5151
* @author Wei Jiang
5252
* @author Pawel Potaczala
5353
*/
54-
@AutoConfiguration(after = ToolCallingAutoConfiguration.class)
54+
@AutoConfiguration(after = ToolCallingAutoConfiguration.class,
55+
beforeName = "org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration")
5556
@EnableConfigurationProperties({ BedrockConverseProxyChatProperties.class, BedrockAwsConnectionConfiguration.class })
5657
@ConditionalOnClass({ BedrockProxyChatModel.class, BedrockRuntimeClient.class, BedrockRuntimeAsyncClient.class })
5758
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.BEDROCK_CONVERSE,

auto-configurations/models/spring-ai-autoconfigure-model-deepseek/src/main/java/org/springframework/ai/model/deepseek/autoconfigure/DeepSeekChatAutoConfiguration.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2024 the original author or authors.
2+
* Copyright 2023-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -51,8 +51,10 @@
5151
* @author Geng Rong
5252
* @author Hyunsang Han
5353
*/
54-
@AutoConfiguration(after = { RestClientAutoConfiguration.class, WebClientAutoConfiguration.class,
55-
SpringAiRetryAutoConfiguration.class, ToolCallingAutoConfiguration.class })
54+
@AutoConfiguration(
55+
after = { RestClientAutoConfiguration.class, WebClientAutoConfiguration.class,
56+
SpringAiRetryAutoConfiguration.class, ToolCallingAutoConfiguration.class },
57+
beforeName = "org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration")
5658
@ConditionalOnClass(DeepSeekApi.class)
5759
@EnableConfigurationProperties({ DeepSeekConnectionProperties.class, DeepSeekChatProperties.class })
5860
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.DEEPSEEK,

auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/main/java/org/springframework/ai/model/google/genai/autoconfigure/chat/GoogleGenAiChatAutoConfiguration.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@
5555
* @author Ilayaperumal Gopinathan
5656
* @since 1.1.0
5757
*/
58-
@AutoConfiguration(after = { SpringAiRetryAutoConfiguration.class, ToolCallingAutoConfiguration.class })
58+
@AutoConfiguration(after = { SpringAiRetryAutoConfiguration.class, ToolCallingAutoConfiguration.class },
59+
beforeName = "org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration")
5960
@ConditionalOnClass({ Client.class, GoogleGenAiChatModel.class })
6061
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.GOOGLE_GEN_AI,
6162
matchIfMissing = true)

auto-configurations/models/spring-ai-autoconfigure-model-huggingface/src/main/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatAutoConfiguration.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2024 the original author or authors.
2+
* Copyright 2023-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,7 +26,7 @@
2626
import org.springframework.boot.context.properties.EnableConfigurationProperties;
2727
import org.springframework.context.annotation.Bean;
2828

29-
@AutoConfiguration
29+
@AutoConfiguration(beforeName = "org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration")
3030
@ConditionalOnClass(HuggingfaceChatModel.class)
3131
@EnableConfigurationProperties(HuggingfaceChatProperties.class)
3232
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.HUGGINGFACE,

auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxChatAutoConfiguration.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2024 the original author or authors.
2+
* Copyright 2023-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -49,8 +49,10 @@
4949
* @author Ilayaperumal Gopinathan
5050
* @author Issam El-atif
5151
*/
52-
@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class,
53-
ToolCallingAutoConfiguration.class })
52+
@AutoConfiguration(
53+
after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class,
54+
ToolCallingAutoConfiguration.class },
55+
beforeName = "org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration")
5456
@ConditionalOnClass(MiniMaxApi.class)
5557
@EnableConfigurationProperties({ MiniMaxConnectionProperties.class, MiniMaxChatProperties.class })
5658
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.MINIMAX,

auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiChatAutoConfiguration.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,10 @@
5353
* @author Ilayaperumal Gopinathan
5454
* @since 0.8.1
5555
*/
56-
@AutoConfiguration(after = { RestClientAutoConfiguration.class, WebClientAutoConfiguration.class,
57-
SpringAiRetryAutoConfiguration.class, ToolCallingAutoConfiguration.class })
56+
@AutoConfiguration(
57+
after = { RestClientAutoConfiguration.class, WebClientAutoConfiguration.class,
58+
SpringAiRetryAutoConfiguration.class, ToolCallingAutoConfiguration.class },
59+
beforeName = "org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration")
5860
@EnableConfigurationProperties({ MistralAiCommonProperties.class, MistralAiChatProperties.class })
5961
@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.MISTRAL,
6062
matchIfMissing = true)

0 commit comments

Comments
 (0)