diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicChatAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicChatAutoConfiguration.java index 0ec1bf2636f..1f2ca4cc7a0 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicChatAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicChatAutoConfiguration.java @@ -34,11 +34,18 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsPropertyMapper; +import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder; import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; +import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.webclient.autoconfigure.WebClientAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.core.retry.RetryTemplate; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; import org.springframework.web.reactive.function.client.WebClient; @@ -65,15 +72,30 @@ public class AnthropicChatAutoConfiguration { @ConditionalOnMissingBean public AnthropicApi anthropicApi(AnthropicConnectionProperties connectionProperties, ObjectProvider restClientBuilderProvider, - ObjectProvider webClientBuilderProvider, ResponseErrorHandler responseErrorHandler) { + ObjectProvider webClientBuilderProvider, ResponseErrorHandler responseErrorHandler, + ObjectProvider sslBundles, ObjectProvider globalHttpClientSettings, + ObjectProvider> factoryBuilder, + ObjectProvider> webConnectorBuilderProvider) { + + HttpClientSettingsPropertyMapper mapper = new HttpClientSettingsPropertyMapper(sslBundles.getIfAvailable(), + globalHttpClientSettings.getIfAvailable()); + HttpClientSettings httpClientSettings = mapper.map(connectionProperties.getHttp()); + + RestClient.Builder restClientBuilder = restClientBuilderProvider.getIfAvailable(RestClient::builder); + applyRestClientSettings(restClientBuilder, httpClientSettings, + factoryBuilder.getIfAvailable(ClientHttpRequestFactoryBuilder::detect)); + + WebClient.Builder webClientBuilder = webClientBuilderProvider.getIfAvailable(WebClient::builder); + applyWebClientSettings(webClientBuilder, httpClientSettings, + webConnectorBuilderProvider.getIfAvailable(ClientHttpConnectorBuilder::detect)); return AnthropicApi.builder() .baseUrl(connectionProperties.getBaseUrl()) .completionsPath(connectionProperties.getCompletionsPath()) .apiKey(connectionProperties.getApiKey()) .anthropicVersion(connectionProperties.getVersion()) - .restClientBuilder(restClientBuilderProvider.getIfAvailable(RestClient::builder)) - .webClientBuilder(webClientBuilderProvider.getIfAvailable(WebClient::builder)) + .restClientBuilder(restClientBuilder) + .webClientBuilder(webClientBuilder) .responseErrorHandler(responseErrorHandler) .anthropicBetaFeatures(connectionProperties.getBetaVersion()) .build(); @@ -102,4 +124,16 @@ public AnthropicChatModel anthropicChatModel(AnthropicApi anthropicApi, Anthropi return chatModel; } + private void applyRestClientSettings(RestClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpRequestFactoryBuilder factoryBuilder) { + ClientHttpRequestFactory requestFactory = factoryBuilder.build(httpClientSettings); + builder.requestFactory(requestFactory); + } + + private void applyWebClientSettings(WebClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpConnectorBuilder connectorBuilder) { + ClientHttpConnector connector = connectorBuilder.build(httpClientSettings); + builder.clientConnector(connector); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicConnectionProperties.java b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicConnectionProperties.java index 871bae627a6..38624678d4a 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicConnectionProperties.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicConnectionProperties.java @@ -16,8 +16,15 @@ package org.springframework.ai.model.anthropic.autoconfigure; +import java.time.Duration; + +import jakarta.annotation.Nullable; + import org.springframework.ai.anthropic.api.AnthropicApi; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.boot.http.client.HttpRedirects; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsProperties; /** * Anthropic API connection properties. @@ -56,6 +63,10 @@ public class AnthropicConnectionProperties { */ private String betaVersion = AnthropicApi.DEFAULT_ANTHROPIC_BETA_VERSION; + @NestedConfigurationProperty + private final HttpClientSettingsProperties http = new HttpClientSettingsProperties() { + }; + public String getApiKey() { return this.apiKey; } @@ -96,4 +107,39 @@ public void setBetaVersion(String betaVersion) { this.betaVersion = betaVersion; } + @Nullable + public HttpRedirects getRedirects() { + return this.http.getRedirects(); + } + + public void setRedirects(HttpRedirects redirects) { + this.http.setRedirects(redirects); + } + + @Nullable + public Duration getConnectTimeout() { + return this.http.getConnectTimeout(); + } + + public void setConnectTimeout(Duration connectTimeout) { + this.http.setConnectTimeout(connectTimeout); + } + + @Nullable + public Duration getReadTimeout() { + return this.http.getReadTimeout(); + } + + public void setReadTimeout(Duration readTimeout) { + this.http.setReadTimeout(readTimeout); + } + + public HttpClientSettingsProperties.Ssl getSsl() { + return this.http.getSsl(); + } + + public HttpClientSettingsProperties getHttp() { + return this.http; + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicChatAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicChatAutoConfigurationIT.java index 53f37f337fa..516fae57ee6 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicChatAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicChatAutoConfigurationIT.java @@ -16,7 +16,9 @@ package org.springframework.ai.model.anthropic.autoconfigure; +import java.time.Duration; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import org.apache.commons.logging.Log; @@ -91,4 +93,51 @@ void stream() { }); } + @Test + void generateWithCustomTimeout() { + new ApplicationContextRunner() + .withPropertyValues("spring.ai.anthropic.apiKey=" + System.getenv("ANTHROPIC_API_KEY"), + "spring.ai.deepseek.connect-timeout=1ms", "spring.ai.deepseek.read-timeout=1ms") + .withConfiguration(SpringAiTestAutoConfigurations.of(AnthropicChatAutoConfiguration.class)) + .run(context -> { + AnthropicChatModel client = context.getBean(AnthropicChatModel.class); + + // Verify that the HTTP client configuration is applied + var connectionProperties = context.getBean(AnthropicConnectionProperties.class); + assertThat(connectionProperties.getConnectTimeout()).isEqualTo(Duration.ofMillis(1)); + assertThat(connectionProperties.getReadTimeout()).isEqualTo(Duration.ofMillis(1)); + + // Verify that the client can actually make requests with the configured + // timeout + String response = client.call("Hello"); + assertThat(response).isNotEmpty(); + logger.info("Response with custom timeout: " + response); + }); + } + + @Test + void generateStreamingWithCustomTimeout() { + new ApplicationContextRunner() + .withPropertyValues("spring.ai.deepseek.apiKey=" + "sk-2567813d742c40e79fa6f1f2ee2f830c", + "spring.ai.deepseek.connect-timeout=1s", "spring.ai.deepseek.read-timeout=1s") + .withConfiguration(SpringAiTestAutoConfigurations.of(AnthropicChatAutoConfiguration.class)) + .run(context -> { + AnthropicChatModel client = context.getBean(AnthropicChatModel.class); + + // Verify that the HTTP client configuration is applied + var connectionProperties = context.getBean(AnthropicConnectionProperties.class); + assertThat(connectionProperties.getConnectTimeout()).isEqualTo(Duration.ofSeconds(1)); + assertThat(connectionProperties.getReadTimeout()).isEqualTo(Duration.ofSeconds(1)); + + Flux responseFlux = client.stream(new Prompt(new UserMessage("Hello"))); + String response = Objects.requireNonNull(responseFlux.collectList().block()) + .stream() + .map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText()) + .collect(Collectors.joining()); + + assertThat(response).isNotEmpty(); + logger.info("Response with custom timeout: " + response); + }); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiClientBuilderConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiClientBuilderConfiguration.java index aff04d20a2d..f0553152c98 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiClientBuilderConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiClientBuilderConfiguration.java @@ -23,8 +23,8 @@ import com.azure.ai.openai.OpenAIClientBuilder; import com.azure.core.credential.AzureKeyCredential; import com.azure.core.credential.KeyCredential; -import com.azure.core.util.ClientOptions; import com.azure.core.util.Header; +import com.azure.core.util.HttpClientOptions; import com.azure.identity.DefaultAzureCredentialBuilder; import org.springframework.beans.factory.ObjectProvider; @@ -56,23 +56,17 @@ public OpenAIClientBuilder openAIClientBuilder(AzureOpenAiConnectionProperties c final OpenAIClientBuilder clientBuilder; + HttpClientOptions clientOptions = createHttpClientOptions(connectionProperties); + // Connect to OpenAI (e.g. not the Azure OpenAI). The deploymentName property is // used as OpenAI model name. if (StringUtils.hasText(connectionProperties.getOpenAiApiKey())) { clientBuilder = new OpenAIClientBuilder().endpoint("https://api.openai.com/v1") .credential(new KeyCredential(connectionProperties.getOpenAiApiKey())) - .clientOptions(new ClientOptions().setApplicationId(APPLICATION_ID)); + .clientOptions(clientOptions); applyOpenAIClientBuilderCustomizers(clientBuilder, customizers); return clientBuilder; } - - Map customHeaders = connectionProperties.getCustomHeaders(); - List
headers = customHeaders.entrySet() - .stream() - .map(entry -> new Header(entry.getKey(), entry.getValue())) - .collect(Collectors.toList()); - ClientOptions clientOptions = new ClientOptions().setApplicationId(APPLICATION_ID).setHeaders(headers); - Assert.hasText(connectionProperties.getEndpoint(), "Endpoint must not be empty"); if (!StringUtils.hasText(connectionProperties.getApiKey())) { @@ -96,4 +90,44 @@ private void applyOpenAIClientBuilderCustomizers(OpenAIClientBuilder clientBuild customizers.orderedStream().forEach(customizer -> customizer.customize(clientBuilder)); } + /** + * Create HttpClientOptions + */ + private HttpClientOptions createHttpClientOptions(AzureOpenAiConnectionProperties connectionProperties) { + // Create HttpClientOptions and apply the configuration + HttpClientOptions options = new HttpClientOptions(); + + options.setApplicationId(APPLICATION_ID); + + Map customHeaders = connectionProperties.getCustomHeaders(); + List
headers = customHeaders.entrySet() + .stream() + .map(entry -> new Header(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + + options.setHeaders(headers); + + if (connectionProperties.getConnectTimeout() != null) { + options.setConnectTimeout(connectionProperties.getConnectTimeout()); + } + + if (connectionProperties.getReadTimeout() != null) { + options.setReadTimeout(connectionProperties.getReadTimeout()); + } + + if (connectionProperties.getWriteTimeout() != null) { + options.setWriteTimeout(connectionProperties.getWriteTimeout()); + } + + if (connectionProperties.getResponseTimeout() != null) { + options.setResponseTimeout(connectionProperties.getResponseTimeout()); + } + + if (connectionProperties.getMaximumConnectionPoolSize() != null) { + options.setMaximumConnectionPoolSize(connectionProperties.getMaximumConnectionPoolSize()); + } + + return options; + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiConnectionProperties.java b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiConnectionProperties.java index a3c2191cc68..a33325ece33 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiConnectionProperties.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiConnectionProperties.java @@ -16,6 +16,7 @@ package org.springframework.ai.model.azure.openai.autoconfigure; +import java.time.Duration; import java.util.HashMap; import java.util.Map; @@ -46,6 +47,31 @@ public class AzureOpenAiConnectionProperties { private Map customHeaders = new HashMap<>(); + /** + * HTTP connection timeout + */ + private Duration connectTimeout; + + /** + * HTTP read timeout + */ + private Duration readTimeout; + + /** + * HTTP write timeout + */ + private Duration writeTimeout; + + /** + * HTTP response timeout + */ + private Duration responseTimeout; + + /** + * The maximum number of connections in the HTTP connection pool + */ + private Integer maximumConnectionPoolSize; + public String getEndpoint() { return this.endpoint; } @@ -78,4 +104,44 @@ public void setCustomHeaders(Map customHeaders) { this.customHeaders = customHeaders; } + public Duration getConnectTimeout() { + return this.connectTimeout; + } + + public void setConnectTimeout(Duration connectTimeout) { + this.connectTimeout = connectTimeout; + } + + public Duration getReadTimeout() { + return this.readTimeout; + } + + public void setReadTimeout(Duration readTimeout) { + this.readTimeout = readTimeout; + } + + public Duration getWriteTimeout() { + return this.writeTimeout; + } + + public void setWriteTimeout(Duration writeTimeout) { + this.writeTimeout = writeTimeout; + } + + public Duration getResponseTimeout() { + return this.responseTimeout; + } + + public void setResponseTimeout(Duration responseTimeout) { + this.responseTimeout = responseTimeout; + } + + public Integer getMaximumConnectionPoolSize() { + return this.maximumConnectionPoolSize; + } + + public void setMaximumConnectionPoolSize(Integer maximumConnectionPoolSize) { + this.maximumConnectionPoolSize = maximumConnectionPoolSize; + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationIT.java index b065b47d1fc..18cf0938b95 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationIT.java @@ -54,6 +54,7 @@ import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * @author Christian Tzolov @@ -288,4 +289,15 @@ void openAIClientBuilderCustomizer() { }); } + @Test + void connectTimeoutShouldTakeEffect() { + new ApplicationContextRunner().withPropertyValues("spring.ai.azure.openai.connect-timeout=1ms") + .withConfiguration(SpringAiTestAutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class)) + .run(context -> { + AzureOpenAiChatModel chatModel = context.getBean(AzureOpenAiChatModel.class); + + assertThatThrownBy(() -> chatModel.call(new Prompt("Hello"))).isInstanceOf(Exception.class); + }); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationPropertyTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationPropertyTests.java index 99a1b190187..983595c388b 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationPropertyTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationPropertyTests.java @@ -16,6 +16,8 @@ package org.springframework.ai.model.azure.openai.autoconfigure; +import java.time.Duration; + import org.junit.jupiter.api.Test; import org.springframework.ai.utils.SpringAiTestAutoConfigurations; @@ -66,7 +68,13 @@ public void chatPropertiesTest() { "spring.ai.azure.openai.chat.options.stop=boza,koza", "spring.ai.azure.openai.chat.options.temperature=0.55", "spring.ai.azure.openai.chat.options.topP=0.56", - "spring.ai.azure.openai.chat.options.user=userXYZ" + "spring.ai.azure.openai.chat.options.user=userXYZ", + + "spring.ai.azure.openai.connect-timeout=10s", + "spring.ai.azure.openai.read-timeout=30s", + "spring.ai.azure.openai.write-timeout=30s", + "spring.ai.azure.openai.response-timeout=60s", + "spring.ai.azure.openai.maximum-connection-pool-size=50" ) // @formatter:on .withConfiguration(SpringAiTestAutoConfigurations.of(AzureOpenAiChatAutoConfiguration.class, @@ -91,6 +99,12 @@ public void chatPropertiesTest() { assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55); assertThat(chatProperties.getOptions().getTopP()).isEqualTo(0.56); + assertThat(connectionProperties.getConnectTimeout()).isEqualTo(Duration.ofSeconds(10)); + assertThat(connectionProperties.getReadTimeout()).isEqualTo(Duration.ofSeconds(30)); + assertThat(connectionProperties.getWriteTimeout()).isEqualTo(Duration.ofSeconds(30)); + assertThat(connectionProperties.getResponseTimeout()).isEqualTo(Duration.ofSeconds(60)); + assertThat(connectionProperties.getMaximumConnectionPoolSize()).isEqualTo(50); + assertThat(chatProperties.getOptions().getUser()).isEqualTo("userXYZ"); }); } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-deepseek/src/main/java/org/springframework/ai/model/deepseek/autoconfigure/DeepSeekChatAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-deepseek/src/main/java/org/springframework/ai/model/deepseek/autoconfigure/DeepSeekChatAutoConfiguration.java index eb84d57ee65..3500e935796 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-deepseek/src/main/java/org/springframework/ai/model/deepseek/autoconfigure/DeepSeekChatAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-deepseek/src/main/java/org/springframework/ai/model/deepseek/autoconfigure/DeepSeekChatAutoConfiguration.java @@ -35,10 +35,17 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsPropertyMapper; +import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder; import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; +import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.webclient.autoconfigure.WebClientAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.core.retry.RetryTemplate; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.client.ResponseErrorHandler; @@ -67,10 +74,24 @@ public DeepSeekChatModel deepSeekChatModel(DeepSeekConnectionProperties commonPr RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, ObjectProvider observationConvention, - ObjectProvider deepseekToolExecutionEligibilityPredicate) { + ObjectProvider deepseekToolExecutionEligibilityPredicate, + ObjectProvider sslBundles, ObjectProvider globalHttpClientSettings, + ObjectProvider> factoryBuilder, + ObjectProvider> webConnectorBuilderProvider) { - var deepSeekApi = deepSeekApi(chatProperties, commonProperties, - restClientBuilderProvider.getIfAvailable(RestClient::builder), + HttpClientSettingsPropertyMapper mapper = new HttpClientSettingsPropertyMapper(sslBundles.getIfAvailable(), + globalHttpClientSettings.getIfAvailable()); + HttpClientSettings httpClientSettings = mapper.map(commonProperties.getHttp()); + + RestClient.Builder restClientBuilder = restClientBuilderProvider.getIfAvailable(RestClient::builder); + applyRestClientSettings(restClientBuilder, httpClientSettings, + factoryBuilder.getIfAvailable(ClientHttpRequestFactoryBuilder::detect)); + + WebClient.Builder webClientBuilder = webClientBuilderProvider.getIfAvailable(WebClient::builder); + applyWebClientSettings(webClientBuilder, httpClientSettings, + webConnectorBuilderProvider.getIfAvailable(ClientHttpConnectorBuilder::detect)); + + var deepSeekApi = deepSeekApi(chatProperties, commonProperties, restClientBuilder, webClientBuilderProvider.getIfAvailable(WebClient::builder), responseErrorHandler); var chatModel = DeepSeekChatModel.builder() @@ -111,4 +132,16 @@ private DeepSeekApi deepSeekApi(DeepSeekChatProperties chatProperties, .build(); } + private void applyRestClientSettings(RestClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpRequestFactoryBuilder factoryBuilder) { + ClientHttpRequestFactory requestFactory = factoryBuilder.build(httpClientSettings); + builder.requestFactory(requestFactory); + } + + private void applyWebClientSettings(WebClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpConnectorBuilder connectorBuilder) { + ClientHttpConnector connector = connectorBuilder.build(httpClientSettings); + builder.clientConnector(connector); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-deepseek/src/main/java/org/springframework/ai/model/deepseek/autoconfigure/DeepSeekConnectionProperties.java b/auto-configurations/models/spring-ai-autoconfigure-model-deepseek/src/main/java/org/springframework/ai/model/deepseek/autoconfigure/DeepSeekConnectionProperties.java index 25deeedf7a9..4d792d7e87c 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-deepseek/src/main/java/org/springframework/ai/model/deepseek/autoconfigure/DeepSeekConnectionProperties.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-deepseek/src/main/java/org/springframework/ai/model/deepseek/autoconfigure/DeepSeekConnectionProperties.java @@ -16,7 +16,14 @@ package org.springframework.ai.model.deepseek.autoconfigure; +import java.time.Duration; + +import jakarta.annotation.Nullable; + import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.boot.http.client.HttpRedirects; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsProperties; /** * Parent properties for DeepSeek. @@ -30,8 +37,47 @@ public class DeepSeekConnectionProperties extends DeepSeekParentProperties { public static final String DEFAULT_BASE_URL = "https://api.deepseek.com"; + @NestedConfigurationProperty + private final HttpClientSettingsProperties http = new HttpClientSettingsProperties() { + }; + public DeepSeekConnectionProperties() { super.setBaseUrl(DEFAULT_BASE_URL); } + @Nullable + public HttpRedirects getRedirects() { + return this.http.getRedirects(); + } + + public void setRedirects(HttpRedirects redirects) { + this.http.setRedirects(redirects); + } + + @Nullable + public Duration getConnectTimeout() { + return this.http.getConnectTimeout(); + } + + public void setConnectTimeout(Duration connectTimeout) { + this.http.setConnectTimeout(connectTimeout); + } + + @Nullable + public Duration getReadTimeout() { + return this.http.getReadTimeout(); + } + + public void setReadTimeout(Duration readTimeout) { + this.http.setReadTimeout(readTimeout); + } + + public HttpClientSettingsProperties.Ssl getSsl() { + return this.http.getSsl(); + } + + public HttpClientSettingsProperties getHttp() { + return this.http; + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-deepseek/src/test/java/org/springframework/ai/model/deepseek/autoconfigure/DeepSeekAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-deepseek/src/test/java/org/springframework/ai/model/deepseek/autoconfigure/DeepSeekAutoConfigurationIT.java index 94db5f43bd3..7aa00b8a38b 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-deepseek/src/test/java/org/springframework/ai/model/deepseek/autoconfigure/DeepSeekAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-deepseek/src/test/java/org/springframework/ai/model/deepseek/autoconfigure/DeepSeekAutoConfigurationIT.java @@ -16,6 +16,7 @@ package org.springframework.ai.model.deepseek.autoconfigure; +import java.time.Duration; import java.util.Objects; import java.util.stream.Collectors; @@ -73,4 +74,51 @@ void generateStreaming() { }); } + @Test + void generateWithCustomTimeout() { + new ApplicationContextRunner() + .withPropertyValues("spring.ai.deepseek.apiKey=" + System.getenv("DEEPSEEK_API_KEY"), + "spring.ai.deepseek.connect-timeout=5s", "spring.ai.deepseek.read-timeout=30s") + .withConfiguration(SpringAiTestAutoConfigurations.of(DeepSeekChatAutoConfiguration.class)) + .run(context -> { + DeepSeekChatModel client = context.getBean(DeepSeekChatModel.class); + + // Verify that the HTTP client configuration is applied + var connectionProperties = context.getBean(DeepSeekConnectionProperties.class); + assertThat(connectionProperties.getConnectTimeout()).isEqualTo(Duration.ofSeconds(5)); + assertThat(connectionProperties.getReadTimeout()).isEqualTo(Duration.ofSeconds(30)); + + // Verify that the client can actually make requests with the configured + // timeout + String response = client.call("Hello"); + assertThat(response).isNotEmpty(); + logger.info("Response with custom timeout: " + response); + }); + } + + @Test + void generateStreamingWithCustomTimeout() { + new ApplicationContextRunner() + .withPropertyValues("spring.ai.deepseek.apiKey=" + System.getenv("DEEPSEEK_API_KEY"), + "spring.ai.deepseek.connect-timeout=1s", "spring.ai.deepseek.read-timeout=1s") + .withConfiguration(SpringAiTestAutoConfigurations.of(DeepSeekChatAutoConfiguration.class)) + .run(context -> { + DeepSeekChatModel client = context.getBean(DeepSeekChatModel.class); + + // Verify that the HTTP client configuration is applied + var connectionProperties = context.getBean(DeepSeekConnectionProperties.class); + assertThat(connectionProperties.getConnectTimeout()).isEqualTo(Duration.ofSeconds(1)); + assertThat(connectionProperties.getReadTimeout()).isEqualTo(Duration.ofSeconds(1)); + + Flux responseFlux = client.stream(new Prompt(new UserMessage("Hello"))); + String response = Objects.requireNonNull(responseFlux.collectList().block()) + .stream() + .map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText()) + .collect(Collectors.joining()); + + assertThat(response).isNotEmpty(); + logger.info("Response with custom timeout: " + response); + }); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-deepseek/src/test/java/org/springframework/ai/model/deepseek/autoconfigure/DeepSeekPropertiesTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-deepseek/src/test/java/org/springframework/ai/model/deepseek/autoconfigure/DeepSeekPropertiesTests.java index e126ba67aba..1e9165813b6 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-deepseek/src/test/java/org/springframework/ai/model/deepseek/autoconfigure/DeepSeekPropertiesTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-deepseek/src/test/java/org/springframework/ai/model/deepseek/autoconfigure/DeepSeekPropertiesTests.java @@ -16,10 +16,13 @@ package org.springframework.ai.model.deepseek.autoconfigure; +import java.time.Duration; + import org.junit.jupiter.api.Test; import org.springframework.ai.deepseek.DeepSeekChatModel; import org.springframework.ai.utils.SpringAiTestAutoConfigurations; +import org.springframework.boot.http.client.HttpRedirects; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import static org.assertj.core.api.Assertions.assertThat; @@ -153,4 +156,37 @@ void chatActivation() { }); } + @Test + public void httpClientCustomTimeouts() { + new ApplicationContextRunner().withPropertyValues( + // @formatter:off + "spring.ai.deepseek.api-key=API_KEY", + "spring.ai.deepseek.base-url=TEST_BASE_URL", + "spring.ai.deepseek.connect-timeout=5s", + "spring.ai.deepseek.read-timeout=30s") + // @formatter:on + .withConfiguration(SpringAiTestAutoConfigurations.of(DeepSeekChatAutoConfiguration.class)) + .run(context -> { + var connectionProperties = context.getBean(DeepSeekConnectionProperties.class); + + assertThat(connectionProperties.getConnectTimeout()).isEqualTo(Duration.ofSeconds(5)); + assertThat(connectionProperties.getReadTimeout()).isEqualTo(Duration.ofSeconds(30)); + }); + } + + @Test + public void httpClientRedirects() { + new ApplicationContextRunner().withPropertyValues( + // @formatter:off + "spring.ai.deepseek.api-key=API_KEY", + "spring.ai.deepseek.base-url=TEST_BASE_URL", + "spring.ai.deepseek.redirects=DONT_FOLLOW") + // @formatter:on + .withConfiguration(SpringAiTestAutoConfigurations.of(DeepSeekChatAutoConfiguration.class)) + .run(context -> { + var connectionProperties = context.getBean(DeepSeekConnectionProperties.class); + assertThat(connectionProperties.getRedirects()).isEqualTo(HttpRedirects.DONT_FOLLOW); + }); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-elevenlabs/src/main/java/org/springframework/ai/model/elevenlabs/autoconfigure/ElevenLabsAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-elevenlabs/src/main/java/org/springframework/ai/model/elevenlabs/autoconfigure/ElevenLabsAutoConfiguration.java index ce1c94d857e..dc10357d7b6 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-elevenlabs/src/main/java/org/springframework/ai/model/elevenlabs/autoconfigure/ElevenLabsAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-elevenlabs/src/main/java/org/springframework/ai/model/elevenlabs/autoconfigure/ElevenLabsAutoConfiguration.java @@ -27,10 +27,17 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsPropertyMapper; +import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder; import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; +import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.webclient.autoconfigure.WebClientAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.core.retry.RetryTemplate; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; import org.springframework.web.reactive.function.client.WebClient; @@ -52,13 +59,28 @@ public class ElevenLabsAutoConfiguration { @ConditionalOnMissingBean public ElevenLabsApi elevenLabsApi(ElevenLabsConnectionProperties connectionProperties, ObjectProvider restClientBuilderProvider, - ObjectProvider webClientBuilderProvider, ResponseErrorHandler responseErrorHandler) { + ObjectProvider webClientBuilderProvider, ResponseErrorHandler responseErrorHandler, + ObjectProvider sslBundles, ObjectProvider globalHttpClientSettings, + ObjectProvider> factoryBuilder, + ObjectProvider> webConnectorBuilderProvider) { + + HttpClientSettingsPropertyMapper mapper = new HttpClientSettingsPropertyMapper(sslBundles.getIfAvailable(), + globalHttpClientSettings.getIfAvailable()); + HttpClientSettings httpClientSettings = mapper.map(connectionProperties.getHttp()); + + RestClient.Builder restClientBuilder = restClientBuilderProvider.getIfAvailable(RestClient::builder); + applyRestClientSettings(restClientBuilder, httpClientSettings, + factoryBuilder.getIfAvailable(ClientHttpRequestFactoryBuilder::detect)); + + WebClient.Builder webClientBuilder = webClientBuilderProvider.getIfAvailable(WebClient::builder); + applyWebClientSettings(webClientBuilder, httpClientSettings, + webConnectorBuilderProvider.getIfAvailable(ClientHttpConnectorBuilder::detect)); return ElevenLabsApi.builder() .baseUrl(connectionProperties.getBaseUrl()) .apiKey(connectionProperties.getApiKey()) - .restClientBuilder(restClientBuilderProvider.getIfAvailable(RestClient::builder)) - .webClientBuilder(webClientBuilderProvider.getIfAvailable(WebClient::builder)) + .restClientBuilder(restClientBuilder) + .webClientBuilder(webClientBuilder) .responseErrorHandler(responseErrorHandler) .build(); } @@ -75,4 +97,16 @@ public ElevenLabsTextToSpeechModel elevenLabsSpeechModel(ElevenLabsApi elevenLab .build(); } + private void applyRestClientSettings(RestClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpRequestFactoryBuilder factoryBuilder) { + ClientHttpRequestFactory requestFactory = factoryBuilder.build(httpClientSettings); + builder.requestFactory(requestFactory); + } + + private void applyWebClientSettings(WebClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpConnectorBuilder connectorBuilder) { + ClientHttpConnector connector = connectorBuilder.build(httpClientSettings); + builder.clientConnector(connector); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-elevenlabs/src/main/java/org/springframework/ai/model/elevenlabs/autoconfigure/ElevenLabsConnectionProperties.java b/auto-configurations/models/spring-ai-autoconfigure-model-elevenlabs/src/main/java/org/springframework/ai/model/elevenlabs/autoconfigure/ElevenLabsConnectionProperties.java index 4f2b299142e..773f03bba45 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-elevenlabs/src/main/java/org/springframework/ai/model/elevenlabs/autoconfigure/ElevenLabsConnectionProperties.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-elevenlabs/src/main/java/org/springframework/ai/model/elevenlabs/autoconfigure/ElevenLabsConnectionProperties.java @@ -16,8 +16,15 @@ package org.springframework.ai.model.elevenlabs.autoconfigure; +import java.time.Duration; + +import jakarta.annotation.Nullable; + import org.springframework.ai.elevenlabs.api.ElevenLabsApi; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.boot.http.client.HttpRedirects; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsProperties; /** * Configuration properties for the ElevenLabs API connection. @@ -39,6 +46,10 @@ public class ElevenLabsConnectionProperties { */ private String baseUrl = ElevenLabsApi.DEFAULT_BASE_URL; + @NestedConfigurationProperty + private final HttpClientSettingsProperties http = new HttpClientSettingsProperties() { + }; + public String getApiKey() { return this.apiKey; } @@ -55,4 +66,39 @@ public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; } + @Nullable + public HttpRedirects getRedirects() { + return this.http.getRedirects(); + } + + public void setRedirects(HttpRedirects redirects) { + this.http.setRedirects(redirects); + } + + @Nullable + public Duration getConnectTimeout() { + return this.http.getConnectTimeout(); + } + + public void setConnectTimeout(Duration connectTimeout) { + this.http.setConnectTimeout(connectTimeout); + } + + @Nullable + public Duration getReadTimeout() { + return this.http.getReadTimeout(); + } + + public void setReadTimeout(Duration readTimeout) { + this.http.setReadTimeout(readTimeout); + } + + public HttpClientSettingsProperties.Ssl getSsl() { + return this.http.getSsl(); + } + + public HttpClientSettingsProperties getHttp() { + return this.http; + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-elevenlabs/src/test/java/org/springframework/ai/model/elevenlabs/autoconfigure/ElevenLabsAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-elevenlabs/src/test/java/org/springframework/ai/model/elevenlabs/autoconfigure/ElevenLabsAutoConfigurationIT.java index 3ed45ca9867..1fe814cf059 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-elevenlabs/src/test/java/org/springframework/ai/model/elevenlabs/autoconfigure/ElevenLabsAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-elevenlabs/src/test/java/org/springframework/ai/model/elevenlabs/autoconfigure/ElevenLabsAutoConfigurationIT.java @@ -16,6 +16,7 @@ package org.springframework.ai.model.elevenlabs.autoconfigure; +import java.time.Duration; import java.util.Arrays; import org.junit.jupiter.api.Test; @@ -73,6 +74,23 @@ void speechStream() { }); } + @Test + void speechWithCustomTimeout() { + this.contextRunner.withConfiguration(SpringAiTestAutoConfigurations.of(ElevenLabsAutoConfiguration.class)) + .withPropertyValues("spring.ai.elevenlabs.connect-timeout=1ms", "spring.ai.elevenlabs.read-timeout=1ms") + .run(context -> { + ElevenLabsTextToSpeechModel speechModel = context.getBean(ElevenLabsTextToSpeechModel.class); + var connectionProperties = context.getBean(ElevenLabsConnectionProperties.class); + assertThat(connectionProperties.getConnectTimeout()).isEqualTo(Duration.ofMillis(1)); + assertThat(connectionProperties.getReadTimeout()).isEqualTo(Duration.ofMillis(1)); + + byte[] response = speechModel.call("H"); + assertThat(response).isNotNull(); + + logger.info("Response with custom timeout: " + Arrays.toString(response)); + }); + } + public boolean verifyMp3FrameHeader(byte[] audioResponse) { if (audioResponse == null || audioResponse.length < 3) { return false; diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-elevenlabs/src/test/java/org/springframework/ai/model/elevenlabs/autoconfigure/ElevenLabsPropertiesTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-elevenlabs/src/test/java/org/springframework/ai/model/elevenlabs/autoconfigure/ElevenLabsPropertiesTests.java index 7061260b097..e13bc026480 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-elevenlabs/src/test/java/org/springframework/ai/model/elevenlabs/autoconfigure/ElevenLabsPropertiesTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-elevenlabs/src/test/java/org/springframework/ai/model/elevenlabs/autoconfigure/ElevenLabsPropertiesTests.java @@ -16,6 +16,8 @@ package org.springframework.ai.model.elevenlabs.autoconfigure; +import java.time.Duration; + import org.junit.jupiter.api.Test; import org.springframework.ai.elevenlabs.ElevenLabsTextToSpeechModel; @@ -46,7 +48,10 @@ public void connectionProperties() { "spring.ai.elevenlabs.tts.options.voice-settings.similarity-boost=0.8", "spring.ai.elevenlabs.tts.options.voice-settings.style=0.2", "spring.ai.elevenlabs.tts.options.voice-settings.use-speaker-boost=false", - "spring.ai.elevenlabs.tts.options.voice-settings.speed=1.5" + "spring.ai.elevenlabs.tts.options.voice-settings.speed=1.5", + + "spring.ai.elevenlabs.connect-timeout=1s", + "spring.ai.elevenlabs.read-timeout=1s" // @formatter:on ).withConfiguration(SpringAiTestAutoConfigurations.of(ElevenLabsAutoConfiguration.class)).run(context -> { var speechProperties = context.getBean(ElevenLabsSpeechProperties.class); @@ -62,6 +67,9 @@ public void connectionProperties() { assertThat(speechProperties.getOptions().getVoiceSettings().style()).isEqualTo(0.2); assertThat(speechProperties.getOptions().getVoiceSettings().useSpeakerBoost()).isFalse(); assertThat(speechProperties.getOptions().getSpeed()).isEqualTo(1.5f); + + assertThat(connectionProperties.getConnectTimeout()).isEqualTo(Duration.ofSeconds(1)); + assertThat(connectionProperties.getReadTimeout()).isEqualTo(Duration.ofSeconds(1)); }); } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/main/java/org/springframework/ai/model/google/genai/autoconfigure/chat/GoogleGenAiChatAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/main/java/org/springframework/ai/model/google/genai/autoconfigure/chat/GoogleGenAiChatAutoConfiguration.java index 664b1e3b9be..5f42d45d142 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/main/java/org/springframework/ai/model/google/genai/autoconfigure/chat/GoogleGenAiChatAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/main/java/org/springframework/ai/model/google/genai/autoconfigure/chat/GoogleGenAiChatAutoConfiguration.java @@ -17,9 +17,14 @@ package org.springframework.ai.model.google.genai.autoconfigure.chat; import java.io.IOException; +import java.time.Duration; +import java.util.Optional; import com.google.auth.oauth2.GoogleCredentials; +import com.google.common.collect.ImmutableMap; import com.google.genai.Client; +import com.google.genai.types.ClientOptions; +import com.google.genai.types.HttpOptions; import io.micrometer.observation.ObservationRegistry; import org.springframework.ai.chat.observation.ChatModelObservationConvention; @@ -88,6 +93,28 @@ public Client googleGenAiClient(GoogleGenAiConnectionProperties connectionProper // credentials are handled automatically when vertexAI is true } } + HttpOptions.Builder httpOptionsBuilder = HttpOptions.builder(); + if (connectionProperties.getTimeout() != null) { + Integer timeout = Optional.ofNullable(connectionProperties.getTimeout()) + .map(Duration::toMillisPart) + .orElse(null); + httpOptionsBuilder.timeout(timeout); + } + if (!connectionProperties.getCustomHeaders().isEmpty()) { + httpOptionsBuilder.headers(ImmutableMap.copyOf(connectionProperties.getCustomHeaders())); + } + + ClientOptions.Builder clientOptionsBuilder = ClientOptions.builder(); + if (connectionProperties.getMaxConnections() != null) { + clientOptionsBuilder.maxConnections(connectionProperties.getMaxConnections()); + } + + if (connectionProperties.getMaxConnectionsPerHost() != null) { + clientOptionsBuilder.maxConnectionsPerHost(connectionProperties.getMaxConnectionsPerHost()); + } + + clientBuilder.httpOptions(httpOptionsBuilder.build()); + clientBuilder.clientOptions(clientOptionsBuilder.build()); return clientBuilder.build(); } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/main/java/org/springframework/ai/model/google/genai/autoconfigure/chat/GoogleGenAiConnectionProperties.java b/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/main/java/org/springframework/ai/model/google/genai/autoconfigure/chat/GoogleGenAiConnectionProperties.java index 24f8dce693d..f741834b0e7 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/main/java/org/springframework/ai/model/google/genai/autoconfigure/chat/GoogleGenAiConnectionProperties.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/main/java/org/springframework/ai/model/google/genai/autoconfigure/chat/GoogleGenAiConnectionProperties.java @@ -16,6 +16,10 @@ package org.springframework.ai.model.google.genai.autoconfigure.chat; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.core.io.Resource; @@ -56,6 +60,23 @@ public class GoogleGenAiConnectionProperties { */ private boolean vertexAi; + /** + * Timeout for the request in milliseconds. + */ + private Duration timeout; + + /** + * The maximum number of connections allowed in the pool. + */ + private Integer maxConnections; + + /** + * The maximum number of connections allowed per host. + */ + private Integer maxConnectionsPerHost; + + private Map customHeaders = new HashMap<>(); + public String getApiKey() { return this.apiKey; } @@ -96,4 +117,36 @@ public void setVertexAi(boolean vertexAi) { this.vertexAi = vertexAi; } + public Duration getTimeout() { + return this.timeout; + } + + public void setTimeout(Duration timeout) { + this.timeout = timeout; + } + + public Integer getMaxConnections() { + return this.maxConnections; + } + + public void setMaxConnections(Integer maxConnections) { + this.maxConnections = maxConnections; + } + + public Integer getMaxConnectionsPerHost() { + return this.maxConnectionsPerHost; + } + + public void setMaxConnectionsPerHost(Integer maxConnectionsPerHost) { + this.maxConnectionsPerHost = maxConnectionsPerHost; + } + + public Map getCustomHeaders() { + return this.customHeaders; + } + + public void setCustomHeaders(Map customHeaders) { + this.customHeaders = customHeaders; + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/main/java/org/springframework/ai/model/google/genai/autoconfigure/embedding/GoogleGenAiEmbeddingConnectionAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/main/java/org/springframework/ai/model/google/genai/autoconfigure/embedding/GoogleGenAiEmbeddingConnectionAutoConfiguration.java index 7c42938d803..0a07da2c911 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/main/java/org/springframework/ai/model/google/genai/autoconfigure/embedding/GoogleGenAiEmbeddingConnectionAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/main/java/org/springframework/ai/model/google/genai/autoconfigure/embedding/GoogleGenAiEmbeddingConnectionAutoConfiguration.java @@ -19,7 +19,10 @@ import java.io.IOException; import com.google.auth.oauth2.GoogleCredentials; +import com.google.common.collect.ImmutableMap; import com.google.genai.Client; +import com.google.genai.types.ClientOptions; +import com.google.genai.types.HttpOptions; import org.springframework.ai.google.genai.GoogleGenAiEmbeddingConnectionDetails; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -49,28 +52,55 @@ public GoogleGenAiEmbeddingConnectionDetails googleGenAiEmbeddingConnectionDetai GoogleGenAiEmbeddingConnectionProperties connectionProperties) throws IOException { var connectionBuilder = GoogleGenAiEmbeddingConnectionDetails.builder(); + Client client = buildGenAiClient(connectionProperties); + connectionBuilder.genAiClient(client); + return connectionBuilder.build(); + } + + private Client buildGenAiClient(GoogleGenAiEmbeddingConnectionProperties connectionProperties) throws IOException { + Client.Builder clientBuilder = Client.builder(); if (StringUtils.hasText(connectionProperties.getApiKey())) { // Gemini Developer API mode - connectionBuilder.apiKey(connectionProperties.getApiKey()); + clientBuilder.apiKey(connectionProperties.getApiKey()); } else { // Vertex AI mode Assert.hasText(connectionProperties.getProjectId(), "Google GenAI project-id must be set!"); Assert.hasText(connectionProperties.getLocation(), "Google GenAI location must be set!"); - connectionBuilder.projectId(connectionProperties.getProjectId()) - .location(connectionProperties.getLocation()); + clientBuilder.project(connectionProperties.getProjectId()) + .location(connectionProperties.getLocation()) + .vertexAI(true); if (connectionProperties.getCredentialsUri() != null) { GoogleCredentials credentials = GoogleCredentials .fromStream(connectionProperties.getCredentialsUri().getInputStream()); - // Note: Credentials are handled automatically by the SDK when using - // Vertex AI mode + // Note: The new SDK doesn't have a direct setCredentials method, + // credentials are handled automatically when vertexAI is true } } + HttpOptions.Builder httpOptionsBuilder = HttpOptions.builder(); + if (connectionProperties.getTimeout() != null) { + httpOptionsBuilder.timeout((int) connectionProperties.getTimeout().getSeconds()); + } + if (!connectionProperties.getCustomHeaders().isEmpty()) { + httpOptionsBuilder.headers(ImmutableMap.copyOf(connectionProperties.getCustomHeaders())); + } - return connectionBuilder.build(); + ClientOptions.Builder clientOptionsBuilder = ClientOptions.builder(); + if (connectionProperties.getMaxConnections() != null) { + clientOptionsBuilder.maxConnections(connectionProperties.getMaxConnections()); + } + + if (connectionProperties.getMaxConnectionsPerHost() != null) { + clientOptionsBuilder.maxConnectionsPerHost(connectionProperties.getMaxConnectionsPerHost()); + } + + clientBuilder.httpOptions(httpOptionsBuilder.build()); + clientBuilder.clientOptions(clientOptionsBuilder.build()); + + return clientBuilder.build(); } } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/main/java/org/springframework/ai/model/google/genai/autoconfigure/embedding/GoogleGenAiEmbeddingConnectionProperties.java b/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/main/java/org/springframework/ai/model/google/genai/autoconfigure/embedding/GoogleGenAiEmbeddingConnectionProperties.java index 818cd6ef4fc..d1eccbd0f79 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/main/java/org/springframework/ai/model/google/genai/autoconfigure/embedding/GoogleGenAiEmbeddingConnectionProperties.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/main/java/org/springframework/ai/model/google/genai/autoconfigure/embedding/GoogleGenAiEmbeddingConnectionProperties.java @@ -16,6 +16,10 @@ package org.springframework.ai.model.google.genai.autoconfigure.embedding; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.core.io.Resource; @@ -52,6 +56,23 @@ public class GoogleGenAiEmbeddingConnectionProperties { */ private Resource credentialsUri; + /** + * Timeout for the request in milliseconds. + */ + private Duration timeout; + + /** + * The maximum number of connections allowed in the pool. + */ + private Integer maxConnections; + + /** + * The maximum number of connections allowed per host. + */ + private Integer maxConnectionsPerHost; + + private Map customHeaders = new HashMap<>(); + /** * Whether to use Vertex AI mode. If false, uses Gemini Developer API mode. This is * automatically determined based on whether apiKey or projectId is set. @@ -98,4 +119,36 @@ public void setVertexAi(boolean vertexAi) { this.vertexAi = vertexAi; } + public Duration getTimeout() { + return this.timeout; + } + + public void setTimeout(Duration timeout) { + this.timeout = timeout; + } + + public Integer getMaxConnections() { + return this.maxConnections; + } + + public void setMaxConnections(Integer maxConnections) { + this.maxConnections = maxConnections; + } + + public Integer getMaxConnectionsPerHost() { + return this.maxConnectionsPerHost; + } + + public void setMaxConnectionsPerHost(Integer maxConnectionsPerHost) { + this.maxConnectionsPerHost = maxConnectionsPerHost; + } + + public Map getCustomHeaders() { + return this.customHeaders; + } + + public void setCustomHeaders(Map customHeaders) { + this.customHeaders = customHeaders; + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/test/java/org/springframework/ai/model/google/genai/autoconfigure/chat/GoogleGenAiChatAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/test/java/org/springframework/ai/model/google/genai/autoconfigure/chat/GoogleGenAiChatAutoConfigurationIT.java index f4a42651661..476b71f0fa1 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/test/java/org/springframework/ai/model/google/genai/autoconfigure/chat/GoogleGenAiChatAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/test/java/org/springframework/ai/model/google/genai/autoconfigure/chat/GoogleGenAiChatAutoConfigurationIT.java @@ -120,4 +120,20 @@ void generateStreamingWithVertexAi() { }); } + @Test + @EnabledIfEnvironmentVariable(named = "GOOGLE_API_KEY", matches = ".*") + void generateWithTimeout() { + ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("spring.ai.google.genai.api-key=" + System.getenv("GOOGLE_API_KEY"), + "spring.ai.google.genai.timeout=1s") + .withConfiguration(SpringAiTestAutoConfigurations.of(GoogleGenAiChatAutoConfiguration.class)); + + contextRunner.run(context -> { + GoogleGenAiChatModel chatModel = context.getBean(GoogleGenAiChatModel.class); + String response = chatModel.call("Hello"); + assertThat(response).isNotEmpty(); + logger.info("Response: " + response); + }); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/test/java/org/springframework/ai/model/google/genai/autoconfigure/chat/GoogleGenAiPropertiesTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/test/java/org/springframework/ai/model/google/genai/autoconfigure/chat/GoogleGenAiPropertiesTests.java index 4dc798bd466..8fcd7aacbb6 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/test/java/org/springframework/ai/model/google/genai/autoconfigure/chat/GoogleGenAiPropertiesTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/test/java/org/springframework/ai/model/google/genai/autoconfigure/chat/GoogleGenAiPropertiesTests.java @@ -16,6 +16,8 @@ package org.springframework.ai.model.google.genai.autoconfigure.chat; +import java.time.Duration; + import org.junit.jupiter.api.Test; import org.springframework.ai.model.google.genai.autoconfigure.embedding.GoogleGenAiEmbeddingConnectionProperties; @@ -37,13 +39,15 @@ public class GoogleGenAiPropertiesTests { void connectionPropertiesBinding() { this.contextRunner .withPropertyValues("spring.ai.google.genai.api-key=test-key", - "spring.ai.google.genai.project-id=test-project", "spring.ai.google.genai.location=us-central1") + "spring.ai.google.genai.project-id=test-project", "spring.ai.google.genai.location=us-central1", + "spring.ai.google.genai.timeout=1ms") .run(context -> { GoogleGenAiConnectionProperties connectionProperties = context .getBean(GoogleGenAiConnectionProperties.class); assertThat(connectionProperties.getApiKey()).isEqualTo("test-key"); assertThat(connectionProperties.getProjectId()).isEqualTo("test-project"); assertThat(connectionProperties.getLocation()).isEqualTo("us-central1"); + assertThat(connectionProperties.getTimeout()).isEqualTo(Duration.ofMillis(1)); }); } @@ -131,6 +135,16 @@ void extendedUsageMetadataDefaultBinding() { }); } + @Test + void extendedUsageCustomTimeoutPropertiesBinding() { + this.contextRunner + .withPropertyValues("spring.ai.google.genai.chat.options.include-extended-usage-metadata=true") + .run(context -> { + GoogleGenAiChatProperties chatProperties = context.getBean(GoogleGenAiChatProperties.class); + assertThat(chatProperties.getOptions().getIncludeExtendedUsageMetadata()).isTrue(); + }); + } + @Configuration @EnableConfigurationProperties({ GoogleGenAiConnectionProperties.class, GoogleGenAiChatProperties.class, GoogleGenAiEmbeddingConnectionProperties.class }) diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxChatAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxChatAutoConfiguration.java index 7cebce533fd..cdb252a9f61 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxChatAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxChatAutoConfiguration.java @@ -34,9 +34,14 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsPropertyMapper; import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; +import org.springframework.boot.ssl.SslBundles; import org.springframework.context.annotation.Bean; import org.springframework.core.retry.RetryTemplate; +import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.client.ResponseErrorHandler; @@ -48,6 +53,7 @@ * @author Geng Rong * @author Ilayaperumal Gopinathan * @author Issam El-atif + * @author yinh */ @AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class, ToolCallingAutoConfiguration.class }) @@ -64,11 +70,20 @@ public MiniMaxChatModel miniMaxChatModel(MiniMaxConnectionProperties commonPrope ToolCallingManager toolCallingManager, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, ObjectProvider observationConvention, - ObjectProvider openAiToolExecutionEligibilityPredicate) { + ObjectProvider openAiToolExecutionEligibilityPredicate, + ObjectProvider sslBundles, ObjectProvider globalHttpClientSettings, + ObjectProvider> factoryBuilder) { + + HttpClientSettingsPropertyMapper mapper = new HttpClientSettingsPropertyMapper(sslBundles.getIfAvailable(), + globalHttpClientSettings.getIfAvailable()); + HttpClientSettings httpClientSettings = mapper.map(commonProperties.getHttp()); + + RestClient.Builder restClientBuilder = restClientBuilderProvider.getIfAvailable(RestClient::builder); + applyRestClientSettings(restClientBuilder, httpClientSettings, + factoryBuilder.getIfAvailable(ClientHttpRequestFactoryBuilder::detect)); var miniMaxApi = miniMaxApi(chatProperties.getBaseUrl(), commonProperties.getBaseUrl(), - chatProperties.getApiKey(), commonProperties.getApiKey(), - restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler); + chatProperties.getApiKey(), commonProperties.getApiKey(), restClientBuilder, responseErrorHandler); var chatModel = new MiniMaxChatModel(miniMaxApi, chatProperties.getOptions(), toolCallingManager, retryTemplate, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP), @@ -90,4 +105,10 @@ private MiniMaxApi miniMaxApi(String baseUrl, String commonBaseUrl, String apiKe return new MiniMaxApi(resolvedBaseUrl, resolvedApiKey, restClientBuilder, responseErrorHandler); } + private void applyRestClientSettings(RestClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpRequestFactoryBuilder factoryBuilder) { + ClientHttpRequestFactory requestFactory = factoryBuilder.build(httpClientSettings); + builder.requestFactory(requestFactory); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxConnectionProperties.java b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxConnectionProperties.java index e41162a39d8..1f476c602a2 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxConnectionProperties.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxConnectionProperties.java @@ -16,7 +16,14 @@ package org.springframework.ai.model.minimax.autoconfigure; +import java.time.Duration; + +import jakarta.annotation.Nullable; + import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.boot.http.client.HttpRedirects; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsProperties; @ConfigurationProperties(MiniMaxConnectionProperties.CONFIG_PREFIX) public class MiniMaxConnectionProperties extends MiniMaxParentProperties { @@ -25,8 +32,47 @@ public class MiniMaxConnectionProperties extends MiniMaxParentProperties { public static final String DEFAULT_BASE_URL = "https://api.minimax.chat"; + @NestedConfigurationProperty + private final HttpClientSettingsProperties http = new HttpClientSettingsProperties() { + }; + public MiniMaxConnectionProperties() { super.setBaseUrl(DEFAULT_BASE_URL); } + @Nullable + public HttpRedirects getRedirects() { + return this.http.getRedirects(); + } + + public void setRedirects(HttpRedirects redirects) { + this.http.setRedirects(redirects); + } + + @Nullable + public Duration getConnectTimeout() { + return this.http.getConnectTimeout(); + } + + public void setConnectTimeout(Duration connectTimeout) { + this.http.setConnectTimeout(connectTimeout); + } + + @Nullable + public Duration getReadTimeout() { + return this.http.getReadTimeout(); + } + + public void setReadTimeout(Duration readTimeout) { + this.http.setReadTimeout(readTimeout); + } + + public HttpClientSettingsProperties.Ssl getSsl() { + return this.http.getSsl(); + } + + public HttpClientSettingsProperties getHttp() { + return this.http; + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxEmbeddingAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxEmbeddingAutoConfiguration.java index 36e3a82cb00..0cc41653ad0 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxEmbeddingAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxEmbeddingAutoConfiguration.java @@ -30,9 +30,14 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsPropertyMapper; import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; +import org.springframework.boot.ssl.SslBundles; import org.springframework.context.annotation.Bean; import org.springframework.core.retry.RetryTemplate; +import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.client.ResponseErrorHandler; @@ -57,7 +62,17 @@ public MiniMaxEmbeddingModel miniMaxEmbeddingModel(MiniMaxConnectionProperties c MiniMaxEmbeddingProperties embeddingProperties, ObjectProvider restClientBuilderProvider, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, - ObjectProvider observationConvention) { + ObjectProvider observationConvention, + ObjectProvider sslBundles, ObjectProvider globalHttpClientSettings, + ObjectProvider> factoryBuilder) { + + HttpClientSettingsPropertyMapper mapper = new HttpClientSettingsPropertyMapper(sslBundles.getIfAvailable(), + globalHttpClientSettings.getIfAvailable()); + HttpClientSettings httpClientSettings = mapper.map(commonProperties.getHttp()); + + RestClient.Builder restClientBuilder = restClientBuilderProvider.getIfAvailable(RestClient::builder); + applyRestClientSettings(restClientBuilder, httpClientSettings, + factoryBuilder.getIfAvailable(ClientHttpRequestFactoryBuilder::detect)); var miniMaxApi = miniMaxApi(embeddingProperties.getBaseUrl(), commonProperties.getBaseUrl(), embeddingProperties.getApiKey(), commonProperties.getApiKey(), @@ -84,4 +99,10 @@ private MiniMaxApi miniMaxApi(String baseUrl, String commonBaseUrl, String apiKe return new MiniMaxApi(resolvedBaseUrl, resolvedApiKey, restClientBuilder, responseErrorHandler); } + private void applyRestClientSettings(RestClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpRequestFactoryBuilder factoryBuilder) { + ClientHttpRequestFactory requestFactory = factoryBuilder.build(httpClientSettings); + builder.requestFactory(requestFactory); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfigurationIT.java index 286b68e10c1..9e01e89ae28 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfigurationIT.java @@ -94,4 +94,18 @@ void embedding() { }); } + @Test + void generateWithCustomTimeout() { + this.contextRunner.withConfiguration(SpringAiTestAutoConfigurations.of(MiniMaxChatAutoConfiguration.class)) + .withPropertyValues("spring.ai.minimax.connect-timeout=1s", "spring.ai.minimax.read-timeout=1s") + .run(context -> { + MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class); + + String response = chatModel.call("Hello"); + assertThat(response).isNotNull(); + + logger.info("Response with custom timeout: " + response); + }); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxPropertiesTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxPropertiesTests.java index d5b20f4b365..07d9f962de7 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxPropertiesTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxPropertiesTests.java @@ -16,6 +16,8 @@ package org.springframework.ai.model.minimax.autoconfigure; +import java.time.Duration; + import org.junit.jupiter.api.Test; import org.skyscreamer.jsonassert.JSONAssert; import org.skyscreamer.jsonassert.JSONCompareMode; @@ -74,7 +76,9 @@ public void chatOverrideConnectionProperties() { "spring.ai.minimax.chat.base-url=TEST_BASE_URL2", "spring.ai.minimax.chat.api-key=456", "spring.ai.minimax.chat.options.model=MODEL_XYZ", - "spring.ai.minimax.chat.options.temperature=0.55") + "spring.ai.minimax.chat.options.temperature=0.55", + "spring.ai.minimax.connect-timeout=1s", + "spring.ai.minimax.read-timeout=1s") // @formatter:on .withConfiguration(SpringAiTestAutoConfigurations.of(MiniMaxChatAutoConfiguration.class)) .run(context -> { @@ -89,6 +93,9 @@ public void chatOverrideConnectionProperties() { assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55); + + assertThat(connectionProperties.getConnectTimeout()).isEqualTo(Duration.ofSeconds(1)); + assertThat(connectionProperties.getReadTimeout()).isEqualTo(Duration.ofSeconds(1)); }); } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiChatAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiChatAutoConfiguration.java index 8ae982b503f..1fab2a885b8 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiChatAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiChatAutoConfiguration.java @@ -34,10 +34,17 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsPropertyMapper; +import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder; import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; +import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.webclient.autoconfigure.WebClientAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.core.retry.RetryTemplate; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.client.ResponseErrorHandler; @@ -51,6 +58,7 @@ * @author Christian Tzolov * @author Thomas Vitale * @author Ilayaperumal Gopinathan + * @author yinh * @since 0.8.1 */ @AutoConfiguration(after = { RestClientAutoConfiguration.class, WebClientAutoConfiguration.class, @@ -69,12 +77,26 @@ public MistralAiChatModel mistralAiChatModel(MistralAiCommonProperties commonPro RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, ObjectProvider observationConvention, - ObjectProvider mistralAiToolExecutionEligibilityPredicate) { + ObjectProvider mistralAiToolExecutionEligibilityPredicate, + ObjectProvider sslBundles, ObjectProvider globalHttpClientSettings, + ObjectProvider> factoryBuilder, + ObjectProvider> webConnectorBuilderProvider) { + + HttpClientSettingsPropertyMapper mapper = new HttpClientSettingsPropertyMapper(sslBundles.getIfAvailable(), + globalHttpClientSettings.getIfAvailable()); + HttpClientSettings httpClientSettings = mapper.map(commonProperties.getHttp()); + + RestClient.Builder restClientBuilder = restClientBuilderProvider.getIfAvailable(RestClient::builder); + applyRestClientSettings(restClientBuilder, httpClientSettings, + factoryBuilder.getIfAvailable(ClientHttpRequestFactoryBuilder::detect)); + + WebClient.Builder webClientBuilder = webClientBuilderProvider.getIfAvailable(WebClient::builder); + applyWebClientSettings(webClientBuilder, httpClientSettings, + webConnectorBuilderProvider.getIfAvailable(ClientHttpConnectorBuilder::detect)); var mistralAiApi = mistralAiApi(chatProperties.getApiKey(), commonProperties.getApiKey(), - chatProperties.getBaseUrl(), commonProperties.getBaseUrl(), - restClientBuilderProvider.getIfAvailable(RestClient::builder), - webClientBuilderProvider.getIfAvailable(WebClient::builder), responseErrorHandler); + chatProperties.getBaseUrl(), commonProperties.getBaseUrl(), restClientBuilder, webClientBuilder, + responseErrorHandler); var chatModel = MistralAiChatModel.builder() .mistralAiApi(mistralAiApi) @@ -110,4 +132,16 @@ private MistralAiApi mistralAiApi(String apiKey, String commonApiKey, String bas .build(); } + private void applyRestClientSettings(RestClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpRequestFactoryBuilder factoryBuilder) { + ClientHttpRequestFactory requestFactory = factoryBuilder.build(httpClientSettings); + builder.requestFactory(requestFactory); + } + + private void applyWebClientSettings(WebClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpConnectorBuilder connectorBuilder) { + ClientHttpConnector connector = connectorBuilder.build(httpClientSettings); + builder.clientConnector(connector); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiCommonProperties.java b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiCommonProperties.java index 3d1a90bfc49..3563435f786 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiCommonProperties.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiCommonProperties.java @@ -16,7 +16,14 @@ package org.springframework.ai.model.mistralai.autoconfigure; +import java.time.Duration; + +import jakarta.annotation.Nullable; + import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.boot.http.client.HttpRedirects; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsProperties; /** * Common properties for Mistral AI. @@ -32,8 +39,47 @@ public class MistralAiCommonProperties extends MistralAiParentProperties { public static final String DEFAULT_BASE_URL = "https://api.mistral.ai"; + @NestedConfigurationProperty + private final HttpClientSettingsProperties http = new HttpClientSettingsProperties() { + }; + public MistralAiCommonProperties() { super.setBaseUrl(DEFAULT_BASE_URL); } + @Nullable + public HttpRedirects getRedirects() { + return this.http.getRedirects(); + } + + public void setRedirects(HttpRedirects redirects) { + this.http.setRedirects(redirects); + } + + @Nullable + public Duration getConnectTimeout() { + return this.http.getConnectTimeout(); + } + + public void setConnectTimeout(Duration connectTimeout) { + this.http.setConnectTimeout(connectTimeout); + } + + @Nullable + public Duration getReadTimeout() { + return this.http.getReadTimeout(); + } + + public void setReadTimeout(Duration readTimeout) { + this.http.setReadTimeout(readTimeout); + } + + public HttpClientSettingsProperties.Ssl getSsl() { + return this.http.getSsl(); + } + + public HttpClientSettingsProperties getHttp() { + return this.http; + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiEmbeddingAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiEmbeddingAutoConfiguration.java index efa30bedd9d..19144bade0b 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiEmbeddingAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiEmbeddingAutoConfiguration.java @@ -30,9 +30,14 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsPropertyMapper; import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; +import org.springframework.boot.ssl.SslBundles; import org.springframework.context.annotation.Bean; import org.springframework.core.retry.RetryTemplate; +import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.client.ResponseErrorHandler; @@ -45,6 +50,7 @@ * @author Christian Tzolov * @author Thomas Vitale * @author Ilayaperumal Gopinathan + * @author yinh * @since 0.8.1 */ @AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class }) @@ -60,11 +66,21 @@ public MistralAiEmbeddingModel mistralAiEmbeddingModel(MistralAiCommonProperties MistralAiEmbeddingProperties embeddingProperties, ObjectProvider restClientBuilderProvider, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, - ObjectProvider observationConvention) { + ObjectProvider observationConvention, + ObjectProvider sslBundles, ObjectProvider globalHttpClientSettings, + ObjectProvider> factoryBuilder) { + + HttpClientSettingsPropertyMapper mapper = new HttpClientSettingsPropertyMapper(sslBundles.getIfAvailable(), + globalHttpClientSettings.getIfAvailable()); + HttpClientSettings httpClientSettings = mapper.map(commonProperties.getHttp()); + + RestClient.Builder restClientBuilder = restClientBuilderProvider.getIfAvailable(RestClient::builder); + applyRestClientSettings(restClientBuilder, httpClientSettings, + factoryBuilder.getIfAvailable(ClientHttpRequestFactoryBuilder::detect)); var mistralAiApi = mistralAiApi(embeddingProperties.getApiKey(), commonProperties.getApiKey(), - embeddingProperties.getBaseUrl(), commonProperties.getBaseUrl(), - restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler); + embeddingProperties.getBaseUrl(), commonProperties.getBaseUrl(), restClientBuilder, + responseErrorHandler); var embeddingModel = MistralAiEmbeddingModel.builder() .mistralAiApi(mistralAiApi) @@ -96,4 +112,10 @@ private MistralAiApi mistralAiApi(String apiKey, String commonApiKey, String bas .build(); } + private void applyRestClientSettings(RestClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpRequestFactoryBuilder factoryBuilder) { + ClientHttpRequestFactory requestFactory = factoryBuilder.build(httpClientSettings); + builder.requestFactory(requestFactory); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiModerationAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiModerationAutoConfiguration.java index cb9d89db164..6b7159df7af 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiModerationAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiModerationAutoConfiguration.java @@ -28,10 +28,15 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsPropertyMapper; import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; +import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.webclient.autoconfigure.WebClientAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.core.retry.RetryTemplate; +import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.client.ResponseErrorHandler; @@ -54,7 +59,9 @@ public class MistralAiModerationAutoConfiguration { @ConditionalOnMissingBean public MistralAiModerationModel mistralAiModerationModel(MistralAiCommonProperties commonProperties, MistralAiModerationProperties moderationProperties, RetryTemplate retryTemplate, - ObjectProvider restClientBuilderProvider, ResponseErrorHandler responseErrorHandler) { + ObjectProvider restClientBuilderProvider, ResponseErrorHandler responseErrorHandler, + ObjectProvider sslBundles, ObjectProvider globalHttpClientSettings, + ObjectProvider> factoryBuilder) { var apiKey = moderationProperties.getApiKey(); var baseUrl = moderationProperties.getBaseUrl(); @@ -65,10 +72,18 @@ public MistralAiModerationModel mistralAiModerationModel(MistralAiCommonProperti Assert.hasText(resolvedApiKey, "Mistral API key must be set"); Assert.hasText(resoledBaseUrl, "Mistral base URL must be set"); + HttpClientSettingsPropertyMapper mapper = new HttpClientSettingsPropertyMapper(sslBundles.getIfAvailable(), + globalHttpClientSettings.getIfAvailable()); + HttpClientSettings httpClientSettings = mapper.map(commonProperties.getHttp()); + + RestClient.Builder restClientBuilder = restClientBuilderProvider.getIfAvailable(RestClient::builder); + applyRestClientSettings(restClientBuilder, httpClientSettings, + factoryBuilder.getIfAvailable(ClientHttpRequestFactoryBuilder::detect)); + var mistralAiModerationApi = MistralAiModerationApi.builder() .baseUrl(resoledBaseUrl) .apiKey(resolvedApiKey) - .restClientBuilder(restClientBuilderProvider.getIfAvailable(RestClient::builder)) + .restClientBuilder(restClientBuilder) .responseErrorHandler(responseErrorHandler) .build(); @@ -79,4 +94,10 @@ public MistralAiModerationModel mistralAiModerationModel(MistralAiCommonProperti .build(); } + private void applyRestClientSettings(RestClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpRequestFactoryBuilder factoryBuilder) { + ClientHttpRequestFactory requestFactory = factoryBuilder.build(httpClientSettings); + builder.requestFactory(requestFactory); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiOcrAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiOcrAutoConfiguration.java index 4a92b82b015..76ec35f1b44 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiOcrAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiOcrAutoConfiguration.java @@ -25,8 +25,13 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsPropertyMapper; import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; +import org.springframework.boot.ssl.SslBundles; import org.springframework.context.annotation.Bean; +import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.client.ResponseErrorHandler; @@ -47,7 +52,9 @@ public class MistralAiOcrAutoConfiguration { @Bean @ConditionalOnMissingBean public MistralOcrApi mistralOcrApi(MistralAiCommonProperties commonProperties, MistralAiOcrProperties ocrProperties, - ObjectProvider restClientBuilderProvider, ResponseErrorHandler responseErrorHandler) { + ObjectProvider restClientBuilderProvider, ResponseErrorHandler responseErrorHandler, + ObjectProvider sslBundles, ObjectProvider globalHttpClientSettings, + ObjectProvider> factoryBuilder) { var apiKey = ocrProperties.getApiKey(); var baseUrl = ocrProperties.getBaseUrl(); @@ -58,8 +65,21 @@ public MistralOcrApi mistralOcrApi(MistralAiCommonProperties commonProperties, M Assert.hasText(resolvedApiKey, "Mistral API key must be set"); Assert.hasText(resolvedBaseUrl, "Mistral base URL must be set"); - return new MistralOcrApi(resolvedBaseUrl, resolvedApiKey, - restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler); + HttpClientSettingsPropertyMapper mapper = new HttpClientSettingsPropertyMapper(sslBundles.getIfAvailable(), + globalHttpClientSettings.getIfAvailable()); + HttpClientSettings httpClientSettings = mapper.map(commonProperties.getHttp()); + + RestClient.Builder restClientBuilder = restClientBuilderProvider.getIfAvailable(RestClient::builder); + applyRestClientSettings(restClientBuilder, httpClientSettings, + factoryBuilder.getIfAvailable(ClientHttpRequestFactoryBuilder::detect)); + + return new MistralOcrApi(resolvedBaseUrl, resolvedApiKey, restClientBuilder, responseErrorHandler); + } + + private void applyRestClientSettings(RestClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpRequestFactoryBuilder factoryBuilder) { + ClientHttpRequestFactory requestFactory = factoryBuilder.build(httpClientSettings); + builder.requestFactory(requestFactory); } } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfigurationIT.java index 96f6ad3650f..8325de8bea8 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfigurationIT.java @@ -97,4 +97,18 @@ void embedding() { }); } + @Test + void generateWithCustomTimeout() { + this.contextRunner.withConfiguration(SpringAiTestAutoConfigurations.of(MistralAiChatAutoConfiguration.class)) + .withPropertyValues("spring.ai.mistralai.connect-timeout=1ms", "spring.ai.mistralai.read-timeout=1ms") + .run(context -> { + MistralAiChatModel chatModel = context.getBean(MistralAiChatModel.class); + + String response = chatModel.call("Hello"); + assertThat(response).isNotNull(); + + logger.info("Response with custom timeout: " + response); + }); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiOcrPropertiesTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiOcrPropertiesTests.java index fd47b5be995..6c5001aa1ff 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiOcrPropertiesTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiOcrPropertiesTests.java @@ -16,6 +16,8 @@ package org.springframework.ai.model.mistralai.autoconfigure; +import java.time.Duration; + import org.junit.jupiter.api.Test; import org.springframework.ai.mistralai.ocr.MistralOcrApi; @@ -166,4 +168,22 @@ void ocrActivationViaModelProperty() { }); } + @Test + public void ocrCustomTimeouts() { + new ApplicationContextRunner().withPropertyValues( + // @formatter:off + "spring.ai.mistralai.api-key=API_KEY", + "spring.ai.mistralai.base-url=TEST_BASE_URL", + "spring.ai.mistralai.connect-timeout=5s", + "spring.ai.mistralai.read-timeout=30s") + // @formatter:on + .withConfiguration(this.autoConfigurations) + .run(context -> { + var connectionProperties = context.getBean(MistralAiCommonProperties.class); + + assertThat(connectionProperties.getConnectTimeout()).isEqualTo(Duration.ofSeconds(5)); + assertThat(connectionProperties.getReadTimeout()).isEqualTo(Duration.ofSeconds(30)); + }); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIConnectionProperties.java b/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIConnectionProperties.java index 69f33d91075..caed50fb366 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIConnectionProperties.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIConnectionProperties.java @@ -17,6 +17,7 @@ package org.springframework.ai.model.oci.genai.autoconfigure; import java.nio.file.Paths; +import java.time.Duration; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.util.StringUtils; @@ -53,6 +54,12 @@ public class OCIConnectionProperties { private String endpoint; + private Duration connectTimeout; + + private Duration readTimeout; + + private Integer maxAsyncThreads; + public String getRegion() { return this.region; } @@ -133,6 +140,30 @@ public void setEndpoint(String endpoint) { this.endpoint = endpoint; } + public Duration getConnectTimeout() { + return this.connectTimeout; + } + + public void setConnectTimeout(Duration connectTimeout) { + this.connectTimeout = connectTimeout; + } + + public Duration getReadTimeout() { + return this.readTimeout; + } + + public void setReadTimeout(Duration readTimeout) { + this.readTimeout = readTimeout; + } + + public Integer getMaxAsyncThreads() { + return this.maxAsyncThreads; + } + + public void setMaxAsyncThreads(Integer maxAsyncThreads) { + this.maxAsyncThreads = maxAsyncThreads; + } + public enum AuthenticationType { FILE("file"), INSTANCE_PRINCIPAL("instance-principal"), WORKLOAD_IDENTITY("workload-identity"), diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiInferenceClientAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiInferenceClientAutoConfiguration.java index 9188756f5cc..0a4207c1d33 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiInferenceClientAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiInferenceClientAutoConfiguration.java @@ -52,9 +52,20 @@ public class OCIGenAiInferenceClientAutoConfiguration { @ConditionalOnMissingBean public GenerativeAiInferenceClient generativeAiInferenceClient(OCIConnectionProperties properties) throws IOException { - ClientConfiguration clientConfiguration = ClientConfiguration.builder() - .retryConfiguration(RetryConfiguration.SDK_DEFAULT_RETRY_CONFIGURATION) - .build(); + ClientConfiguration.ClientConfigurationBuilder clientConfigurationBuilder = ClientConfiguration.builder() + .retryConfiguration(RetryConfiguration.SDK_DEFAULT_RETRY_CONFIGURATION); + + if (properties.getConnectTimeout() != null) { + clientConfigurationBuilder.connectionTimeoutMillis(properties.getConnectTimeout().toMillisPart()); + } + if (properties.getReadTimeout() != null) { + clientConfigurationBuilder.readTimeoutMillis(properties.getReadTimeout().toMillisPart()); + } + if (properties.getMaxAsyncThreads() != null) { + clientConfigurationBuilder.maxAsyncThreads(properties.getMaxAsyncThreads()); + } + ClientConfiguration clientConfiguration = clientConfigurationBuilder.build(); + GenerativeAiInferenceClient.Builder builder = GenerativeAiInferenceClient.builder() .configuration(clientConfiguration); if (StringUtils.hasText(properties.getRegion())) { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAIAutoConfigurationTest.java b/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAIAutoConfigurationTest.java index 4bca60c5590..4a6c6a708ab 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAIAutoConfigurationTest.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAIAutoConfigurationTest.java @@ -20,6 +20,7 @@ import java.nio.file.Path; import java.security.KeyPair; import java.security.KeyPairGenerator; +import java.time.Duration; import com.oracle.bmc.http.client.pki.Pem; import org.junit.jupiter.api.Test; @@ -54,7 +55,11 @@ void setProperties(@TempDir Path tempDir) throws Exception { "spring.ai.oci.genai.cohere.chat.options.topP=0.8", "spring.ai.oci.genai.cohere.chat.options.maxTokens=1000", "spring.ai.oci.genai.cohere.chat.options.frequencyPenalty=0.1", - "spring.ai.oci.genai.cohere.chat.options.presencePenalty=0.2" + "spring.ai.oci.genai.cohere.chat.options.presencePenalty=0.2", + + "spring.ai.oci.genai.connect-timeout:1s", + "spring.ai.oci.genai.read-timeout:1s", + "spring.ai.oci.genai.max-async-threads:30" // @formatter:on ).withConfiguration(SpringAiTestAutoConfigurations.of(OCIGenAiChatAutoConfiguration.class)); @@ -79,6 +84,10 @@ void setProperties(@TempDir Path tempDir) throws Exception { assertThat(props.getPrivateKey()).isEqualTo(tmp.toAbsolutePath().toString()); assertThat(props.getRegion()).isEqualTo("us-ashburn-1"); + assertThat(props.getConnectTimeout()).isEqualTo(Duration.ofSeconds(1)); + assertThat(props.getReadTimeout()).isEqualTo(Duration.ofSeconds(1)); + assertThat(props.getMaxAsyncThreads()).isEqualTo(30); + }); } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfigurationIT.java index c7a33a6444e..c041a89db84 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfigurationIT.java @@ -50,7 +50,9 @@ public class OCIGenAiAutoConfigurationIT { "spring.ai.oci.genai.file=" + this.CONFIG_FILE, "spring.ai.oci.genai.embedding.compartment=" + this.COMPARTMENT_ID, "spring.ai.oci.genai.embedding.servingMode=on-demand", - "spring.ai.oci.genai.embedding.model=cohere.embed-english-light-v2.0" + "spring.ai.oci.genai.embedding.model=cohere.embed-english-light-v2.0", + "spring.ai.oci.genai.connect-timeout:1ms", + "spring.ai.oci.genai.read-timeout:1ms" // @formatter:on ).withConfiguration(SpringAiTestAutoConfigurations.of(OCIGenAiEmbeddingAutoConfiguration.class)); @@ -60,7 +62,9 @@ public class OCIGenAiAutoConfigurationIT { "spring.ai.oci.genai.file=" + this.CONFIG_FILE, "spring.ai.oci.genai.cohere.chat.options.compartment=" + this.COMPARTMENT_ID, "spring.ai.oci.genai.cohere.chat.options.servingMode=on-demand", - "spring.ai.oci.genai.cohere.chat.options.model=" + this.CHAT_MODEL_ID + "spring.ai.oci.genai.cohere.chat.options.model=" + this.CHAT_MODEL_ID, + "spring.ai.oci.genai.connect-timeout:1ms", + "spring.ai.oci.genai.read-timeout:1ms" // @formatter:on ).withConfiguration(SpringAiTestAutoConfigurations.of(OCIGenAiChatAutoConfiguration.class)); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaApiAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaApiAutoConfiguration.java index 3df440cdc3f..400fe034eac 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaApiAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaApiAutoConfiguration.java @@ -23,9 +23,16 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsPropertyMapper; +import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder; import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; +import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.webclient.autoconfigure.WebClientAutoConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; import org.springframework.web.reactive.function.client.WebClient; @@ -37,6 +44,7 @@ * @author Eddú Meléndez * @author Thomas Vitale * @author Ilayaperumal Gopinathan + * @author yinh * @since 0.8.0 */ @AutoConfiguration(after = { RestClientAutoConfiguration.class, WebClientAutoConfiguration.class, @@ -54,16 +62,45 @@ PropertiesOllamaConnectionDetails ollamaConnectionDetails(OllamaConnectionProper @Bean @ConditionalOnMissingBean public OllamaApi ollamaApi(OllamaConnectionDetails connectionDetails, + OllamaConnectionProperties connectionProperties, ObjectProvider restClientBuilderProvider, - ObjectProvider webClientBuilderProvider, ResponseErrorHandler responseErrorHandler) { + ObjectProvider webClientBuilderProvider, ResponseErrorHandler responseErrorHandler, + ObjectProvider sslBundles, ObjectProvider globalHttpClientSettings, + ObjectProvider> factoryBuilder, + ObjectProvider> webConnectorBuilderProvider) { + + HttpClientSettingsPropertyMapper mapper = new HttpClientSettingsPropertyMapper(sslBundles.getIfAvailable(), + globalHttpClientSettings.getIfAvailable()); + HttpClientSettings httpClientSettings = mapper.map(connectionProperties.getHttp()); + + RestClient.Builder restClientBuilder = restClientBuilderProvider.getIfAvailable(RestClient::builder); + applyRestClientSettings(restClientBuilder, httpClientSettings, + factoryBuilder.getIfAvailable(ClientHttpRequestFactoryBuilder::detect)); + + WebClient.Builder webClientBuilder = webClientBuilderProvider.getIfAvailable(WebClient::builder); + applyWebClientSettings(webClientBuilder, httpClientSettings, + webConnectorBuilderProvider.getIfAvailable(ClientHttpConnectorBuilder::detect)); + return OllamaApi.builder() .baseUrl(connectionDetails.getBaseUrl()) - .restClientBuilder(restClientBuilderProvider.getIfAvailable(RestClient::builder)) - .webClientBuilder(webClientBuilderProvider.getIfAvailable(WebClient::builder)) + .restClientBuilder(restClientBuilder) + .webClientBuilder(webClientBuilder) .responseErrorHandler(responseErrorHandler) .build(); } + private void applyRestClientSettings(RestClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpRequestFactoryBuilder factoryBuilder) { + ClientHttpRequestFactory requestFactory = factoryBuilder.build(httpClientSettings); + builder.requestFactory(requestFactory); + } + + private void applyWebClientSettings(WebClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpConnectorBuilder connectorBuilder) { + ClientHttpConnector connector = connectorBuilder.build(httpClientSettings); + builder.clientConnector(connector); + } + static class PropertiesOllamaConnectionDetails implements OllamaConnectionDetails { private final OllamaConnectionProperties properties; diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaConnectionProperties.java b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaConnectionProperties.java index 58e7e1c6298..758396592de 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaConnectionProperties.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaConnectionProperties.java @@ -16,7 +16,14 @@ package org.springframework.ai.model.ollama.autoconfigure; +import java.time.Duration; + +import jakarta.annotation.Nullable; + import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.boot.http.client.HttpRedirects; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsProperties; /** * Ollama connection autoconfiguration properties. @@ -34,6 +41,10 @@ public class OllamaConnectionProperties { */ private String baseUrl = "http://localhost:11434"; + @NestedConfigurationProperty + private final HttpClientSettingsProperties http = new HttpClientSettingsProperties() { + }; + public String getBaseUrl() { return this.baseUrl; } @@ -42,4 +53,39 @@ public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; } + @Nullable + public HttpRedirects getRedirects() { + return this.http.getRedirects(); + } + + public void setRedirects(HttpRedirects redirects) { + this.http.setRedirects(redirects); + } + + @Nullable + public Duration getConnectTimeout() { + return this.http.getConnectTimeout(); + } + + public void setConnectTimeout(Duration connectTimeout) { + this.http.setConnectTimeout(connectTimeout); + } + + @Nullable + public Duration getReadTimeout() { + return this.http.getReadTimeout(); + } + + public void setReadTimeout(Duration readTimeout) { + this.http.setReadTimeout(readTimeout); + } + + public HttpClientSettingsProperties.Ssl getSsl() { + return this.http.getSsl(); + } + + public HttpClientSettingsProperties getHttp() { + return this.http; + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationIT.java index 74ca973d90e..c797926aeb8 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationIT.java @@ -129,4 +129,15 @@ void chatActivation() { }); } + @Test + void chatCompletionWithCustomTimeout() { + this.contextRunner.withPropertyValues("spring.ai.ollama.read-timeout=1ms") + .withPropertyValues("spring.ai.ollama.connect-timeout=1ms") + .run(context -> { + OllamaChatModel chatModel = context.getBean(OllamaChatModel.class); + ChatResponse response = chatModel.call(new Prompt(this.userMessage)); + assertThat(response.getResult().getOutput().getText()).contains("Copenhagen"); + }); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioSpeechAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioSpeechAutoConfiguration.java index a4623072f90..159ce7db167 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioSpeechAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioSpeechAutoConfiguration.java @@ -29,10 +29,17 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsPropertyMapper; +import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder; import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; +import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.webclient.autoconfigure.WebClientAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.core.retry.RetryTemplate; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; import org.springframework.web.reactive.function.client.WebClient; @@ -61,21 +68,48 @@ public class OpenAiAudioSpeechAutoConfiguration { public OpenAiAudioSpeechModel openAiAudioSpeechModel(OpenAiConnectionProperties commonProperties, OpenAiAudioSpeechProperties speechProperties, RetryTemplate retryTemplate, ObjectProvider restClientBuilderProvider, - ObjectProvider webClientBuilderProvider, ResponseErrorHandler responseErrorHandler) { + ObjectProvider webClientBuilderProvider, ResponseErrorHandler responseErrorHandler, + ObjectProvider sslBundles, ObjectProvider globalHttpClientSettings, + ObjectProvider> factoryBuilder, + ObjectProvider> webConnectorBuilderProvider) { OpenAIAutoConfigurationUtil.ResolvedConnectionProperties resolved = resolveConnectionProperties( commonProperties, speechProperties, "speech"); + HttpClientSettingsPropertyMapper mapper = new HttpClientSettingsPropertyMapper(sslBundles.getIfAvailable(), + globalHttpClientSettings.getIfAvailable()); + HttpClientSettings httpClientSettings = mapper.map(commonProperties.getHttp()); + + RestClient.Builder restClientBuilder = restClientBuilderProvider.getIfAvailable(RestClient::builder); + applyRestClientSettings(restClientBuilder, httpClientSettings, + factoryBuilder.getIfAvailable(ClientHttpRequestFactoryBuilder::detect)); + + WebClient.Builder webClientBuilder = webClientBuilderProvider.getIfAvailable(WebClient::builder); + applyWebClientSettings(webClientBuilder, httpClientSettings, + webConnectorBuilderProvider.getIfAvailable(ClientHttpConnectorBuilder::detect)); + var openAiAudioApi = OpenAiAudioApi.builder() .baseUrl(resolved.baseUrl()) .apiKey(new SimpleApiKey(resolved.apiKey())) .headers(resolved.headers()) - .restClientBuilder(restClientBuilderProvider.getIfAvailable(RestClient::builder)) - .webClientBuilder(webClientBuilderProvider.getIfAvailable(WebClient::builder)) + .restClientBuilder(restClientBuilder) + .webClientBuilder(webClientBuilder) .responseErrorHandler(responseErrorHandler) .build(); return new OpenAiAudioSpeechModel(openAiAudioApi, speechProperties.getOptions()); } + private void applyRestClientSettings(RestClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpRequestFactoryBuilder factoryBuilder) { + ClientHttpRequestFactory requestFactory = factoryBuilder.build(httpClientSettings); + builder.requestFactory(requestFactory); + } + + private void applyWebClientSettings(WebClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpConnectorBuilder connectorBuilder) { + ClientHttpConnector connector = connectorBuilder.build(httpClientSettings); + builder.clientConnector(connector); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioTranscriptionAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioTranscriptionAutoConfiguration.java index acba2f5d927..cf895811569 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioTranscriptionAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioTranscriptionAutoConfiguration.java @@ -29,10 +29,17 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsPropertyMapper; +import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder; import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; +import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.webclient.autoconfigure.WebClientAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.core.retry.RetryTemplate; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; import org.springframework.web.reactive.function.client.WebClient; @@ -61,17 +68,32 @@ public class OpenAiAudioTranscriptionAutoConfiguration { public OpenAiAudioTranscriptionModel openAiAudioTranscriptionModel(OpenAiConnectionProperties commonProperties, OpenAiAudioTranscriptionProperties transcriptionProperties, RetryTemplate retryTemplate, ObjectProvider restClientBuilderProvider, - ObjectProvider webClientBuilderProvider, ResponseErrorHandler responseErrorHandler) { + ObjectProvider webClientBuilderProvider, ResponseErrorHandler responseErrorHandler, + ObjectProvider sslBundles, ObjectProvider globalHttpClientSettings, + ObjectProvider> factoryBuilder, + ObjectProvider> webConnectorBuilderProvider) { OpenAIAutoConfigurationUtil.ResolvedConnectionProperties resolved = resolveConnectionProperties( commonProperties, transcriptionProperties, "transcription"); + HttpClientSettingsPropertyMapper mapper = new HttpClientSettingsPropertyMapper(sslBundles.getIfAvailable(), + globalHttpClientSettings.getIfAvailable()); + HttpClientSettings httpClientSettings = mapper.map(commonProperties.getHttp()); + + RestClient.Builder restClientBuilder = restClientBuilderProvider.getIfAvailable(RestClient::builder); + applyRestClientSettings(restClientBuilder, httpClientSettings, + factoryBuilder.getIfAvailable(ClientHttpRequestFactoryBuilder::detect)); + + WebClient.Builder webClientBuilder = webClientBuilderProvider.getIfAvailable(WebClient::builder); + applyWebClientSettings(webClientBuilder, httpClientSettings, + webConnectorBuilderProvider.getIfAvailable(ClientHttpConnectorBuilder::detect)); + var openAiAudioApi = OpenAiAudioApi.builder() .baseUrl(resolved.baseUrl()) .apiKey(new SimpleApiKey(resolved.apiKey())) .headers(resolved.headers()) - .restClientBuilder(restClientBuilderProvider.getIfAvailable(RestClient::builder)) - .webClientBuilder(webClientBuilderProvider.getIfAvailable(WebClient::builder)) + .restClientBuilder(restClientBuilder) + .webClientBuilder(webClientBuilder) .responseErrorHandler(responseErrorHandler) .build(); @@ -79,4 +101,16 @@ public OpenAiAudioTranscriptionModel openAiAudioTranscriptionModel(OpenAiConnect } + private void applyRestClientSettings(RestClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpRequestFactoryBuilder factoryBuilder) { + ClientHttpRequestFactory requestFactory = factoryBuilder.build(httpClientSettings); + builder.requestFactory(requestFactory); + } + + private void applyWebClientSettings(WebClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpConnectorBuilder connectorBuilder) { + ClientHttpConnector connector = connectorBuilder.build(httpClientSettings); + builder.clientConnector(connector); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiChatAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiChatAutoConfiguration.java index f8f5f801a11..a7e5da1dca3 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiChatAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiChatAutoConfiguration.java @@ -35,10 +35,17 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsPropertyMapper; +import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder; import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; +import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.webclient.autoconfigure.WebClientAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.core.retry.RetryTemplate; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; import org.springframework.web.reactive.function.client.WebClient; @@ -66,19 +73,34 @@ public class OpenAiChatAutoConfiguration { @ConditionalOnMissingBean public OpenAiApi openAiApi(OpenAiConnectionProperties commonProperties, OpenAiChatProperties chatProperties, ObjectProvider restClientBuilderProvider, - ObjectProvider webClientBuilderProvider, ResponseErrorHandler responseErrorHandler) { + ObjectProvider webClientBuilderProvider, ResponseErrorHandler responseErrorHandler, + ObjectProvider sslBundles, ObjectProvider globalHttpClientSettings, + ObjectProvider> factoryBuilder, + ObjectProvider> webConnectorBuilderProvider) { OpenAIAutoConfigurationUtil.ResolvedConnectionProperties resolved = resolveConnectionProperties( commonProperties, chatProperties, "chat"); + HttpClientSettingsPropertyMapper mapper = new HttpClientSettingsPropertyMapper(sslBundles.getIfAvailable(), + globalHttpClientSettings.getIfAvailable()); + HttpClientSettings httpClientSettings = mapper.map(commonProperties.getHttp()); + + RestClient.Builder restClientBuilder = restClientBuilderProvider.getIfAvailable(RestClient::builder); + applyRestClientSettings(restClientBuilder, httpClientSettings, + factoryBuilder.getIfAvailable(ClientHttpRequestFactoryBuilder::detect)); + + WebClient.Builder webClientBuilder = webClientBuilderProvider.getIfAvailable(WebClient::builder); + applyWebClientSettings(webClientBuilder, httpClientSettings, + webConnectorBuilderProvider.getIfAvailable(ClientHttpConnectorBuilder::detect)); + return OpenAiApi.builder() .baseUrl(resolved.baseUrl()) .apiKey(new SimpleApiKey(resolved.apiKey())) .headers(resolved.headers()) .completionsPath(chatProperties.getCompletionsPath()) .embeddingsPath(OpenAiEmbeddingProperties.DEFAULT_EMBEDDINGS_PATH) - .restClientBuilder(restClientBuilderProvider.getIfAvailable(RestClient::builder)) - .webClientBuilder(webClientBuilderProvider.getIfAvailable(WebClient::builder)) + .restClientBuilder(restClientBuilder) + .webClientBuilder(webClientBuilder) .responseErrorHandler(responseErrorHandler) .build(); } @@ -106,4 +128,16 @@ public OpenAiChatModel openAiChatModel(OpenAiApi openAiApi, OpenAiChatProperties return chatModel; } + private void applyRestClientSettings(RestClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpRequestFactoryBuilder factoryBuilder) { + ClientHttpRequestFactory requestFactory = factoryBuilder.build(httpClientSettings); + builder.requestFactory(requestFactory); + } + + private void applyWebClientSettings(WebClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpConnectorBuilder connectorBuilder) { + ClientHttpConnector connector = connectorBuilder.build(httpClientSettings); + builder.clientConnector(connector); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiConnectionProperties.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiConnectionProperties.java index 41f00e30331..c8d5e1a3013 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiConnectionProperties.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiConnectionProperties.java @@ -16,7 +16,14 @@ package org.springframework.ai.model.openai.autoconfigure; +import java.time.Duration; + +import jakarta.annotation.Nullable; + import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.boot.http.client.HttpRedirects; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsProperties; @ConfigurationProperties(OpenAiConnectionProperties.CONFIG_PREFIX) public class OpenAiConnectionProperties extends OpenAiParentProperties { @@ -25,8 +32,47 @@ public class OpenAiConnectionProperties extends OpenAiParentProperties { public static final String DEFAULT_BASE_URL = "https://api.openai.com"; + @NestedConfigurationProperty + private final HttpClientSettingsProperties http = new HttpClientSettingsProperties() { + }; + public OpenAiConnectionProperties() { super.setBaseUrl(DEFAULT_BASE_URL); } + @Nullable + public HttpRedirects getRedirects() { + return this.http.getRedirects(); + } + + public void setRedirects(HttpRedirects redirects) { + this.http.setRedirects(redirects); + } + + @Nullable + public Duration getConnectTimeout() { + return this.http.getConnectTimeout(); + } + + public void setConnectTimeout(Duration connectTimeout) { + this.http.setConnectTimeout(connectTimeout); + } + + @Nullable + public Duration getReadTimeout() { + return this.http.getReadTimeout(); + } + + public void setReadTimeout(Duration readTimeout) { + this.http.setReadTimeout(readTimeout); + } + + public HttpClientSettingsProperties.Ssl getSsl() { + return this.http.getSsl(); + } + + public HttpClientSettingsProperties getHttp() { + return this.http; + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiEmbeddingAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiEmbeddingAutoConfiguration.java index ac85dbdc248..bf97d8e5192 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiEmbeddingAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiEmbeddingAutoConfiguration.java @@ -31,10 +31,17 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsPropertyMapper; +import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder; import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; +import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.webclient.autoconfigure.WebClientAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.core.retry.RetryTemplate; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; import org.springframework.web.reactive.function.client.WebClient; @@ -64,11 +71,25 @@ public OpenAiEmbeddingModel openAiEmbeddingModel(OpenAiConnectionProperties comm OpenAiEmbeddingProperties embeddingProperties, ObjectProvider restClientBuilderProvider, ObjectProvider webClientBuilderProvider, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, - ObjectProvider observationConvention) { + ObjectProvider observationConvention, + ObjectProvider sslBundles, ObjectProvider globalHttpClientSettings, + ObjectProvider> factoryBuilder, + ObjectProvider> webConnectorBuilderProvider) { - var openAiApi = openAiApi(embeddingProperties, commonProperties, - restClientBuilderProvider.getIfAvailable(RestClient::builder), - webClientBuilderProvider.getIfAvailable(WebClient::builder), responseErrorHandler, "embedding"); + HttpClientSettingsPropertyMapper mapper = new HttpClientSettingsPropertyMapper(sslBundles.getIfAvailable(), + globalHttpClientSettings.getIfAvailable()); + HttpClientSettings httpClientSettings = mapper.map(commonProperties.getHttp()); + + RestClient.Builder restClientBuilder = restClientBuilderProvider.getIfAvailable(RestClient::builder); + applyRestClientSettings(restClientBuilder, httpClientSettings, + factoryBuilder.getIfAvailable(ClientHttpRequestFactoryBuilder::detect)); + + WebClient.Builder webClientBuilder = webClientBuilderProvider.getIfAvailable(WebClient::builder); + applyWebClientSettings(webClientBuilder, httpClientSettings, + webConnectorBuilderProvider.getIfAvailable(ClientHttpConnectorBuilder::detect)); + + var openAiApi = openAiApi(embeddingProperties, commonProperties, restClientBuilder, webClientBuilder, + responseErrorHandler, "embedding"); var embeddingModel = new OpenAiEmbeddingModel(openAiApi, embeddingProperties.getMetadataMode(), embeddingProperties.getOptions(), retryTemplate, @@ -98,4 +119,16 @@ private OpenAiApi openAiApi(OpenAiEmbeddingProperties embeddingProperties, .build(); } + private void applyRestClientSettings(RestClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpRequestFactoryBuilder factoryBuilder) { + ClientHttpRequestFactory requestFactory = factoryBuilder.build(httpClientSettings); + builder.requestFactory(requestFactory); + } + + private void applyWebClientSettings(WebClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpConnectorBuilder connectorBuilder) { + ClientHttpConnector connector = connectorBuilder.build(httpClientSettings); + builder.clientConnector(connector); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiImageAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiImageAutoConfiguration.java index ba7ee1f8c11..db95d9a441c 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiImageAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiImageAutoConfiguration.java @@ -32,10 +32,15 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsPropertyMapper; import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; +import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.webclient.autoconfigure.WebClientAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.core.retry.RetryTemplate; +import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; @@ -65,17 +70,27 @@ public OpenAiImageModel openAiImageModel(OpenAiConnectionProperties commonProper OpenAiImageProperties imageProperties, ObjectProvider restClientBuilderProvider, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, - ObjectProvider observationConvention) { + ObjectProvider observationConvention, + ObjectProvider sslBundles, ObjectProvider globalHttpClientSettings, + ObjectProvider> factoryBuilder) { OpenAIAutoConfigurationUtil.ResolvedConnectionProperties resolved = resolveConnectionProperties( commonProperties, imageProperties, "image"); + HttpClientSettingsPropertyMapper mapper = new HttpClientSettingsPropertyMapper(sslBundles.getIfAvailable(), + globalHttpClientSettings.getIfAvailable()); + HttpClientSettings httpClientSettings = mapper.map(commonProperties.getHttp()); + + RestClient.Builder restClientBuilder = restClientBuilderProvider.getIfAvailable(RestClient::builder); + applyRestClientSettings(restClientBuilder, httpClientSettings, + factoryBuilder.getIfAvailable(ClientHttpRequestFactoryBuilder::detect)); + var openAiImageApi = OpenAiImageApi.builder() .baseUrl(resolved.baseUrl()) .apiKey(new SimpleApiKey(resolved.apiKey())) .headers(resolved.headers()) .imagesPath(imageProperties.getImagesPath()) - .restClientBuilder(restClientBuilderProvider.getIfAvailable(RestClient::builder)) + .restClientBuilder(restClientBuilder) .responseErrorHandler(responseErrorHandler) .build(); var imageModel = new OpenAiImageModel(openAiImageApi, imageProperties.getOptions(), retryTemplate, @@ -86,4 +101,10 @@ public OpenAiImageModel openAiImageModel(OpenAiConnectionProperties commonProper return imageModel; } + private void applyRestClientSettings(RestClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpRequestFactoryBuilder factoryBuilder) { + ClientHttpRequestFactory requestFactory = factoryBuilder.build(httpClientSettings); + builder.requestFactory(requestFactory); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiModerationAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiModerationAutoConfiguration.java index f844f4d5ec8..21f7a35a8ee 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiModerationAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiModerationAutoConfiguration.java @@ -29,10 +29,15 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsPropertyMapper; import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; +import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.webclient.autoconfigure.WebClientAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.core.retry.RetryTemplate; +import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.RestClient; @@ -59,20 +64,36 @@ public class OpenAiModerationAutoConfiguration { @ConditionalOnMissingBean public OpenAiModerationModel openAiModerationModel(OpenAiConnectionProperties commonProperties, OpenAiModerationProperties moderationProperties, RetryTemplate retryTemplate, - ObjectProvider restClientBuilderProvider, ResponseErrorHandler responseErrorHandler) { + ObjectProvider restClientBuilderProvider, ResponseErrorHandler responseErrorHandler, + ObjectProvider sslBundles, ObjectProvider globalHttpClientSettings, + ObjectProvider> factoryBuilder) { OpenAIAutoConfigurationUtil.ResolvedConnectionProperties resolved = resolveConnectionProperties( commonProperties, moderationProperties, "moderation"); + HttpClientSettingsPropertyMapper mapper = new HttpClientSettingsPropertyMapper(sslBundles.getIfAvailable(), + globalHttpClientSettings.getIfAvailable()); + HttpClientSettings httpClientSettings = mapper.map(commonProperties.getHttp()); + + RestClient.Builder restClientBuilder = restClientBuilderProvider.getIfAvailable(RestClient::builder); + applyRestClientSettings(restClientBuilder, httpClientSettings, + factoryBuilder.getIfAvailable(ClientHttpRequestFactoryBuilder::detect)); + var openAiModerationApi = OpenAiModerationApi.builder() .baseUrl(resolved.baseUrl()) .apiKey(new SimpleApiKey(resolved.apiKey())) .headers(resolved.headers()) - .restClientBuilder(restClientBuilderProvider.getIfAvailable(RestClient::builder)) + .restClientBuilder(restClientBuilder) .responseErrorHandler(responseErrorHandler) .build(); return new OpenAiModerationModel(openAiModerationApi, retryTemplate) .withDefaultOptions(moderationProperties.getOptions()); } + private void applyRestClientSettings(RestClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpRequestFactoryBuilder factoryBuilder) { + ClientHttpRequestFactory requestFactory = factoryBuilder.build(httpClientSettings); + builder.requestFactory(requestFactory); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAutoConfigurationIT.java index 8cbacee1ac6..0707fe24800 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAutoConfigurationIT.java @@ -16,6 +16,7 @@ package org.springframework.ai.model.openai.autoconfigure; +import java.time.Duration; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @@ -214,4 +215,24 @@ void generateImageWithModel() { }); } + @Test + void generateWithCustomTimeout() { + // The 256x256 size is supported by dall-e-2, but not by dall-e-3. + this.contextRunner + .withPropertyValues("spring.ai.openai.connect-timeout=1ms", "spring.ai.openai.read-timeout=1ms") + .withConfiguration(SpringAiTestAutoConfigurations.of(OpenAiChatAutoConfiguration.class)) + .run(context -> { + OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); + + var connectionProperties = context.getBean(OpenAiConnectionProperties.class); + assertThat(connectionProperties.getConnectTimeout()).isEqualTo(Duration.ofMillis(1)); + assertThat(connectionProperties.getReadTimeout()).isEqualTo(Duration.ofMillis(1)); + + String response = chatModel.call("Hello"); + + assertThat(response).isNotEmpty(); + logger.info("Response: " + response); + }); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiPropertiesTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiPropertiesTests.java index 4271bbb234b..2638f949fea 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiPropertiesTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiPropertiesTests.java @@ -16,6 +16,8 @@ package org.springframework.ai.model.openai.autoconfigure; +import java.time.Duration; + import org.junit.jupiter.api.Test; import org.skyscreamer.jsonassert.JSONAssert; import org.skyscreamer.jsonassert.JSONCompareMode; @@ -699,4 +701,21 @@ public void moderationOptionsTest() { }); } + @Test + public void httpClientCustomTimeouts() { + this.contextRunner.withPropertyValues( + // @formatter:off + "spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL", + "spring.ai.openai.connect-timeout=5s", + "spring.ai.openai.read-timeout=30s") + // @formatter:on + .withConfiguration(SpringAiTestAutoConfigurations.of(OpenAiChatAutoConfiguration.class)) + .run(context -> { + var connectionProperties = context.getBean(OpenAiConnectionProperties.class); + + assertThat(connectionProperties.getConnectTimeout()).isEqualTo(Duration.ofSeconds(5)); + assertThat(connectionProperties.getReadTimeout()).isEqualTo(Duration.ofSeconds(30)); + }); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiConnectionProperties.java b/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiConnectionProperties.java index d1ec0261dc6..2435be29862 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiConnectionProperties.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiConnectionProperties.java @@ -16,8 +16,15 @@ package org.springframework.ai.model.stabilityai.autoconfigure; +import java.time.Duration; + +import jakarta.annotation.Nullable; + import org.springframework.ai.stabilityai.api.StabilityAiApi; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.boot.http.client.HttpRedirects; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsProperties; @ConfigurationProperties(StabilityAiConnectionProperties.CONFIG_PREFIX) public class StabilityAiConnectionProperties extends StabilityAiParentProperties { @@ -26,8 +33,47 @@ public class StabilityAiConnectionProperties extends StabilityAiParentProperties public static final String DEFAULT_BASE_URL = StabilityAiApi.DEFAULT_BASE_URL; + @NestedConfigurationProperty + private final HttpClientSettingsProperties http = new HttpClientSettingsProperties() { + }; + public StabilityAiConnectionProperties() { super.setBaseUrl(DEFAULT_BASE_URL); } + @Nullable + public HttpRedirects getRedirects() { + return this.http.getRedirects(); + } + + public void setRedirects(HttpRedirects redirects) { + this.http.setRedirects(redirects); + } + + @Nullable + public Duration getConnectTimeout() { + return this.http.getConnectTimeout(); + } + + public void setConnectTimeout(Duration connectTimeout) { + this.http.setConnectTimeout(connectTimeout); + } + + @Nullable + public Duration getReadTimeout() { + return this.http.getReadTimeout(); + } + + public void setReadTimeout(Duration readTimeout) { + this.http.setReadTimeout(readTimeout); + } + + public HttpClientSettingsProperties.Ssl getSsl() { + return this.http.getSsl(); + } + + public HttpClientSettingsProperties getHttp() { + return this.http; + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageAutoConfiguration.java index 70833cf572a..a6d1859cd0e 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageAutoConfiguration.java @@ -26,8 +26,13 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsPropertyMapper; import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; +import org.springframework.boot.ssl.SslBundles; import org.springframework.context.annotation.Bean; +import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.client.RestClient; @@ -50,7 +55,9 @@ public class StabilityAiImageAutoConfiguration { @Bean @ConditionalOnMissingBean public StabilityAiApi stabilityAiApi(StabilityAiConnectionProperties commonProperties, - StabilityAiImageProperties imageProperties, ObjectProvider restClientBuilderProvider) { + StabilityAiImageProperties imageProperties, ObjectProvider restClientBuilderProvider, + ObjectProvider sslBundles, ObjectProvider globalHttpClientSettings, + ObjectProvider> factoryBuilder) { String apiKey = StringUtils.hasText(imageProperties.getApiKey()) ? imageProperties.getApiKey() : commonProperties.getApiKey(); @@ -61,8 +68,15 @@ public StabilityAiApi stabilityAiApi(StabilityAiConnectionProperties commonPrope Assert.hasText(apiKey, "StabilityAI API key must be set"); Assert.hasText(baseUrl, "StabilityAI base URL must be set"); - return new StabilityAiApi(apiKey, imageProperties.getOptions().getModel(), baseUrl, - restClientBuilderProvider.getIfAvailable(RestClient::builder)); + HttpClientSettingsPropertyMapper mapper = new HttpClientSettingsPropertyMapper(sslBundles.getIfAvailable(), + globalHttpClientSettings.getIfAvailable()); + HttpClientSettings httpClientSettings = mapper.map(commonProperties.getHttp()); + + RestClient.Builder restClientBuilder = restClientBuilderProvider.getIfAvailable(RestClient::builder); + applyRestClientSettings(restClientBuilder, httpClientSettings, + factoryBuilder.getIfAvailable(ClientHttpRequestFactoryBuilder::detect)); + + return new StabilityAiApi(apiKey, imageProperties.getOptions().getModel(), baseUrl, restClientBuilder); } @Bean @@ -72,4 +86,10 @@ public StabilityAiImageModel stabilityAiImageModel(StabilityAiApi stabilityAiApi return new StabilityAiImageModel(stabilityAiApi, stabilityAiImageProperties.getOptions()); } + private void applyRestClientSettings(RestClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpRequestFactoryBuilder factoryBuilder) { + ClientHttpRequestFactory requestFactory = factoryBuilder.build(httpClientSettings); + builder.requestFactory(requestFactory); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiAutoConfigurationIT.java index ad447cb4fe3..7884b8af498 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiAutoConfigurationIT.java @@ -60,4 +60,29 @@ void generate() { }); } + @Test + void generateWithCustomTimeout() { + this.contextRunner + .withPropertyValues("spring.ai.stabilityai.connect-timeout=1s", "spring.ai.stabilityai.read-timeout=1s") + .withConfiguration(AutoConfigurations.of(StabilityAiImageAutoConfiguration.class)) + .run(context -> { + ImageModel imageModel = context.getBean(ImageModel.class); + StabilityAiImageOptions imageOptions = StabilityAiImageOptions.builder() + .stylePreset(StyleEnum.PHOTOGRAPHIC) + .build(); + + var instructions = """ + A light cream colored mini golden doodle. + """; + + ImagePrompt imagePrompt = new ImagePrompt(instructions, imageOptions); + ImageResponse imageResponse = imageModel.call(imagePrompt); + + ImageGeneration imageGeneration = imageResponse.getResult(); + Image image = imageGeneration.getOutput(); + + assertThat(image.getB64Json()).isNotEmpty(); + }); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImagePropertiesTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImagePropertiesTests.java index cfd9e7132c0..c50cd735098 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImagePropertiesTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImagePropertiesTests.java @@ -16,6 +16,8 @@ package org.springframework.ai.model.stabilityai.autoconfigure; +import java.time.Duration; + import org.junit.jupiter.api.Test; import org.springframework.ai.stabilityai.StabilityAiImageModel; @@ -48,12 +50,16 @@ public void chatPropertiesTest() { "spring.ai.stabilityai.image.options.sampler=K_EULER", "spring.ai.stabilityai.image.options.seed=0", "spring.ai.stabilityai.image.options.steps=30", - "spring.ai.stabilityai.image.options.style-preset=neon-punk" + "spring.ai.stabilityai.image.options.style-preset=neon-punk", + + "spring.ai.stabilityai.connect-timeout=5s", + "spring.ai.stabilityai.read-timeout=30s" ) // @formatter:on .withConfiguration(AutoConfigurations.of(StabilityAiImageAutoConfiguration.class)) .run(context -> { var chatProperties = context.getBean(StabilityAiImageProperties.class); + var connectionProperties = context.getBean(StabilityAiConnectionProperties.class); assertThat(chatProperties.getBaseUrl()).isEqualTo("ENDPOINT"); assertThat(chatProperties.getApiKey()).isEqualTo("API_KEY"); @@ -69,6 +75,9 @@ public void chatPropertiesTest() { assertThat(chatProperties.getOptions().getSeed()).isEqualTo(0); assertThat(chatProperties.getOptions().getSteps()).isEqualTo(30); assertThat(chatProperties.getOptions().getStylePreset()).isEqualTo("neon-punk"); + + assertThat(connectionProperties.getConnectTimeout()).isEqualTo(Duration.ofSeconds(5)); + assertThat(connectionProperties.getReadTimeout()).isEqualTo(Duration.ofSeconds(30)); }); } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiChatAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiChatAutoConfiguration.java index 4023978c697..a538785878a 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiChatAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiChatAutoConfiguration.java @@ -35,9 +35,16 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsPropertyMapper; +import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder; import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; +import org.springframework.boot.ssl.SslBundles; import org.springframework.context.annotation.Bean; import org.springframework.core.retry.RetryTemplate; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.client.ResponseErrorHandler; @@ -65,12 +72,26 @@ public ZhiPuAiChatModel zhiPuAiChatModel(ZhiPuAiConnectionProperties commonPrope ObjectProvider webClientBuilderProvider, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, ObjectProvider observationConvention, ToolCallingManager toolCallingManager, - ObjectProvider toolExecutionEligibilityPredicate) { + ObjectProvider toolExecutionEligibilityPredicate, + ObjectProvider sslBundles, ObjectProvider globalHttpClientSettings, + ObjectProvider> factoryBuilder, + ObjectProvider> webConnectorBuilderProvider) { + + HttpClientSettingsPropertyMapper mapper = new HttpClientSettingsPropertyMapper(sslBundles.getIfAvailable(), + globalHttpClientSettings.getIfAvailable()); + HttpClientSettings httpClientSettings = mapper.map(commonProperties.getHttp()); + + RestClient.Builder restClientBuilder = restClientBuilderProvider.getIfAvailable(RestClient::builder); + applyRestClientSettings(restClientBuilder, httpClientSettings, + factoryBuilder.getIfAvailable(ClientHttpRequestFactoryBuilder::detect)); + + WebClient.Builder webClientBuilder = webClientBuilderProvider.getIfAvailable(WebClient::builder); + applyWebClientSettings(webClientBuilder, httpClientSettings, + webConnectorBuilderProvider.getIfAvailable(ClientHttpConnectorBuilder::detect)); var zhiPuAiApi = zhiPuAiApi(chatProperties.getBaseUrl(), commonProperties.getBaseUrl(), - chatProperties.getApiKey(), commonProperties.getApiKey(), - restClientBuilderProvider.getIfAvailable(RestClient::builder), - webClientBuilderProvider.getIfAvailable(WebClient::builder), responseErrorHandler); + chatProperties.getApiKey(), commonProperties.getApiKey(), restClientBuilder, webClientBuilder, + responseErrorHandler); var chatModel = new ZhiPuAiChatModel(zhiPuAiApi, chatProperties.getOptions(), toolCallingManager, retryTemplate, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP), @@ -101,4 +122,16 @@ private ZhiPuAiApi zhiPuAiApi(String baseUrl, String commonBaseUrl, String apiKe } + private void applyRestClientSettings(RestClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpRequestFactoryBuilder factoryBuilder) { + ClientHttpRequestFactory requestFactory = factoryBuilder.build(httpClientSettings); + builder.requestFactory(requestFactory); + } + + private void applyWebClientSettings(WebClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpConnectorBuilder connectorBuilder) { + ClientHttpConnector connector = connectorBuilder.build(httpClientSettings); + builder.clientConnector(connector); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiConnectionProperties.java b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiConnectionProperties.java index 3e3aee1445e..733d5d62170 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiConnectionProperties.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiConnectionProperties.java @@ -16,7 +16,14 @@ package org.springframework.ai.model.zhipuai.autoconfigure; +import java.time.Duration; + +import jakarta.annotation.Nullable; + import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; +import org.springframework.boot.http.client.HttpRedirects; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsProperties; @ConfigurationProperties(ZhiPuAiConnectionProperties.CONFIG_PREFIX) public class ZhiPuAiConnectionProperties extends ZhiPuAiParentProperties { @@ -25,8 +32,47 @@ public class ZhiPuAiConnectionProperties extends ZhiPuAiParentProperties { public static final String DEFAULT_BASE_URL = "https://open.bigmodel.cn/api/paas"; + @NestedConfigurationProperty + private final HttpClientSettingsProperties http = new HttpClientSettingsProperties() { + }; + public ZhiPuAiConnectionProperties() { super.setBaseUrl(DEFAULT_BASE_URL); } + @Nullable + public HttpRedirects getRedirects() { + return this.http.getRedirects(); + } + + public void setRedirects(HttpRedirects redirects) { + this.http.setRedirects(redirects); + } + + @Nullable + public Duration getConnectTimeout() { + return this.http.getConnectTimeout(); + } + + public void setConnectTimeout(Duration connectTimeout) { + this.http.setConnectTimeout(connectTimeout); + } + + @Nullable + public Duration getReadTimeout() { + return this.http.getReadTimeout(); + } + + public void setReadTimeout(Duration readTimeout) { + this.http.setReadTimeout(readTimeout); + } + + public HttpClientSettingsProperties.Ssl getSsl() { + return this.http.getSsl(); + } + + public HttpClientSettingsProperties getHttp() { + return this.http; + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiEmbeddingAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiEmbeddingAutoConfiguration.java index 94ab54dfda9..2f5f3e8b0ec 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiEmbeddingAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiEmbeddingAutoConfiguration.java @@ -31,9 +31,16 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsPropertyMapper; +import org.springframework.boot.http.client.reactive.ClientHttpConnectorBuilder; import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; +import org.springframework.boot.ssl.SslBundles; import org.springframework.context.annotation.Bean; import org.springframework.core.retry.RetryTemplate; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.client.ResponseErrorHandler; @@ -60,12 +67,26 @@ public ZhiPuAiEmbeddingModel zhiPuAiEmbeddingModel(ZhiPuAiConnectionProperties c ObjectProvider restClientBuilderProvider, ObjectProvider webClientBuilderProvider, RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, - ObjectProvider observationConvention) { + ObjectProvider observationConvention, + ObjectProvider sslBundles, ObjectProvider globalHttpClientSettings, + ObjectProvider> factoryBuilder, + ObjectProvider> webConnectorBuilderProvider) { + + HttpClientSettingsPropertyMapper mapper = new HttpClientSettingsPropertyMapper(sslBundles.getIfAvailable(), + globalHttpClientSettings.getIfAvailable()); + HttpClientSettings httpClientSettings = mapper.map(commonProperties.getHttp()); + + RestClient.Builder restClientBuilder = restClientBuilderProvider.getIfAvailable(RestClient::builder); + applyRestClientSettings(restClientBuilder, httpClientSettings, + factoryBuilder.getIfAvailable(ClientHttpRequestFactoryBuilder::detect)); + + WebClient.Builder webClientBuilder = webClientBuilderProvider.getIfAvailable(WebClient::builder); + applyWebClientSettings(webClientBuilder, httpClientSettings, + webConnectorBuilderProvider.getIfAvailable(ClientHttpConnectorBuilder::detect)); var zhiPuAiApi = zhiPuAiApi(embeddingProperties.getBaseUrl(), commonProperties.getBaseUrl(), - embeddingProperties.getApiKey(), commonProperties.getApiKey(), - restClientBuilderProvider.getIfAvailable(RestClient::builder), - webClientBuilderProvider.getIfAvailable(WebClient::builder), responseErrorHandler); + embeddingProperties.getApiKey(), commonProperties.getApiKey(), restClientBuilder, webClientBuilder, + responseErrorHandler); var embeddingModel = new ZhiPuAiEmbeddingModel(zhiPuAiApi, embeddingProperties.getMetadataMode(), embeddingProperties.getOptions(), retryTemplate, @@ -95,4 +116,16 @@ private ZhiPuAiApi zhiPuAiApi(String baseUrl, String commonBaseUrl, String apiKe .build(); } + private void applyRestClientSettings(RestClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpRequestFactoryBuilder factoryBuilder) { + ClientHttpRequestFactory requestFactory = factoryBuilder.build(httpClientSettings); + builder.requestFactory(requestFactory); + } + + private void applyWebClientSettings(WebClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpConnectorBuilder connectorBuilder) { + ClientHttpConnector connector = connectorBuilder.build(httpClientSettings); + builder.clientConnector(connector); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiImageAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiImageAutoConfiguration.java index d1e876f2afd..3c61304551a 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiImageAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiImageAutoConfiguration.java @@ -28,9 +28,14 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder; +import org.springframework.boot.http.client.HttpClientSettings; +import org.springframework.boot.http.client.autoconfigure.HttpClientSettingsPropertyMapper; import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; +import org.springframework.boot.ssl.SslBundles; import org.springframework.context.annotation.Bean; import org.springframework.core.retry.RetryTemplate; +import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.web.client.ResponseErrorHandler; @@ -53,7 +58,9 @@ public class ZhiPuAiImageAutoConfiguration { @ConditionalOnMissingBean public ZhiPuAiImageModel zhiPuAiImageModel(ZhiPuAiConnectionProperties commonProperties, ZhiPuAiImageProperties imageProperties, ObjectProvider restClientBuilderProvider, - RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler) { + RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, + ObjectProvider sslBundles, ObjectProvider globalHttpClientSettings, + ObjectProvider> factoryBuilder) { String apiKey = StringUtils.hasText(imageProperties.getApiKey()) ? imageProperties.getApiKey() : commonProperties.getApiKey(); @@ -64,11 +71,24 @@ public ZhiPuAiImageModel zhiPuAiImageModel(ZhiPuAiConnectionProperties commonPro Assert.hasText(apiKey, "ZhiPuAI API key must be set"); Assert.hasText(baseUrl, "ZhiPuAI base URL must be set"); + HttpClientSettingsPropertyMapper mapper = new HttpClientSettingsPropertyMapper(sslBundles.getIfAvailable(), + globalHttpClientSettings.getIfAvailable()); + HttpClientSettings httpClientSettings = mapper.map(commonProperties.getHttp()); + + RestClient.Builder restClientBuilder = restClientBuilderProvider.getIfAvailable(RestClient::builder); + applyRestClientSettings(restClientBuilder, httpClientSettings, + factoryBuilder.getIfAvailable(ClientHttpRequestFactoryBuilder::detect)); + // TODO add ZhiPuAiApi support for image - var zhiPuAiImageApi = new ZhiPuAiImageApi(baseUrl, apiKey, - restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler); + var zhiPuAiImageApi = new ZhiPuAiImageApi(baseUrl, apiKey, restClientBuilder, responseErrorHandler); return new ZhiPuAiImageModel(zhiPuAiImageApi, imageProperties.getOptions(), retryTemplate); } + private void applyRestClientSettings(RestClient.Builder builder, HttpClientSettings httpClientSettings, + ClientHttpRequestFactoryBuilder factoryBuilder) { + ClientHttpRequestFactory requestFactory = factoryBuilder.build(httpClientSettings); + builder.requestFactory(requestFactory); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfigurationIT.java index ee10dfd6972..c31a4fa8a3c 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfigurationIT.java @@ -16,7 +16,9 @@ package org.springframework.ai.model.zhipuai.autoconfigure; +import java.time.Duration; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import org.apache.commons.logging.Log; @@ -112,4 +114,46 @@ void generateImage() { }); } + @Test + void generateWithCustomTimeout() { + this.contextRunner + .withPropertyValues("spring.ai.zhipuai.connect-timeout=1s", "spring.ai.zhipuai.read-timeout=1s") + .withConfiguration(SpringAiTestAutoConfigurations.of(ZhiPuAiChatAutoConfiguration.class)) + .run(context -> { + ZhiPuAiChatModel client = context.getBean(ZhiPuAiChatModel.class); + + var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class); + assertThat(connectionProperties.getConnectTimeout()).isEqualTo(Duration.ofSeconds(5)); + assertThat(connectionProperties.getReadTimeout()).isEqualTo(Duration.ofSeconds(30)); + + String response = client.call("Hello"); + assertThat(response).isNotEmpty(); + logger.info("Response with custom timeout: " + response); + }); + } + + @Test + void generateStreamingWithCustomTimeout() { + this.contextRunner + .withPropertyValues("spring.ai.zhipuai.connect-timeout=1s", "spring.ai.zhipuai.read-timeout=1s") + .withConfiguration(SpringAiTestAutoConfigurations.of(ZhiPuAiChatAutoConfiguration.class)) + .run(context -> { + ZhiPuAiChatModel client = context.getBean(ZhiPuAiChatModel.class); + + // Verify that the HTTP client configuration is applied + var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class); + assertThat(connectionProperties.getConnectTimeout()).isEqualTo(Duration.ofSeconds(1)); + assertThat(connectionProperties.getReadTimeout()).isEqualTo(Duration.ofSeconds(1)); + + Flux responseFlux = client.stream(new Prompt(new UserMessage("Hello"))); + String response = Objects.requireNonNull(responseFlux.collectList().block()) + .stream() + .map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText()) + .collect(Collectors.joining()); + + assertThat(response).isNotEmpty(); + logger.info("Response with custom timeout: " + response); + }); + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiPropertiesTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiPropertiesTests.java index 15bfca09096..dcb91aea75f 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiPropertiesTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiPropertiesTests.java @@ -16,6 +16,8 @@ package org.springframework.ai.model.zhipuai.autoconfigure; +import java.time.Duration; + import org.junit.jupiter.api.Test; import org.skyscreamer.jsonassert.JSONAssert; import org.skyscreamer.jsonassert.JSONCompareMode; @@ -76,7 +78,9 @@ public void chatOverrideConnectionProperties() { "spring.ai.zhipuai.chat.base-url=TEST_BASE_URL2", "spring.ai.zhipuai.chat.api-key=456", "spring.ai.zhipuai.chat.options.model=MODEL_XYZ", - "spring.ai.zhipuai.chat.options.temperature=0.55") + "spring.ai.zhipuai.chat.options.temperature=0.55", + "spring.ai.zhipuai.connect-timeout=5s", + "spring.ai.zhipuai.read-timeout=30s") // @formatter:on .withConfiguration(SpringAiTestAutoConfigurations.of(ZhiPuAiChatAutoConfiguration.class)) .run(context -> { @@ -91,6 +95,9 @@ public void chatOverrideConnectionProperties() { assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ"); assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55); + + assertThat(connectionProperties.getConnectTimeout()).isEqualTo(Duration.ofSeconds(5)); + assertThat(connectionProperties.getReadTimeout()).isEqualTo(Duration.ofSeconds(30)); }); } diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/audio/speech/elevenlabs-speech.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/audio/speech/elevenlabs-speech.adoc index d680e1fb127..bdaa9f52199 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/audio/speech/elevenlabs-speech.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/audio/speech/elevenlabs-speech.adoc @@ -46,6 +46,20 @@ The prefix `spring.ai.elevenlabs` is used as the property prefix for *all* Eleve | spring.ai.elevenlabs.api-key | Your ElevenLabs API key. | - |==== +==== HTTP Client Properties + +The prefix `spring.ai.elevenlabs` is used as the property prefix that lets you configure the HTTP client for ElevenLabs API calls. + +[cols="3,5,1"] +|==== +| Property | Description | Default + +| spring.ai.elevenlabs.connect-timeout | HTTP connection timeout | - +| spring.ai.elevenlabs.read-timeout | HTTP read timeout | - +| spring.ai.elevenlabs.redirects | HTTP redirect strategy (FOLLOW, NORMAL, DONT_FOLLOW) | - +| spring.ai.elevenlabs.http.ssl.bundle | SSL bundle name for secure connections | - +|==== + === Configuration Properties [NOTE] diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/anthropic-chat.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/anthropic-chat.adoc index 0e3e6a23853..6297ea93f3a 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/anthropic-chat.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/anthropic-chat.adoc @@ -128,6 +128,20 @@ The prefix `spring.ai.anthropic` is used as the property prefix that lets you co the output tokens limit is increased from `4096` to `8192` tokens (for claude-3-5-sonnet only). | `tools-2024-04-04` |==== +==== HTTP Client Properties + +The prefix `spring.ai.anthropic` is used as the property prefix that lets you configure the HTTP client for Anthropic API calls. + +[cols="3,5,1", stripes=even] +|==== +| Property | Description | Default + +| spring.ai.anthropic.connect-timeout | HTTP connection timeout | - +| spring.ai.anthropic.read-timeout | HTTP read timeout | - +| spring.ai.anthropic.redirects | HTTP redirect strategy (FOLLOW, NORMAL, DONT_FOLLOW) | - +| spring.ai.anthropic.http.ssl.bundle | SSL bundle name for secure connections | - +|==== + ==== Configuration Properties [NOTE] diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/azure-openai-chat.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/azure-openai-chat.adoc index e9dfc40b670..59739022f03 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/azure-openai-chat.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/azure-openai-chat.adoc @@ -201,6 +201,11 @@ The prefix `spring.ai.azure.openai` is the property prefix to configure the conn This automatically sets the endpoint to https://api.openai.com/v1. Use either `api-key` or `openai-api-key` property. With this configuration the `spring.ai.azure.openai.chat.options.deployment-name` is treated as an https://platform.openai.com/docs/models[OpenAi Model] name.| - | spring.ai.azure.openai.custom-headers | A map of custom headers to be included in the API requests. Each entry in the map represents a header, where the key is the header name and the value is the header value. | Empty map +| spring.ai.azure.openai.connect-timeout | HTTP connection timeout | - +| spring.ai.azure.openai.read-timeout | HTTP read timeout | - +| spring.ai.azure.openai.write-timeout | HTTP write timeout | - +| spring.ai.azure.openai.response-timeout | HTTP response timeout | - +| spring.ai.azure.openai.maximum-connection-pool-size | Maximum number of connections in the HTTP connection pool | - |==== [NOTE] diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/deepseek-chat.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/deepseek-chat.adoc index a5362409c2d..7ad61d6f95b 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/deepseek-chat.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/deepseek-chat.adoc @@ -106,6 +106,20 @@ The prefix `spring.ai.deepseek` is used as the property prefix that lets you con | spring.ai.deepseek.api-key | The API Key | - |==== +==== HTTP Client Properties + +The prefix `spring.ai.deepseek` is used as the property prefix that lets you configure the HTTP client for DeepSeek API calls. + +[cols="3,5,1"] +|==== +| Property | Description | Default + +| spring.ai.deepseek.connect-timeout | HTTP connection timeout | - +| spring.ai.deepseek.read-timeout | HTTP read timeout | - +| spring.ai.deepseek.redirects | HTTP redirect strategy (FOLLOW, NORMAL, DONT_FOLLOW) | - +| spring.ai.deepseek.http.ssl.bundle | SSL bundle name for secure connections | - +|==== + ==== Configuration Properties The prefix `spring.ai.deepseek.chat` is the property prefix that lets you configure the chat model implementation for DeepSeek. diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/google-genai-chat.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/google-genai-chat.adoc index 1bc24d26c9a..c1975b50267 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/google-genai-chat.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/google-genai-chat.adoc @@ -97,6 +97,19 @@ The prefix `spring.ai.google.genai` is used as the property prefix that lets you | spring.ai.google.genai.credentials-uri | URI to Google Cloud credentials. When provided it is used to create a `GoogleCredentials` instance for authentication. | - |==== +==== HTTP Client Properties + +The prefix `spring.ai.google.genai` is used as the property prefix that lets you configure the HTTP client for Google GenAI API calls. + +[cols="3,5,1", stripes=even] +|==== +| Property | Description | Default + +| spring.ai.google.genai.timeout | Timeout for the request | - +| spring.ai.google.genai.max-connections | Maximum number of connections allowed in the pool | - +| spring.ai.google.genai.max-connections-per-host | Maximum number of connections allowed per host | - +|==== + ==== Chat Model Properties The prefix `spring.ai.google.genai.chat` is the property prefix that lets you configure the chat model implementation for Google GenAI Chat. diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/minimax-chat.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/minimax-chat.adoc index 613cc1c61e2..bae238bf5b6 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/minimax-chat.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/minimax-chat.adoc @@ -112,6 +112,20 @@ The prefix `spring.ai.minimax` is used as the property prefix that lets you conn | spring.ai.minimax.api-key | The API Key | - |==== +==== HTTP Client Properties + +The prefix `spring.ai.minimax` is used as the property prefix that lets you configure the HTTP client for MiniMax API calls. + +[cols="3,5,1", stripes=even] +|==== +| Property | Description | Default + +| spring.ai.minimax.connect-timeout | HTTP connection timeout | - +| spring.ai.minimax.read-timeout | HTTP read timeout | - +| spring.ai.minimax.redirects | HTTP redirect strategy (FOLLOW, NORMAL, DONT_FOLLOW) | - +| spring.ai.minimax.http.ssl.bundle | SSL bundle name for secure connections | - +|==== + ==== Configuration Properties [NOTE] diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/mistralai-chat.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/mistralai-chat.adoc index 0a06fbdeb48..d234695ef97 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/mistralai-chat.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/mistralai-chat.adoc @@ -113,6 +113,20 @@ The prefix `spring.ai.mistralai` is used as the property prefix that lets you co | spring.ai.mistralai.api-key | The API Key | - |==== +==== HTTP Client Properties + +The prefix `spring.ai.mistralai` is used as the property prefix that lets you configure the HTTP client for Mistral AI API calls. + +[cols="3,5,1", stripes=even] +|==== +| Property | Description | Default + +| spring.ai.mistralai.connect-timeout | HTTP connection timeout | - +| spring.ai.mistralai.read-timeout | HTTP read timeout | - +| spring.ai.mistralai.redirects | HTTP redirect strategy (FOLLOW, NORMAL, DONT_FOLLOW) | - +| spring.ai.mistralai.http.ssl.bundle | SSL bundle name for secure connections | - +|==== + ==== Configuration Properties [NOTE] diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/oci-genai/cohere-chat.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/oci-genai/cohere-chat.adoc index ae6df44fa18..e109b1d6728 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/oci-genai/cohere-chat.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/oci-genai/cohere-chat.adoc @@ -64,7 +64,9 @@ The prefix `spring.ai.oci.genai` is the property prefix to configure the connect | spring.ai.oci.genai.file | Path to OCI config file. Used when authenticating with `file` auth. | /.oci/config | spring.ai.oci.genai.profile | OCI profile name. Used when authenticating with `file` auth. | DEFAULT | spring.ai.oci.genai.endpoint | Optional OCI GenAI endpoint. | - - +| spring.ai.oci.genai.connect-timeout | HTTP connection timeout | - +| spring.ai.oci.genai.read-timeout | HTTP read timeout | - +| spring.ai.oci.genai.max-async-threads | Maximum number of async threads | - |==== diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/ollama-chat.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/ollama-chat.adoc index 55b010cc58e..7af6e75a41d 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/ollama-chat.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/ollama-chat.adoc @@ -75,6 +75,20 @@ The prefix `spring.ai.ollama` is the property prefix to configure the connection | spring.ai.ollama.base-url | Base URL where Ollama API server is running. | `+http://localhost:11434+` |==== +==== HTTP Client Properties + +The prefix `spring.ai.ollama` is used as the property prefix that lets you configure the HTTP client for Ollama API calls. + +[cols="3,6,1", stripes=even] +|==== +| Property | Description | Default + +| spring.ai.ollama.connect-timeout | HTTP connection timeout | - +| spring.ai.ollama.read-timeout | HTTP read timeout | - +| spring.ai.ollama.redirects | HTTP redirect strategy (FOLLOW, NORMAL, DONT_FOLLOW) | - +| spring.ai.ollama.http.ssl.bundle | SSL bundle name for secure connections | - +|==== + Here are the properties for initializing the Ollama integration and xref:auto-pulling-models[auto-pulling models]. [cols="3,6,1"] diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/openai-chat.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/openai-chat.adoc index 7977fcd3730..759620eb154 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/openai-chat.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/openai-chat.adoc @@ -120,6 +120,20 @@ The prefix `spring.ai.openai` is used as the property prefix that lets you conne TIP: For users that belong to multiple organizations (or are accessing their projects through their legacy user API key), you can optionally specify which organization and project is used for an API request. Usage from these API requests will count as usage for the specified organization and project. +==== HTTP Client Properties + +The prefix `spring.ai.openai` is used as the property prefix that lets you configure the HTTP client for OpenAI API calls. + +[cols="3,5,1", stripes=even] +|==== +| Property | Description | Default + +| spring.ai.openai.connect-timeout | HTTP connection timeout | - +| spring.ai.openai.read-timeout | HTTP read timeout | - +| spring.ai.openai.redirects | HTTP redirect strategy (FOLLOW, NORMAL, DONT_FOLLOW) | - +| spring.ai.openai.http.ssl.bundle | SSL bundle name for secure connections | - +|==== + ==== User-Agent Header Spring AI automatically sends a `User-Agent: spring-ai` header with all requests to OpenAI. diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/zhipuai-chat.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/zhipuai-chat.adoc index 34f6e4bfe30..c93f4571adc 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/zhipuai-chat.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/zhipuai-chat.adoc @@ -115,6 +115,20 @@ If you are using the Z.ai Platform, you need to set it to `https://api.z.ai/api/ | spring.ai.zhipuai.api-key | The API Key | - |==== +==== HTTP Client Properties + +The prefix `spring.ai.zhipuai` is used as the property prefix that lets you configure the HTTP client for ZhiPu AI API calls. + +[cols="3,5,1", stripes=even] +|==== +| Property | Description | Default + +| spring.ai.zhipuai.connect-timeout | HTTP connection timeout | - +| spring.ai.zhipuai.read-timeout | HTTP read timeout | - +| spring.ai.zhipuai.redirects | HTTP redirect strategy (FOLLOW, NORMAL, DONT_FOLLOW) | - +| spring.ai.zhipuai.http.ssl.bundle | SSL bundle name for secure connections | - +|==== + ==== Configuration Properties [NOTE] diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/image/stabilityai-image.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/image/stabilityai-image.adoc index a94f1593c6f..aa3010adbf7 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/image/stabilityai-image.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/image/stabilityai-image.adoc @@ -83,6 +83,20 @@ The prefix `spring.ai.stabilityai` is used as the property prefix that lets you | spring.ai.stabilityai.api-key | The API Key | - |==== +==== HTTP Client Properties + +The prefix `spring.ai.stabilityai` is used as the property prefix that lets you configure the HTTP client for Stability AI API calls. + +[cols="3,5,1"] +|==== +| Property | Description | Default + +| spring.ai.stabilityai.connect-timeout | HTTP connection timeout | - +| spring.ai.stabilityai.read-timeout | HTTP read timeout | - +| spring.ai.stabilityai.redirects | HTTP redirect strategy (FOLLOW, NORMAL, DONT_FOLLOW) | - +| spring.ai.stabilityai.http.ssl.bundle | SSL bundle name for secure connections | - +|==== + [NOTE] ==== Enabling and disabling of the image auto-configurations are now configured via top level properties with the prefix `spring.ai.model.image`.