Skip to content

Commit 29eb951

Browse files
committed
perf(anthropic): support HTTP client timeout configuration
Signed-off-by: yinh <[email protected]>
1 parent 7b88929 commit 29eb951

File tree

4 files changed

+109
-27
lines changed

4 files changed

+109
-27
lines changed

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

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,18 @@
3434
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
3535
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
3636
import org.springframework.boot.context.properties.EnableConfigurationProperties;
37+
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder;
38+
import org.springframework.boot.http.client.HttpClientSettings;
39+
import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsPropertyMapper;
40+
import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder;
3741
import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration;
42+
import org.springframework.boot.ssl.SslBundles;
3843
import org.springframework.boot.webclient.autoconfigure.WebClientAutoConfiguration;
3944
import org.springframework.context.annotation.Bean;
4045
import org.springframework.context.annotation.Import;
4146
import org.springframework.core.retry.RetryTemplate;
47+
import org.springframework.http.client.ClientHttpRequestFactory;
48+
import org.springframework.http.client.reactive.ClientHttpConnector;
4249
import org.springframework.web.client.ResponseErrorHandler;
4350
import org.springframework.web.client.RestClient;
4451
import org.springframework.web.reactive.function.client.WebClient;
@@ -65,15 +72,30 @@ public class AnthropicChatAutoConfiguration {
6572
@ConditionalOnMissingBean
6673
public AnthropicApi anthropicApi(AnthropicConnectionProperties connectionProperties,
6774
ObjectProvider<RestClient.Builder> restClientBuilderProvider,
68-
ObjectProvider<WebClient.Builder> webClientBuilderProvider, ResponseErrorHandler responseErrorHandler) {
75+
ObjectProvider<WebClient.Builder> webClientBuilderProvider, ResponseErrorHandler responseErrorHandler,
76+
ObjectProvider<SslBundles> sslBundles, ObjectProvider<HttpClientSettings> globalHttpClientSettings,
77+
ObjectProvider<ClientHttpRequestFactoryBuilder<?>> factoryBuilder,
78+
ObjectProvider<ClientHttpConnectorBuilder<?>> webConnectorBuilderProvider) {
79+
80+
HttpClientSettingsPropertyMapper mapper = new HttpClientSettingsPropertyMapper(sslBundles.getIfAvailable(),
81+
globalHttpClientSettings.getIfAvailable());
82+
HttpClientSettings httpClientSettings = mapper.map(connectionProperties);
83+
84+
RestClient.Builder restClientBuilder = restClientBuilderProvider.getIfAvailable(RestClient::builder);
85+
applyRestClientSettings(restClientBuilder, httpClientSettings,
86+
factoryBuilder.getIfAvailable(ClientHttpRequestFactoryBuilder::detect));
87+
88+
WebClient.Builder webClientBuilder = webClientBuilderProvider.getIfAvailable(WebClient::builder);
89+
applyWebClientSettings(webClientBuilder, httpClientSettings,
90+
webConnectorBuilderProvider.getIfAvailable(ClientHttpConnectorBuilder::detect));
6991

7092
return AnthropicApi.builder()
7193
.baseUrl(connectionProperties.getBaseUrl())
7294
.completionsPath(connectionProperties.getCompletionsPath())
7395
.apiKey(connectionProperties.getApiKey())
7496
.anthropicVersion(connectionProperties.getVersion())
75-
.restClientBuilder(restClientBuilderProvider.getIfAvailable(RestClient::builder))
76-
.webClientBuilder(webClientBuilderProvider.getIfAvailable(WebClient::builder))
97+
.restClientBuilder(restClientBuilder)
98+
.webClientBuilder(webClientBuilder)
7799
.responseErrorHandler(responseErrorHandler)
78100
.anthropicBetaFeatures(connectionProperties.getBetaVersion())
79101
.build();
@@ -102,4 +124,16 @@ public AnthropicChatModel anthropicChatModel(AnthropicApi anthropicApi, Anthropi
102124
return chatModel;
103125
}
104126

127+
private void applyRestClientSettings(RestClient.Builder builder, HttpClientSettings httpClientSettings,
128+
ClientHttpRequestFactoryBuilder<?> factoryBuilder) {
129+
ClientHttpRequestFactory requestFactory = factoryBuilder.build(httpClientSettings);
130+
builder.requestFactory(requestFactory);
131+
}
132+
133+
private void applyWebClientSettings(WebClient.Builder builder, HttpClientSettings httpClientSettings,
134+
ClientHttpConnectorBuilder<?> connectorBuilder) {
135+
ClientHttpConnector connector = connectorBuilder.build(httpClientSettings);
136+
builder.clientConnector(connector);
137+
}
138+
105139
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import org.springframework.ai.anthropic.api.AnthropicApi;
2020
import org.springframework.boot.context.properties.ConfigurationProperties;
21+
import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsProperties;
2122

2223
/**
2324
* Anthropic API connection properties.
@@ -26,7 +27,7 @@
2627
* @since 1.0.0
2728
*/
2829
@ConfigurationProperties(AnthropicConnectionProperties.CONFIG_PREFIX)
29-
public class AnthropicConnectionProperties {
30+
public class AnthropicConnectionProperties extends HttpClientSettingsProperties {
3031

3132
public static final String CONFIG_PREFIX = "spring.ai.anthropic";
3233

auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicChatAutoConfigurationIT.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717
package org.springframework.ai.model.anthropic.autoconfigure;
1818

19+
import java.time.Duration;
1920
import java.util.List;
21+
import java.util.Objects;
2022
import java.util.stream.Collectors;
2123

2224
import org.apache.commons.logging.Log;
@@ -91,4 +93,51 @@ void stream() {
9193
});
9294
}
9395

96+
@Test
97+
void generateWithCustomTimeout() {
98+
new ApplicationContextRunner()
99+
.withPropertyValues("spring.ai.anthropic.apiKey=" + System.getenv("ANTHROPIC_API_KEY"),
100+
"spring.ai.deepseek.connect-timeout=1ms", "spring.ai.deepseek.read-timeout=1ms")
101+
.withConfiguration(SpringAiTestAutoConfigurations.of(AnthropicChatAutoConfiguration.class))
102+
.run(context -> {
103+
AnthropicChatModel client = context.getBean(AnthropicChatModel.class);
104+
105+
// Verify that the HTTP client configuration is applied
106+
var connectionProperties = context.getBean(AnthropicConnectionProperties.class);
107+
assertThat(connectionProperties.getConnectTimeout()).isEqualTo(Duration.ofMillis(1));
108+
assertThat(connectionProperties.getReadTimeout()).isEqualTo(Duration.ofMillis(1));
109+
110+
// Verify that the client can actually make requests with the configured
111+
// timeout
112+
String response = client.call("Hello");
113+
assertThat(response).isNotEmpty();
114+
logger.info("Response with custom timeout: " + response);
115+
});
116+
}
117+
118+
@Test
119+
void generateStreamingWithCustomTimeout() {
120+
new ApplicationContextRunner()
121+
.withPropertyValues("spring.ai.deepseek.apiKey=" + "sk-2567813d742c40e79fa6f1f2ee2f830c",
122+
"spring.ai.deepseek.connect-timeout=1s", "spring.ai.deepseek.read-timeout=1s")
123+
.withConfiguration(SpringAiTestAutoConfigurations.of(AnthropicChatAutoConfiguration.class))
124+
.run(context -> {
125+
AnthropicChatModel client = context.getBean(AnthropicChatModel.class);
126+
127+
// Verify that the HTTP client configuration is applied
128+
var connectionProperties = context.getBean(AnthropicConnectionProperties.class);
129+
assertThat(connectionProperties.getConnectTimeout()).isEqualTo(Duration.ofSeconds(1));
130+
assertThat(connectionProperties.getReadTimeout()).isEqualTo(Duration.ofSeconds(1));
131+
132+
Flux<ChatResponse> responseFlux = client.stream(new Prompt(new UserMessage("Hello")));
133+
String response = Objects.requireNonNull(responseFlux.collectList().block())
134+
.stream()
135+
.map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText())
136+
.collect(Collectors.joining());
137+
138+
assertThat(response).isNotEmpty();
139+
logger.info("Response with custom timeout: " + response);
140+
});
141+
}
142+
94143
}

auto-configurations/models/spring-ai-autoconfigure-model-deepseek/src/test/java/org/springframework/ai/model/deepseek/autoconfigure/DeepSeekAutoConfigurationIT.java

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,7 @@ void generateStreaming() {
7878
void generateWithCustomTimeout() {
7979
new ApplicationContextRunner()
8080
.withPropertyValues("spring.ai.deepseek.apiKey=" + System.getenv("DEEPSEEK_API_KEY"),
81-
"spring.ai.deepseek.connect-timeout=5s",
82-
"spring.ai.deepseek.read-timeout=30s")
81+
"spring.ai.deepseek.connect-timeout=5s", "spring.ai.deepseek.read-timeout=30s")
8382
.withConfiguration(SpringAiTestAutoConfigurations.of(DeepSeekChatAutoConfiguration.class))
8483
.run(context -> {
8584
DeepSeekChatModel client = context.getBean(DeepSeekChatModel.class);
@@ -100,27 +99,26 @@ void generateWithCustomTimeout() {
10099
@Test
101100
void generateStreamingWithCustomTimeout() {
102101
new ApplicationContextRunner()
103-
.withPropertyValues("spring.ai.deepseek.apiKey=" + "sk-2567813d742c40e79fa6f1f2ee2f830c",
104-
"spring.ai.deepseek.connect-timeout=1s",
105-
"spring.ai.deepseek.read-timeout=1s")
106-
.withConfiguration(SpringAiTestAutoConfigurations.of(DeepSeekChatAutoConfiguration.class))
107-
.run(context -> {
108-
DeepSeekChatModel client = context.getBean(DeepSeekChatModel.class);
109-
110-
// Verify that the HTTP client configuration is applied
111-
var connectionProperties = context.getBean(DeepSeekConnectionProperties.class);
112-
assertThat(connectionProperties.getConnectTimeout()).isEqualTo(Duration.ofSeconds(1));
113-
assertThat(connectionProperties.getReadTimeout()).isEqualTo(Duration.ofSeconds(1));
114-
115-
Flux<ChatResponse> responseFlux = client.stream(new Prompt(new UserMessage("Hello")));
116-
String response = Objects.requireNonNull(responseFlux.collectList().block())
117-
.stream()
118-
.map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText())
119-
.collect(Collectors.joining());
120-
121-
assertThat(response).isNotEmpty();
122-
logger.info("Response with custom timeout: " + response);
123-
});
102+
.withPropertyValues("spring.ai.deepseek.apiKey=" + System.getenv("DEEPSEEK_API_KEY"),
103+
"spring.ai.deepseek.connect-timeout=1s", "spring.ai.deepseek.read-timeout=1s")
104+
.withConfiguration(SpringAiTestAutoConfigurations.of(DeepSeekChatAutoConfiguration.class))
105+
.run(context -> {
106+
DeepSeekChatModel client = context.getBean(DeepSeekChatModel.class);
107+
108+
// Verify that the HTTP client configuration is applied
109+
var connectionProperties = context.getBean(DeepSeekConnectionProperties.class);
110+
assertThat(connectionProperties.getConnectTimeout()).isEqualTo(Duration.ofSeconds(1));
111+
assertThat(connectionProperties.getReadTimeout()).isEqualTo(Duration.ofSeconds(1));
112+
113+
Flux<ChatResponse> responseFlux = client.stream(new Prompt(new UserMessage("Hello")));
114+
String response = Objects.requireNonNull(responseFlux.collectList().block())
115+
.stream()
116+
.map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText())
117+
.collect(Collectors.joining());
118+
119+
assertThat(response).isNotEmpty();
120+
logger.info("Response with custom timeout: " + response);
121+
});
124122
}
125123

126124
}

0 commit comments

Comments
 (0)