bedrockRuntimeAsyncClient) {
+
+ var chatModel = BedrockProxyChatModel.builder()
+ .credentialsProvider(credentialsProvider)
+ .region(regionProvider.getRegion())
+ .timeout(connectionProperties.getTimeout())
+ .defaultOptions(chatProperties.getOptions())
+ .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
+ .toolCallingManager(toolCallingManager)
+ .bedrockRuntimeClient(bedrockRuntimeClient.getIfAvailable())
+ .bedrockRuntimeAsyncClient(bedrockRuntimeAsyncClient.getIfAvailable())
+ .build();
+
+ observationConvention.ifAvailable(chatModel::setObservationConvention);
+
+ return chatModel;
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) {
+ DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver();
+ manager.setApplicationContext(context);
+ return manager;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatProperties.java
new file mode 100644
index 000000000..0428a9c90
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatProperties.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2024-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.bedrock.autoconfigure.converse;
+
+import org.springframework.ai.model.tool.ToolCallingChatOptions;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+import org.springframework.util.Assert;
+
+/**
+ * Configuration properties for Bedrock Converse.
+ *
+ * @author Christian Tzolov
+ * @since 1.0.0
+ */
+@ConfigurationProperties(BedrockConverseProxyChatProperties.CONFIG_PREFIX)
+public class BedrockConverseProxyChatProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.bedrock.converse.chat";
+
+ /**
+ * Enable Bedrock Converse chat model.
+ */
+ private boolean enabled = true;
+
+ @NestedConfigurationProperty
+ private ToolCallingChatOptions options = ToolCallingChatOptions.builder()
+ .temperature(0.7)
+ .maxTokens(300)
+ .topK(10)
+ .build();
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public ToolCallingChatOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(ToolCallingChatOptions options) {
+ Assert.notNull(options, "FunctionCallingOptions must not be null");
+ this.options = options;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfiguration.java
new file mode 100644
index 000000000..1cfea4c07
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfiguration.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.bedrock.autoconfigure.titan;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.regions.providers.AwsRegionProvider;
+
+import org.springframework.ai.model.bedrock.autoconfigure.BedrockAwsConnectionConfiguration;
+import org.springframework.ai.model.bedrock.autoconfigure.BedrockAwsConnectionProperties;
+import org.springframework.ai.bedrock.titan.BedrockTitanEmbeddingModel;
+import org.springframework.ai.bedrock.titan.api.TitanEmbeddingBedrockApi;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Import;
+
+/**
+ * {@link AutoConfiguration Auto-configuration} for Bedrock Titan Embedding Model.
+ *
+ * @author Christian Tzolov
+ * @author Wei Jiang
+ * @since 0.8.0
+ */
+@AutoConfiguration
+@ConditionalOnClass(TitanEmbeddingBedrockApi.class)
+@EnableConfigurationProperties({ BedrockTitanEmbeddingProperties.class, BedrockAwsConnectionProperties.class })
+@ConditionalOnProperty(prefix = BedrockTitanEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true")
+@Import(BedrockAwsConnectionConfiguration.class)
+public class BedrockTitanEmbeddingAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class })
+ public TitanEmbeddingBedrockApi titanEmbeddingBedrockApi(AwsCredentialsProvider credentialsProvider,
+ AwsRegionProvider regionProvider, BedrockTitanEmbeddingProperties properties,
+ BedrockAwsConnectionProperties awsProperties, ObjectMapper objectMapper) {
+ return new TitanEmbeddingBedrockApi(properties.getModel(), credentialsProvider, regionProvider.getRegion(),
+ objectMapper, awsProperties.getTimeout());
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnBean(TitanEmbeddingBedrockApi.class)
+ public BedrockTitanEmbeddingModel titanEmbeddingModel(TitanEmbeddingBedrockApi titanEmbeddingApi,
+ BedrockTitanEmbeddingProperties properties) {
+
+ return new BedrockTitanEmbeddingModel(titanEmbeddingApi).withInputType(properties.getInputType());
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingProperties.java
new file mode 100644
index 000000000..48c452ea4
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingProperties.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.bedrock.autoconfigure.titan;
+
+import org.springframework.ai.bedrock.titan.BedrockTitanEmbeddingModel.InputType;
+import org.springframework.ai.bedrock.titan.api.TitanEmbeddingBedrockApi.TitanEmbeddingModel;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Bedrock Titan Embedding autoconfiguration properties.
+ *
+ * @author Christian Tzolov
+ * @since 0.8.0
+ */
+@ConfigurationProperties(BedrockTitanEmbeddingProperties.CONFIG_PREFIX)
+public class BedrockTitanEmbeddingProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.bedrock.titan.embedding";
+
+ /**
+ * Enable Bedrock Titan Embedding Model. False by default.
+ */
+ private boolean enabled = false;
+
+ /**
+ * Bedrock Titan Embedding generative name. Defaults to 'amazon.titan-embed-image-v1'.
+ */
+ private String model = TitanEmbeddingModel.TITAN_EMBED_IMAGE_V1.id();
+
+ /**
+ * Titan Embedding API input types. Could be either text or image (encoded in base64).
+ * Defaults to {@link InputType#IMAGE}.
+ */
+ private InputType inputType = InputType.IMAGE;
+
+ public static String getConfigPrefix() {
+ return CONFIG_PREFIX;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public String getModel() {
+ return this.model;
+ }
+
+ public void setModel(String model) {
+ this.model = model;
+ }
+
+ public InputType getInputType() {
+ return this.inputType;
+ }
+
+ public void setInputType(InputType inputType) {
+ this.inputType = inputType;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientAutoConfiguration.java
new file mode 100644
index 000000000..89d790a9f
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientAutoConfiguration.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.chat.client.autoconfigure;
+
+import io.micrometer.observation.ObservationRegistry;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.chat.client.ChatClientCustomizer;
+import org.springframework.ai.chat.client.observation.ChatClientInputContentObservationFilter;
+import org.springframework.ai.chat.client.observation.ChatClientObservationConvention;
+import org.springframework.ai.chat.model.ChatModel;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Scope;
+
+/**
+ * {@link EnableAutoConfiguration Auto-configuration} for {@link ChatClient}.
+ *
+ * This will produce a {@link ChatClient.Builder ChatClient.Builder} bean with the
+ * {@code prototype} scope, meaning each injection point will receive a newly cloned
+ * instance of the builder.
+ *
+ * @author Christian Tzolov
+ * @author Mark Pollack
+ * @author Josh Long
+ * @author Arjen Poutsma
+ * @author Thomas Vitale
+ * @since 1.0.0
+ */
+@AutoConfiguration
+@ConditionalOnClass(ChatClient.class)
+@EnableConfigurationProperties(ChatClientBuilderProperties.class)
+@ConditionalOnProperty(prefix = ChatClientBuilderProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+public class ChatClientAutoConfiguration {
+
+ private static final Logger logger = LoggerFactory.getLogger(ChatClientAutoConfiguration.class);
+
+ @Bean
+ @ConditionalOnMissingBean
+ ChatClientBuilderConfigurer chatClientBuilderConfigurer(ObjectProvider customizerProvider) {
+ ChatClientBuilderConfigurer configurer = new ChatClientBuilderConfigurer();
+ configurer.setChatClientCustomizers(customizerProvider.orderedStream().toList());
+ return configurer;
+ }
+
+ @Bean
+ @Scope("prototype")
+ @ConditionalOnMissingBean
+ ChatClient.Builder chatClientBuilder(ChatClientBuilderConfigurer chatClientBuilderConfigurer, ChatModel chatModel,
+ ObjectProvider observationRegistry,
+ ObjectProvider observationConvention) {
+
+ ChatClient.Builder builder = ChatClient.builder(chatModel,
+ observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP),
+ observationConvention.getIfUnique(() -> null));
+ return chatClientBuilderConfigurer.configure(builder);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = ChatClientBuilderProperties.CONFIG_PREFIX + ".observations", name = "include-input",
+ havingValue = "true")
+ ChatClientInputContentObservationFilter chatClientInputContentObservationFilter() {
+ logger.warn(
+ "You have enabled the inclusion of the input content in the observations, with the risk of exposing sensitive or private information. Please, be careful!");
+ return new ChatClientInputContentObservationFilter();
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientBuilderConfigurer.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientBuilderConfigurer.java
new file mode 100644
index 000000000..f0448058c
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientBuilderConfigurer.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.chat.client.autoconfigure;
+
+import java.util.List;
+
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.chat.client.ChatClientCustomizer;
+
+/**
+ * Builder for configuring a {@link ChatClient.Builder}.
+ *
+ * @author Christian Tzolov
+ * @author Mark Pollack
+ * @author Josh Long
+ * @author Arjen Poutsma
+ * @since 1.0.0 M1
+ */
+public class ChatClientBuilderConfigurer {
+
+ private List customizers;
+
+ void setChatClientCustomizers(List customizers) {
+ this.customizers = customizers;
+ }
+
+ /**
+ * Configure the specified {@link ChatClient.Builder}. The builder can be further
+ * tuned and default settings can be overridden.
+ * @param builder the {@link ChatClient.Builder} instance to configure
+ * @return the configured builder
+ */
+ public ChatClient.Builder configure(ChatClient.Builder builder) {
+ applyCustomizers(builder);
+ return builder;
+ }
+
+ private void applyCustomizers(ChatClient.Builder builder) {
+ if (this.customizers != null) {
+ for (ChatClientCustomizer customizer : this.customizers) {
+ customizer.customize(builder);
+ }
+ }
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientBuilderProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientBuilderProperties.java
new file mode 100644
index 000000000..0ca9023d7
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientBuilderProperties.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.chat.client.autoconfigure;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Configuration properties for the chat client builder.
+ *
+ * @author Christian Tzolov
+ * @author Mark Pollack
+ * @author Josh Long
+ * @author Arjen Poutsma
+ * @since 1.0.0
+ */
+@ConfigurationProperties(ChatClientBuilderProperties.CONFIG_PREFIX)
+public class ChatClientBuilderProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.chat.client";
+
+ /**
+ * Enable chat client builder.
+ */
+ private boolean enabled = true;
+
+ private Observations observations = new Observations();
+
+ public Observations getObservations() {
+ return this.observations;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public static class Observations {
+
+ /**
+ * Whether to include the input content in the observations.
+ */
+ private boolean includeInput = false;
+
+ public boolean isIncludeInput() {
+ return this.includeInput;
+ }
+
+ public void setIncludeInput(boolean includeCompletion) {
+ this.includeInput = includeCompletion;
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/memory/autoconfigure/CommonChatMemoryProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/memory/autoconfigure/CommonChatMemoryProperties.java
new file mode 100644
index 000000000..00ea424c4
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/memory/autoconfigure/CommonChatMemoryProperties.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.chat.memory.autoconfigure;
+
+/**
+ * Configuration properties for the common chat memory.
+ *
+ * @author Mick Semb Wever
+ * @since 1.0.0
+ */
+public class CommonChatMemoryProperties {
+
+ private boolean initializeSchema = true;
+
+ public boolean isInitializeSchema() {
+ return this.initializeSchema;
+ }
+
+ public void setInitializeSchema(boolean initializeSchema) {
+ this.initializeSchema = initializeSchema;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryAutoConfiguration.java
new file mode 100644
index 000000000..937a49beb
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryAutoConfiguration.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.chat.memory.autoconfigure.cassandra;
+
+import com.datastax.oss.driver.api.core.CqlSession;
+
+import org.springframework.ai.chat.memory.cassandra.CassandraChatMemory;
+import org.springframework.ai.chat.memory.cassandra.CassandraChatMemoryConfig;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * {@link AutoConfiguration Auto-configuration} for {@link CassandraChatMemory}.
+ *
+ * @author Mick Semb Wever
+ * @author Jihoon Kim
+ * @since 1.0.0
+ */
+@AutoConfiguration(after = CassandraAutoConfiguration.class)
+@ConditionalOnClass({ CassandraChatMemory.class, CqlSession.class })
+@EnableConfigurationProperties(CassandraChatMemoryProperties.class)
+public class CassandraChatMemoryAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ public CassandraChatMemory chatMemory(CassandraChatMemoryProperties properties, CqlSession cqlSession) {
+
+ var builder = CassandraChatMemoryConfig.builder().withCqlSession(cqlSession);
+
+ builder = builder.withKeyspaceName(properties.getKeyspace())
+ .withTableName(properties.getTable())
+ .withAssistantColumnName(properties.getAssistantColumn())
+ .withUserColumnName(properties.getUserColumn());
+
+ if (!properties.isInitializeSchema()) {
+ builder = builder.disallowSchemaChanges();
+ }
+ if (null != properties.getTimeToLive()) {
+ builder = builder.withTimeToLive(properties.getTimeToLive());
+ }
+
+ return CassandraChatMemory.create(builder.build());
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryProperties.java
new file mode 100644
index 000000000..14ae105ab
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryProperties.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.chat.memory.autoconfigure.cassandra;
+
+import java.time.Duration;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.ai.model.chat.memory.autoconfigure.CommonChatMemoryProperties;
+import org.springframework.ai.chat.memory.cassandra.CassandraChatMemoryConfig;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.lang.Nullable;
+
+/**
+ * Configuration properties for Cassandra chat memory.
+ *
+ * @author Mick Semb Wever
+ * @author Jihoon Kim
+ * @since 1.0.0
+ */
+@ConfigurationProperties(CassandraChatMemoryProperties.CONFIG_PREFIX)
+public class CassandraChatMemoryProperties extends CommonChatMemoryProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.chat.memory.cassandra";
+
+ private static final Logger logger = LoggerFactory.getLogger(CassandraChatMemoryProperties.class);
+
+ private String keyspace = CassandraChatMemoryConfig.DEFAULT_KEYSPACE_NAME;
+
+ private String table = CassandraChatMemoryConfig.DEFAULT_TABLE_NAME;
+
+ private String assistantColumn = CassandraChatMemoryConfig.DEFAULT_ASSISTANT_COLUMN_NAME;
+
+ private String userColumn = CassandraChatMemoryConfig.DEFAULT_USER_COLUMN_NAME;
+
+ private Duration timeToLive = null;
+
+ public String getKeyspace() {
+ return this.keyspace;
+ }
+
+ public void setKeyspace(String keyspace) {
+ this.keyspace = keyspace;
+ }
+
+ public String getTable() {
+ return this.table;
+ }
+
+ public void setTable(String table) {
+ this.table = table;
+ }
+
+ public String getAssistantColumn() {
+ return this.assistantColumn;
+ }
+
+ public void setAssistantColumn(String assistantColumn) {
+ this.assistantColumn = assistantColumn;
+ }
+
+ public String getUserColumn() {
+ return this.userColumn;
+ }
+
+ public void setUserColumn(String userColumn) {
+ this.userColumn = userColumn;
+ }
+
+ @Nullable
+ public Duration getTimeToLive() {
+ return this.timeToLive;
+ }
+
+ public void setTimeToLive(Duration timeToLive) {
+ this.timeToLive = timeToLive;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatAutoConfiguration.java
new file mode 100644
index 000000000..a79a6517d
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatAutoConfiguration.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.huggingface.autoconfigure;
+
+import org.springframework.ai.huggingface.HuggingfaceChatModel;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+
+@AutoConfiguration
+@ConditionalOnClass(HuggingfaceChatModel.class)
+@EnableConfigurationProperties(HuggingfaceChatProperties.class)
+public class HuggingfaceChatAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = HuggingfaceChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+ public HuggingfaceChatModel huggingfaceChatModel(HuggingfaceChatProperties huggingfaceChatProperties) {
+ return new HuggingfaceChatModel(huggingfaceChatProperties.getApiKey(), huggingfaceChatProperties.getUrl());
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatProperties.java
new file mode 100644
index 000000000..371df322f
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatProperties.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.huggingface.autoconfigure;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Configuration properties for Hugging Face chat model.
+ *
+ * @author Christian Tzolov
+ * @author Josh Long
+ * @author Mark Pollack
+ * @author Thomas Vitale
+ */
+@ConfigurationProperties(HuggingfaceChatProperties.CONFIG_PREFIX)
+public class HuggingfaceChatProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.huggingface.chat";
+
+ /**
+ * API Key to authenticate with the Inference Endpoint.
+ */
+ private String apiKey;
+
+ /**
+ * URL of the Inference Endpoint.
+ */
+ private String url;
+
+ /**
+ * Enable Hugging Face chat model.
+ */
+ private boolean enabled = true;
+
+ public String getApiKey() {
+ return this.apiKey;
+ }
+
+ public void setApiKey(String apiKey) {
+ this.apiKey = apiKey;
+ }
+
+ public String getUrl() {
+ return this.url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfiguration.java
new file mode 100644
index 000000000..caea39372
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfiguration.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.minimax.autoconfigure;
+
+import java.util.List;
+
+import io.micrometer.observation.ObservationRegistry;
+
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.chat.observation.ChatModelObservationConvention;
+import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
+import org.springframework.ai.minimax.MiniMaxChatModel;
+import org.springframework.ai.minimax.MiniMaxEmbeddingModel;
+import org.springframework.ai.minimax.api.MiniMaxApi;
+import org.springframework.ai.model.function.DefaultFunctionCallbackResolver;
+import org.springframework.ai.model.function.FunctionCallback;
+import org.springframework.ai.model.function.FunctionCallbackResolver;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.retry.support.RetryTemplate;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.ResponseErrorHandler;
+import org.springframework.web.client.RestClient;
+
+/**
+ * {@link AutoConfiguration Auto-configuration} for MiniMax Chat and Embedding Models.
+ *
+ * @author Geng Rong
+ */
+@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class })
+@ConditionalOnClass(MiniMaxApi.class)
+@EnableConfigurationProperties({ MiniMaxConnectionProperties.class, MiniMaxChatProperties.class,
+ MiniMaxEmbeddingProperties.class })
+public class MiniMaxAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = MiniMaxChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+ public MiniMaxChatModel miniMaxChatModel(MiniMaxConnectionProperties commonProperties,
+ MiniMaxChatProperties chatProperties, ObjectProvider restClientBuilderProvider,
+ List toolFunctionCallbacks, FunctionCallbackResolver functionCallbackResolver,
+ RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler,
+ ObjectProvider observationRegistry,
+ ObjectProvider observationConvention) {
+
+ var miniMaxApi = miniMaxApi(chatProperties.getBaseUrl(), commonProperties.getBaseUrl(),
+ chatProperties.getApiKey(), commonProperties.getApiKey(),
+ restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler);
+
+ var chatModel = new MiniMaxChatModel(miniMaxApi, chatProperties.getOptions(), functionCallbackResolver,
+ toolFunctionCallbacks, retryTemplate, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
+
+ observationConvention.ifAvailable(chatModel::setObservationConvention);
+ return chatModel;
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = MiniMaxEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+ public MiniMaxEmbeddingModel miniMaxEmbeddingModel(MiniMaxConnectionProperties commonProperties,
+ MiniMaxEmbeddingProperties embeddingProperties,
+ ObjectProvider restClientBuilderProvider, RetryTemplate retryTemplate,
+ ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry,
+ ObjectProvider observationConvention) {
+
+ var miniMaxApi = miniMaxApi(embeddingProperties.getBaseUrl(), commonProperties.getBaseUrl(),
+ embeddingProperties.getApiKey(), commonProperties.getApiKey(),
+ restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler);
+
+ var embeddingModel = new MiniMaxEmbeddingModel(miniMaxApi, embeddingProperties.getMetadataMode(),
+ embeddingProperties.getOptions(), retryTemplate,
+ observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
+
+ observationConvention.ifAvailable(embeddingModel::setObservationConvention);
+
+ return embeddingModel;
+ }
+
+ private MiniMaxApi miniMaxApi(String baseUrl, String commonBaseUrl, String apiKey, String commonApiKey,
+ RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {
+
+ String resolvedBaseUrl = StringUtils.hasText(baseUrl) ? baseUrl : commonBaseUrl;
+ Assert.hasText(resolvedBaseUrl, "MiniMax base URL must be set");
+
+ String resolvedApiKey = StringUtils.hasText(apiKey) ? apiKey : commonApiKey;
+ Assert.hasText(resolvedApiKey, "MiniMax API key must be set");
+
+ return new MiniMaxApi(resolvedBaseUrl, resolvedApiKey, restClientBuilder, responseErrorHandler);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) {
+ DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver();
+ manager.setApplicationContext(context);
+ return manager;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxChatProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxChatProperties.java
new file mode 100644
index 000000000..a04b1655c
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxChatProperties.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.minimax.autoconfigure;
+
+import org.springframework.ai.minimax.MiniMaxChatOptions;
+import org.springframework.ai.minimax.api.MiniMaxApi;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+/**
+ * Configuration properties for MiniMax chat model.
+ *
+ * @author Geng Rong
+ */
+@ConfigurationProperties(MiniMaxChatProperties.CONFIG_PREFIX)
+public class MiniMaxChatProperties extends MiniMaxParentProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.minimax.chat";
+
+ public static final String DEFAULT_CHAT_MODEL = MiniMaxApi.ChatModel.ABAB_5_5_Chat.value;
+
+ private static final Double DEFAULT_TEMPERATURE = 0.7;
+
+ /**
+ * Enable MiniMax chat model.
+ */
+ private boolean enabled = true;
+
+ @NestedConfigurationProperty
+ private MiniMaxChatOptions options = MiniMaxChatOptions.builder()
+ .model(DEFAULT_CHAT_MODEL)
+ .temperature(DEFAULT_TEMPERATURE)
+ .build();
+
+ public MiniMaxChatOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(MiniMaxChatOptions options) {
+ this.options = options;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxConnectionProperties.java
new file mode 100644
index 000000000..e41162a39
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxConnectionProperties.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.minimax.autoconfigure;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties(MiniMaxConnectionProperties.CONFIG_PREFIX)
+public class MiniMaxConnectionProperties extends MiniMaxParentProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.minimax";
+
+ public static final String DEFAULT_BASE_URL = "https://api.minimax.chat";
+
+ public MiniMaxConnectionProperties() {
+ super.setBaseUrl(DEFAULT_BASE_URL);
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxEmbeddingProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxEmbeddingProperties.java
new file mode 100644
index 000000000..465a573f9
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxEmbeddingProperties.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.minimax.autoconfigure;
+
+import org.springframework.ai.document.MetadataMode;
+import org.springframework.ai.minimax.MiniMaxEmbeddingOptions;
+import org.springframework.ai.minimax.api.MiniMaxApi;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+/**
+ * Configuration properties for MiniMax embedding model.
+ *
+ * @author Geng Rong
+ */
+@ConfigurationProperties(MiniMaxEmbeddingProperties.CONFIG_PREFIX)
+public class MiniMaxEmbeddingProperties extends MiniMaxParentProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.minimax.embedding";
+
+ public static final String DEFAULT_EMBEDDING_MODEL = MiniMaxApi.EmbeddingModel.Embo_01.value;
+
+ /**
+ * Enable MiniMax embedding model.
+ */
+ private boolean enabled = true;
+
+ private MetadataMode metadataMode = MetadataMode.EMBED;
+
+ @NestedConfigurationProperty
+ private MiniMaxEmbeddingOptions options = MiniMaxEmbeddingOptions.builder().model(DEFAULT_EMBEDDING_MODEL).build();
+
+ public MiniMaxEmbeddingOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(MiniMaxEmbeddingOptions options) {
+ this.options = options;
+ }
+
+ public MetadataMode getMetadataMode() {
+ return this.metadataMode;
+ }
+
+ public void setMetadataMode(MetadataMode metadataMode) {
+ this.metadataMode = metadataMode;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxParentProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxParentProperties.java
new file mode 100644
index 000000000..2927c6ea3
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxParentProperties.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.minimax.autoconfigure;
+
+/**
+ * @author Geng Rong
+ */
+class MiniMaxParentProperties {
+
+ private String apiKey;
+
+ private String baseUrl;
+
+ public String getApiKey() {
+ return this.apiKey;
+ }
+
+ public void setApiKey(String apiKey) {
+ this.apiKey = apiKey;
+ }
+
+ public String getBaseUrl() {
+ return this.baseUrl;
+ }
+
+ public void setBaseUrl(String baseUrl) {
+ this.baseUrl = baseUrl;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfiguration.java
new file mode 100644
index 000000000..b58a75200
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfiguration.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2023-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.mistralai.autoconfigure;
+
+import io.micrometer.observation.ObservationRegistry;
+
+import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration;
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.chat.observation.ChatModelObservationConvention;
+import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
+import org.springframework.ai.mistralai.MistralAiChatModel;
+import org.springframework.ai.mistralai.MistralAiEmbeddingModel;
+import org.springframework.ai.mistralai.api.MistralAiApi;
+import org.springframework.ai.model.function.DefaultFunctionCallbackResolver;
+import org.springframework.ai.model.function.FunctionCallbackResolver;
+import org.springframework.ai.model.tool.ToolCallingManager;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.retry.support.RetryTemplate;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.ResponseErrorHandler;
+import org.springframework.web.client.RestClient;
+
+/**
+ * {@link AutoConfiguration Auto-configuration} for Mistral AI.
+ *
+ * @author Ricken Bazolo
+ * @author Christian Tzolov
+ * @author Thomas Vitale
+ * @since 0.8.1
+ */
+@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class,
+ ToolCallingAutoConfiguration.class })
+@EnableConfigurationProperties({ MistralAiEmbeddingProperties.class, MistralAiCommonProperties.class,
+ MistralAiChatProperties.class })
+@ConditionalOnClass(MistralAiApi.class)
+@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class,
+ ToolCallingAutoConfiguration.class })
+public class MistralAiAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = MistralAiEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+ public MistralAiEmbeddingModel mistralAiEmbeddingModel(MistralAiCommonProperties commonProperties,
+ MistralAiEmbeddingProperties embeddingProperties,
+ ObjectProvider restClientBuilderProvider, RetryTemplate retryTemplate,
+ ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry,
+ ObjectProvider observationConvention) {
+
+ var mistralAiApi = mistralAiApi(embeddingProperties.getApiKey(), commonProperties.getApiKey(),
+ embeddingProperties.getBaseUrl(), commonProperties.getBaseUrl(),
+ restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler);
+
+ var embeddingModel = new MistralAiEmbeddingModel(mistralAiApi, embeddingProperties.getMetadataMode(),
+ embeddingProperties.getOptions(), retryTemplate,
+ observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
+
+ observationConvention.ifAvailable(embeddingModel::setObservationConvention);
+
+ return embeddingModel;
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = MistralAiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+ public MistralAiChatModel mistralAiChatModel(MistralAiCommonProperties commonProperties,
+ MistralAiChatProperties chatProperties, ObjectProvider restClientBuilderProvider,
+ ToolCallingManager toolCallingManager, RetryTemplate retryTemplate,
+ ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry,
+ ObjectProvider observationConvention) {
+
+ var mistralAiApi = mistralAiApi(chatProperties.getApiKey(), commonProperties.getApiKey(),
+ chatProperties.getBaseUrl(), commonProperties.getBaseUrl(),
+ restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler);
+
+ var chatModel = MistralAiChatModel.builder()
+ .mistralAiApi(mistralAiApi)
+ .defaultOptions(chatProperties.getOptions())
+ .toolCallingManager(toolCallingManager)
+ .retryTemplate(retryTemplate)
+ .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
+ .build();
+
+ observationConvention.ifAvailable(chatModel::setObservationConvention);
+
+ return chatModel;
+ }
+
+ private MistralAiApi mistralAiApi(String apiKey, String commonApiKey, String baseUrl, String commonBaseUrl,
+ RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {
+
+ var resolvedApiKey = StringUtils.hasText(apiKey) ? apiKey : commonApiKey;
+ var resoledBaseUrl = StringUtils.hasText(baseUrl) ? baseUrl : commonBaseUrl;
+
+ Assert.hasText(resolvedApiKey, "Mistral API key must be set");
+ Assert.hasText(resoledBaseUrl, "Mistral base URL must be set");
+
+ return new MistralAiApi(resoledBaseUrl, resolvedApiKey, restClientBuilder, responseErrorHandler);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) {
+ DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver();
+ manager.setApplicationContext(context);
+ return manager;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mistralai/MistralAiChatProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiChatProperties.java
similarity index 97%
rename from spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mistralai/MistralAiChatProperties.java
rename to spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiChatProperties.java
index 746529d82..19a782a8e 100644
--- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mistralai/MistralAiChatProperties.java
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiChatProperties.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package org.springframework.ai.autoconfigure.mistralai;
+package org.springframework.ai.model.mistralai.autoconfigure;
import org.springframework.ai.mistralai.MistralAiChatOptions;
import org.springframework.ai.mistralai.api.MistralAiApi;
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiCommonProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiCommonProperties.java
new file mode 100644
index 000000000..3d1a90bfc
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiCommonProperties.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.mistralai.autoconfigure;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Common properties for Mistral AI.
+ *
+ * @author Ricken Bazolo
+ * @author Christian Tzolov
+ * @since 0.8.1
+ */
+@ConfigurationProperties(MistralAiCommonProperties.CONFIG_PREFIX)
+public class MistralAiCommonProperties extends MistralAiParentProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.mistralai";
+
+ public static final String DEFAULT_BASE_URL = "https://api.mistral.ai";
+
+ public MistralAiCommonProperties() {
+ super.setBaseUrl(DEFAULT_BASE_URL);
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiEmbeddingProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiEmbeddingProperties.java
new file mode 100644
index 000000000..f8c3498be
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiEmbeddingProperties.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.mistralai.autoconfigure;
+
+import org.springframework.ai.document.MetadataMode;
+import org.springframework.ai.mistralai.MistralAiEmbeddingOptions;
+import org.springframework.ai.mistralai.api.MistralAiApi;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+/**
+ * Configuration properties for MistralAI embedding model.
+ *
+ * @author Ricken Bazolo
+ * @since 0.8.1
+ */
+@ConfigurationProperties(MistralAiEmbeddingProperties.CONFIG_PREFIX)
+public class MistralAiEmbeddingProperties extends MistralAiParentProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.mistralai.embedding";
+
+ public static final String DEFAULT_EMBEDDING_MODEL = MistralAiApi.EmbeddingModel.EMBED.getValue();
+
+ public static final String DEFAULT_ENCODING_FORMAT = "float";
+
+ public MetadataMode metadataMode = MetadataMode.EMBED;
+
+ /**
+ * Enable MistralAI embedding model.
+ */
+ private boolean enabled = true;
+
+ @NestedConfigurationProperty
+ private MistralAiEmbeddingOptions options = MistralAiEmbeddingOptions.builder()
+ .withModel(DEFAULT_EMBEDDING_MODEL)
+ .withEncodingFormat(DEFAULT_ENCODING_FORMAT)
+ .build();
+
+ public MistralAiEmbeddingProperties() {
+ super.setBaseUrl(MistralAiCommonProperties.DEFAULT_BASE_URL);
+ }
+
+ public MistralAiEmbeddingOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(MistralAiEmbeddingOptions options) {
+ this.options = options;
+ }
+
+ public MetadataMode getMetadataMode() {
+ return this.metadataMode;
+ }
+
+ public void setMetadataMode(MetadataMode metadataMode) {
+ this.metadataMode = metadataMode;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiParentProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiParentProperties.java
new file mode 100644
index 000000000..0da844cc2
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiParentProperties.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.mistralai.autoconfigure;
+
+/**
+ * Parent properties for Mistral AI.
+ *
+ * @author Ricken Bazolo
+ * @since 0.8.1
+ */
+public class MistralAiParentProperties {
+
+ private String apiKey;
+
+ private String baseUrl;
+
+ public String getApiKey() {
+ return this.apiKey;
+ }
+
+ public void setApiKey(String apiKey) {
+ this.apiKey = apiKey;
+ }
+
+ public String getBaseUrl() {
+ return this.baseUrl;
+ }
+
+ public void setBaseUrl(String baseUrl) {
+ this.baseUrl = baseUrl;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotAutoConfiguration.java
new file mode 100644
index 000000000..51674fc76
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotAutoConfiguration.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.moonshot.autoconfigure;
+
+import java.util.List;
+
+import io.micrometer.observation.ObservationRegistry;
+
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.chat.observation.ChatModelObservationConvention;
+import org.springframework.ai.model.function.DefaultFunctionCallbackResolver;
+import org.springframework.ai.model.function.FunctionCallback;
+import org.springframework.ai.model.function.FunctionCallbackResolver;
+import org.springframework.ai.moonshot.MoonshotChatModel;
+import org.springframework.ai.moonshot.api.MoonshotApi;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.retry.support.RetryTemplate;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.ResponseErrorHandler;
+import org.springframework.web.client.RestClient;
+
+/**
+ * {@link AutoConfiguration Auto-configuration} for Moonshot Chat Model.
+ *
+ * @author Geng Rong
+ */
+@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class })
+@EnableConfigurationProperties({ MoonshotCommonProperties.class, MoonshotChatProperties.class })
+@ConditionalOnClass(MoonshotApi.class)
+public class MoonshotAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = MoonshotChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+ public MoonshotChatModel moonshotChatModel(MoonshotCommonProperties commonProperties,
+ MoonshotChatProperties chatProperties, ObjectProvider restClientBuilderProvider,
+ List toolFunctionCallbacks, FunctionCallbackResolver functionCallbackResolver,
+ RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler,
+ ObjectProvider observationRegistry,
+ ObjectProvider observationConvention) {
+
+ var moonshotApi = moonshotApi(chatProperties.getApiKey(), commonProperties.getApiKey(),
+ chatProperties.getBaseUrl(), commonProperties.getBaseUrl(),
+ restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler);
+
+ var chatModel = new MoonshotChatModel(moonshotApi, chatProperties.getOptions(), functionCallbackResolver,
+ toolFunctionCallbacks, retryTemplate, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
+
+ observationConvention.ifAvailable(chatModel::setObservationConvention);
+ return chatModel;
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) {
+ DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver();
+ manager.setApplicationContext(context);
+ return manager;
+ }
+
+ private MoonshotApi moonshotApi(String apiKey, String commonApiKey, String baseUrl, String commonBaseUrl,
+ RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {
+
+ var resolvedApiKey = StringUtils.hasText(apiKey) ? apiKey : commonApiKey;
+ var resoledBaseUrl = StringUtils.hasText(baseUrl) ? baseUrl : commonBaseUrl;
+
+ Assert.hasText(resolvedApiKey, "Moonshot API key must be set");
+ Assert.hasText(resoledBaseUrl, "Moonshot base URL must be set");
+
+ return new MoonshotApi(resoledBaseUrl, resolvedApiKey, restClientBuilder, responseErrorHandler);
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotChatProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotChatProperties.java
new file mode 100644
index 000000000..3933eba3e
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotChatProperties.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.moonshot.autoconfigure;
+
+import org.springframework.ai.moonshot.MoonshotChatOptions;
+import org.springframework.ai.moonshot.api.MoonshotApi;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+/**
+ * Configuration properties for Moonshot chat client.
+ *
+ * @author Geng Rong
+ * @author Alexandros Pappas
+ */
+@ConfigurationProperties(MoonshotChatProperties.CONFIG_PREFIX)
+public class MoonshotChatProperties extends MoonshotParentProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.moonshot.chat";
+
+ public static final String DEFAULT_CHAT_MODEL = MoonshotApi.ChatModel.MOONSHOT_V1_8K.getValue();
+
+ private static final Double DEFAULT_TEMPERATURE = 0.7;
+
+ /**
+ * Enable Moonshot chat client.
+ */
+ private boolean enabled = true;
+
+ @NestedConfigurationProperty
+ private MoonshotChatOptions options = MoonshotChatOptions.builder()
+ .model(DEFAULT_CHAT_MODEL)
+ .temperature(DEFAULT_TEMPERATURE)
+ .build();
+
+ public MoonshotChatOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(MoonshotChatOptions options) {
+ this.options = options;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotCommonProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotCommonProperties.java
new file mode 100644
index 000000000..7d9461f91
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotCommonProperties.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.moonshot.autoconfigure;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Parent properties for Moonshot.
+ *
+ * @author Geng Rong
+ */
+@ConfigurationProperties(MoonshotCommonProperties.CONFIG_PREFIX)
+public class MoonshotCommonProperties extends MoonshotParentProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.moonshot";
+
+ public static final String DEFAULT_BASE_URL = "https://api.moonshot.cn";
+
+ public MoonshotCommonProperties() {
+ super.setBaseUrl(DEFAULT_BASE_URL);
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotParentProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotParentProperties.java
new file mode 100644
index 000000000..23a07ced3
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotParentProperties.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.moonshot.autoconfigure;
+
+/**
+ * Parent properties for Moonshot.
+ *
+ * @author Geng Rong
+ */
+public class MoonshotParentProperties {
+
+ private String apiKey;
+
+ private String baseUrl;
+
+ public String getApiKey() {
+ return this.apiKey;
+ }
+
+ public void setApiKey(String apiKey) {
+ this.apiKey = apiKey;
+ }
+
+ public String getBaseUrl() {
+ return this.baseUrl;
+ }
+
+ public void setBaseUrl(String baseUrl) {
+ this.baseUrl = baseUrl;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCICohereChatModelProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCICohereChatModelProperties.java
new file mode 100644
index 000000000..ebed22636
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCICohereChatModelProperties.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.oci.genai.autoconfigure;
+
+import org.springframework.ai.oci.cohere.OCICohereChatOptions;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+/**
+ * Configuration properties for OCI Cohere chat model.
+ *
+ * @author Anders Swanson
+ */
+@ConfigurationProperties(OCICohereChatModelProperties.CONFIG_PREFIX)
+public class OCICohereChatModelProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.oci.genai.cohere.chat";
+
+ private static final String DEFAULT_SERVING_MODE = ServingMode.ON_DEMAND.getMode();
+
+ private static final Double DEFAULT_TEMPERATURE = 0.7;
+
+ private boolean enabled;
+
+ @NestedConfigurationProperty
+ private OCICohereChatOptions options = OCICohereChatOptions.builder()
+ .servingMode(DEFAULT_SERVING_MODE)
+ .temperature(DEFAULT_TEMPERATURE)
+ .build();
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public OCICohereChatOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(OCICohereChatOptions options) {
+ this.options = options;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIConnectionProperties.java
new file mode 100644
index 000000000..69f33d910
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIConnectionProperties.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.oci.genai.autoconfigure;
+
+import java.nio.file.Paths;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.util.StringUtils;
+
+/**
+ * Configuration properties for OCI connection.
+ *
+ * @author Anders Swanson
+ */
+@ConfigurationProperties(OCIConnectionProperties.CONFIG_PREFIX)
+public class OCIConnectionProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.oci.genai";
+
+ private static final String DEFAULT_PROFILE = "DEFAULT";
+
+ private AuthenticationType authenticationType = AuthenticationType.FILE;
+
+ private String profile;
+
+ private String file = Paths.get(System.getProperty("user.home"), ".oci", "config").toString();
+
+ private String tenantId;
+
+ private String userId;
+
+ private String fingerprint;
+
+ private String privateKey;
+
+ private String passPhrase;
+
+ private String region = "us-chicago-1";
+
+ private String endpoint;
+
+ public String getRegion() {
+ return this.region;
+ }
+
+ public void setRegion(String region) {
+ this.region = region;
+ }
+
+ public String getPassPhrase() {
+ return this.passPhrase;
+ }
+
+ public void setPassPhrase(String passPhrase) {
+ this.passPhrase = passPhrase;
+ }
+
+ public String getPrivateKey() {
+ return this.privateKey;
+ }
+
+ public void setPrivateKey(String privateKey) {
+ this.privateKey = privateKey;
+ }
+
+ public String getFingerprint() {
+ return this.fingerprint;
+ }
+
+ public void setFingerprint(String fingerprint) {
+ this.fingerprint = fingerprint;
+ }
+
+ public String getUserId() {
+ return this.userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
+ public String getTenantId() {
+ return this.tenantId;
+ }
+
+ public void setTenantId(String tenantId) {
+ this.tenantId = tenantId;
+ }
+
+ public String getFile() {
+ return this.file;
+ }
+
+ public void setFile(String file) {
+ this.file = file;
+ }
+
+ public String getProfile() {
+ return StringUtils.hasText(this.profile) ? this.profile : DEFAULT_PROFILE;
+ }
+
+ public void setProfile(String profile) {
+ this.profile = profile;
+ }
+
+ public AuthenticationType getAuthenticationType() {
+ return this.authenticationType;
+ }
+
+ public void setAuthenticationType(AuthenticationType authenticationType) {
+ this.authenticationType = authenticationType;
+ }
+
+ public String getEndpoint() {
+ return this.endpoint;
+ }
+
+ public void setEndpoint(String endpoint) {
+ this.endpoint = endpoint;
+ }
+
+ public enum AuthenticationType {
+
+ FILE("file"), INSTANCE_PRINCIPAL("instance-principal"), WORKLOAD_IDENTITY("workload-identity"),
+ SIMPLE("simple");
+
+ private final String authType;
+
+ AuthenticationType(String authType) {
+ this.authType = authType;
+ }
+
+ public String getAuthType() {
+ return this.authType;
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIEmbeddingModelProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIEmbeddingModelProperties.java
new file mode 100644
index 000000000..3ba1ea993
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIEmbeddingModelProperties.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.oci.genai.autoconfigure;
+
+import com.oracle.bmc.generativeaiinference.model.EmbedTextDetails;
+
+import org.springframework.ai.oci.OCIEmbeddingOptions;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Configuration properties for OCI embedding model.
+ *
+ * @author Anders Swanson
+ */
+@ConfigurationProperties(OCIEmbeddingModelProperties.CONFIG_PREFIX)
+public class OCIEmbeddingModelProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.oci.genai.embedding";
+
+ private ServingMode servingMode = ServingMode.ON_DEMAND;
+
+ private EmbedTextDetails.Truncate truncate = EmbedTextDetails.Truncate.End;
+
+ private String compartment;
+
+ private String model;
+
+ private boolean enabled;
+
+ public OCIEmbeddingOptions getEmbeddingOptions() {
+ return OCIEmbeddingOptions.builder()
+ .compartment(this.compartment)
+ .model(this.model)
+ .servingMode(this.servingMode.getMode())
+ .truncate(this.truncate)
+ .build();
+ }
+
+ public ServingMode getServingMode() {
+ return this.servingMode;
+ }
+
+ public void setServingMode(ServingMode servingMode) {
+ this.servingMode = servingMode;
+ }
+
+ public String getCompartment() {
+ return this.compartment;
+ }
+
+ public void setCompartment(String compartment) {
+ this.compartment = compartment;
+ }
+
+ public String getModel() {
+ return this.model;
+ }
+
+ public void setModel(String model) {
+ this.model = model;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public EmbedTextDetails.Truncate getTruncate() {
+ return this.truncate;
+ }
+
+ public void setTruncate(EmbedTextDetails.Truncate truncate) {
+ this.truncate = truncate;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfiguration.java
new file mode 100644
index 000000000..84d872137
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfiguration.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.oci.genai.autoconfigure;
+
+import java.io.IOException;
+
+import com.oracle.bmc.ClientConfiguration;
+import com.oracle.bmc.Region;
+import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider;
+import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider;
+import com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider;
+import com.oracle.bmc.auth.SimpleAuthenticationDetailsProvider;
+import com.oracle.bmc.auth.SimplePrivateKeySupplier;
+import com.oracle.bmc.auth.okeworkloadidentity.OkeWorkloadIdentityAuthenticationDetailsProvider;
+import com.oracle.bmc.generativeaiinference.GenerativeAiInferenceClient;
+import com.oracle.bmc.retrier.RetryConfiguration;
+import io.micrometer.observation.ObservationRegistry;
+
+import org.springframework.ai.chat.observation.ChatModelObservationConvention;
+import org.springframework.ai.oci.OCIEmbeddingModel;
+import org.springframework.ai.oci.cohere.OCICohereChatModel;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.util.StringUtils;
+
+/**
+ * {@link AutoConfiguration Auto-configuration} for Oracle Cloud Infrastructure Generative
+ * AI.
+ *
+ * @author Anders Swanson
+ */
+@AutoConfiguration
+@ConditionalOnClass({ GenerativeAiInferenceClient.class, OCIEmbeddingModel.class })
+@EnableConfigurationProperties({ OCIConnectionProperties.class, OCIEmbeddingModelProperties.class,
+ OCICohereChatModelProperties.class })
+public class OCIGenAiAutoConfiguration {
+
+ private static BasicAuthenticationDetailsProvider authenticationProvider(OCIConnectionProperties properties)
+ throws IOException {
+ return switch (properties.getAuthenticationType()) {
+ case FILE -> new ConfigFileAuthenticationDetailsProvider(properties.getFile(), properties.getProfile());
+ case INSTANCE_PRINCIPAL -> InstancePrincipalsAuthenticationDetailsProvider.builder().build();
+ case WORKLOAD_IDENTITY -> OkeWorkloadIdentityAuthenticationDetailsProvider.builder().build();
+ case SIMPLE -> SimpleAuthenticationDetailsProvider.builder()
+ .userId(properties.getUserId())
+ .tenantId(properties.getTenantId())
+ .fingerprint(properties.getFingerprint())
+ .privateKeySupplier(new SimplePrivateKeySupplier(properties.getPrivateKey()))
+ .passPhrase(properties.getPassPhrase())
+ .region(Region.valueOf(properties.getRegion()))
+ .build();
+ };
+ }
+
+ @ConditionalOnMissingBean
+ @Bean
+ public GenerativeAiInferenceClient generativeAiInferenceClient(OCIConnectionProperties properties)
+ throws IOException {
+ ClientConfiguration clientConfiguration = ClientConfiguration.builder()
+ .retryConfiguration(RetryConfiguration.SDK_DEFAULT_RETRY_CONFIGURATION)
+ .build();
+ GenerativeAiInferenceClient.Builder builder = GenerativeAiInferenceClient.builder()
+ .configuration(clientConfiguration);
+ if (StringUtils.hasText(properties.getRegion())) {
+ builder.region(Region.valueOf(properties.getRegion()));
+ }
+ if (StringUtils.hasText(properties.getEndpoint())) {
+ builder.endpoint(properties.getEndpoint());
+ }
+ return builder.build(authenticationProvider(properties));
+ }
+
+ @Bean
+ @ConditionalOnProperty(prefix = OCIEmbeddingModelProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+ public OCIEmbeddingModel ociEmbeddingModel(GenerativeAiInferenceClient generativeAiClient,
+ OCIEmbeddingModelProperties properties) {
+ return new OCIEmbeddingModel(generativeAiClient, properties.getEmbeddingOptions());
+ }
+
+ @Bean
+ @ConditionalOnProperty(prefix = OCICohereChatModelProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+ public OCICohereChatModel ociChatModel(GenerativeAiInferenceClient generativeAiClient,
+ OCICohereChatModelProperties properties, ObjectProvider observationRegistry,
+ ObjectProvider observationConvention) {
+ var chatModel = new OCICohereChatModel(generativeAiClient, properties.getOptions(),
+ observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
+ observationConvention.ifAvailable(chatModel::setObservationConvention);
+
+ return chatModel;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/ServingMode.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/ServingMode.java
new file mode 100644
index 000000000..49b09ae13
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/ServingMode.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.oci.genai.autoconfigure;
+
+/**
+ * OCI serving mode.
+ *
+ * @author Anders Swanson
+ */
+public enum ServingMode {
+
+ ON_DEMAND("on-demand"), DEDICATED("dedicated");
+
+ private final String mode;
+
+ ServingMode(String mode) {
+ this.mode = mode;
+ }
+
+ public String getMode() {
+ return this.mode;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaAutoConfiguration.java
new file mode 100644
index 000000000..7d7948f8e
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaAutoConfiguration.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2023-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.ollama.autoconfigure;
+
+import io.micrometer.observation.ObservationRegistry;
+
+import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration;
+import org.springframework.ai.chat.observation.ChatModelObservationConvention;
+import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
+import org.springframework.ai.model.function.DefaultFunctionCallbackResolver;
+import org.springframework.ai.model.function.FunctionCallbackResolver;
+import org.springframework.ai.model.tool.ToolCallingManager;
+import org.springframework.ai.ollama.OllamaChatModel;
+import org.springframework.ai.ollama.OllamaEmbeddingModel;
+import org.springframework.ai.ollama.api.OllamaApi;
+import org.springframework.ai.ollama.management.ModelManagementOptions;
+import org.springframework.ai.ollama.management.PullModelStrategy;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.web.client.RestClient;
+import org.springframework.web.reactive.function.client.WebClient;
+
+/**
+ * {@link AutoConfiguration Auto-configuration} for Ollama Chat Client.
+ *
+ * @author Christian Tzolov
+ * @author Eddú Meléndez
+ * @author Thomas Vitale
+ * @since 0.8.0
+ */
+@AutoConfiguration(after = { RestClientAutoConfiguration.class, ToolCallingAutoConfiguration.class })
+@ConditionalOnClass(OllamaApi.class)
+@EnableConfigurationProperties({ OllamaChatProperties.class, OllamaEmbeddingProperties.class,
+ OllamaConnectionProperties.class, OllamaInitializationProperties.class })
+@ImportAutoConfiguration(classes = { RestClientAutoConfiguration.class, ToolCallingAutoConfiguration.class,
+ WebClientAutoConfiguration.class })
+public class OllamaAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean(OllamaConnectionDetails.class)
+ public PropertiesOllamaConnectionDetails ollamaConnectionDetails(OllamaConnectionProperties properties) {
+ return new PropertiesOllamaConnectionDetails(properties);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public OllamaApi ollamaApi(OllamaConnectionDetails connectionDetails,
+ ObjectProvider restClientBuilderProvider,
+ ObjectProvider webClientBuilderProvider) {
+ return new OllamaApi(connectionDetails.getBaseUrl(),
+ restClientBuilderProvider.getIfAvailable(RestClient::builder),
+ webClientBuilderProvider.getIfAvailable(WebClient::builder));
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = OllamaChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+ public OllamaChatModel ollamaChatModel(OllamaApi ollamaApi, OllamaChatProperties properties,
+ OllamaInitializationProperties initProperties, ToolCallingManager toolCallingManager,
+ ObjectProvider observationRegistry,
+ ObjectProvider observationConvention) {
+ var chatModelPullStrategy = initProperties.getChat().isInclude() ? initProperties.getPullModelStrategy()
+ : PullModelStrategy.NEVER;
+
+ var chatModel = OllamaChatModel.builder()
+ .ollamaApi(ollamaApi)
+ .defaultOptions(properties.getOptions())
+ .toolCallingManager(toolCallingManager)
+ .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
+ .modelManagementOptions(
+ new ModelManagementOptions(chatModelPullStrategy, initProperties.getChat().getAdditionalModels(),
+ initProperties.getTimeout(), initProperties.getMaxRetries()))
+ .build();
+
+ observationConvention.ifAvailable(chatModel::setObservationConvention);
+
+ return chatModel;
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = OllamaEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+ public OllamaEmbeddingModel ollamaEmbeddingModel(OllamaApi ollamaApi, OllamaEmbeddingProperties properties,
+ OllamaInitializationProperties initProperties, ObjectProvider observationRegistry,
+ ObjectProvider observationConvention) {
+ var embeddingModelPullStrategy = initProperties.getEmbedding().isInclude()
+ ? initProperties.getPullModelStrategy() : PullModelStrategy.NEVER;
+
+ var embeddingModel = OllamaEmbeddingModel.builder()
+ .ollamaApi(ollamaApi)
+ .defaultOptions(properties.getOptions())
+ .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
+ .modelManagementOptions(new ModelManagementOptions(embeddingModelPullStrategy,
+ initProperties.getEmbedding().getAdditionalModels(), initProperties.getTimeout(),
+ initProperties.getMaxRetries()))
+ .build();
+
+ observationConvention.ifAvailable(embeddingModel::setObservationConvention);
+
+ return embeddingModel;
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) {
+ DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver();
+ manager.setApplicationContext(context);
+ return manager;
+ }
+
+ static class PropertiesOllamaConnectionDetails implements OllamaConnectionDetails {
+
+ private final OllamaConnectionProperties properties;
+
+ PropertiesOllamaConnectionDetails(OllamaConnectionProperties properties) {
+ this.properties = properties;
+ }
+
+ @Override
+ public String getBaseUrl() {
+ return this.properties.getBaseUrl();
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatProperties.java
new file mode 100644
index 000000000..22569554a
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatProperties.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.ollama.autoconfigure;
+
+import org.springframework.ai.ollama.api.OllamaModel;
+import org.springframework.ai.ollama.api.OllamaOptions;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+/**
+ * Ollama Chat autoconfiguration properties.
+ *
+ * @author Christian Tzolov
+ * @since 0.8.0
+ */
+@ConfigurationProperties(OllamaChatProperties.CONFIG_PREFIX)
+public class OllamaChatProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.ollama.chat";
+
+ /**
+ * Enable Ollama chat model.
+ */
+ private boolean enabled = true;
+
+ /**
+ * Client lever Ollama options. Use this property to configure generative temperature,
+ * topK and topP and alike parameters. The null values are ignored defaulting to the
+ * generative's defaults.
+ */
+ @NestedConfigurationProperty
+ private OllamaOptions options = OllamaOptions.builder().model(OllamaModel.MISTRAL.id()).build();
+
+ public String getModel() {
+ return this.options.getModel();
+ }
+
+ public void setModel(String model) {
+ this.options.setModel(model);
+ }
+
+ public OllamaOptions getOptions() {
+ return this.options;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaConnectionDetails.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaConnectionDetails.java
new file mode 100644
index 000000000..eef0f2af3
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaConnectionDetails.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.ollama.autoconfigure;
+
+import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
+
+/**
+ * Connection details for an Ollama service.
+ *
+ * @author Eddú Meléndez
+ */
+public interface OllamaConnectionDetails extends ConnectionDetails {
+
+ String getBaseUrl();
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaConnectionProperties.java
new file mode 100644
index 000000000..58e7e1c62
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaConnectionProperties.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.ollama.autoconfigure;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Ollama connection autoconfiguration properties.
+ *
+ * @author Christian Tzolov
+ * @since 0.8.0
+ */
+@ConfigurationProperties(OllamaConnectionProperties.CONFIG_PREFIX)
+public class OllamaConnectionProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.ollama";
+
+ /**
+ * Base URL where Ollama API server is running.
+ */
+ private String baseUrl = "http://localhost:11434";
+
+ public String getBaseUrl() {
+ return this.baseUrl;
+ }
+
+ public void setBaseUrl(String baseUrl) {
+ this.baseUrl = baseUrl;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingProperties.java
new file mode 100644
index 000000000..287c52999
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingProperties.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.ollama.autoconfigure;
+
+import org.springframework.ai.ollama.api.OllamaModel;
+import org.springframework.ai.ollama.api.OllamaOptions;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+/**
+ * Ollama Embedding autoconfiguration properties.
+ *
+ * @author Christian Tzolov
+ * @since 0.8.0
+ */
+@ConfigurationProperties(OllamaEmbeddingProperties.CONFIG_PREFIX)
+public class OllamaEmbeddingProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.ollama.embedding";
+
+ /**
+ * Enable Ollama embedding model.
+ */
+ private boolean enabled = true;
+
+ /**
+ * Client lever Ollama options. Use this property to configure generative temperature,
+ * topK and topP and alike parameters. The null values are ignored defaulting to the
+ * generative's defaults.
+ */
+ @NestedConfigurationProperty
+ private OllamaOptions options = OllamaOptions.builder().model(OllamaModel.MXBAI_EMBED_LARGE.id()).build();
+
+ public String getModel() {
+ return this.options.getModel();
+ }
+
+ public void setModel(String model) {
+ this.options.setModel(model);
+ }
+
+ public OllamaOptions getOptions() {
+ return this.options;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaInitializationProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaInitializationProperties.java
new file mode 100644
index 000000000..31f92fc3d
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaInitializationProperties.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.ollama.autoconfigure;
+
+import java.time.Duration;
+import java.util.List;
+
+import org.springframework.ai.ollama.management.PullModelStrategy;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Ollama initialization configuration properties.
+ *
+ * @author Thomas Vitale
+ * @since 1.0.0
+ */
+@ConfigurationProperties(OllamaInitializationProperties.CONFIG_PREFIX)
+public class OllamaInitializationProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.ollama.init";
+
+ /**
+ * Chat models initialization settings.
+ */
+ private final ModelTypeInit chat = new ModelTypeInit();
+
+ /**
+ * Embedding models initialization settings.
+ */
+ private final ModelTypeInit embedding = new ModelTypeInit();
+
+ /**
+ * Whether to pull models at startup-time and how.
+ */
+ private PullModelStrategy pullModelStrategy = PullModelStrategy.NEVER;
+
+ /**
+ * How long to wait for a model to be pulled.
+ */
+ private Duration timeout = Duration.ofMinutes(5);
+
+ /**
+ * Maximum number of retries for the model pull operation.
+ */
+ private int maxRetries = 0;
+
+ public PullModelStrategy getPullModelStrategy() {
+ return this.pullModelStrategy;
+ }
+
+ public void setPullModelStrategy(PullModelStrategy pullModelStrategy) {
+ this.pullModelStrategy = pullModelStrategy;
+ }
+
+ public ModelTypeInit getChat() {
+ return this.chat;
+ }
+
+ public ModelTypeInit getEmbedding() {
+ return this.embedding;
+ }
+
+ public Duration getTimeout() {
+ return this.timeout;
+ }
+
+ public void setTimeout(Duration timeout) {
+ this.timeout = timeout;
+ }
+
+ public int getMaxRetries() {
+ return this.maxRetries;
+ }
+
+ public void setMaxRetries(int maxRetries) {
+ this.maxRetries = maxRetries;
+ }
+
+ public static class ModelTypeInit {
+
+ /**
+ * Include this type of models in the initialization task.
+ */
+ private boolean include = true;
+
+ /**
+ * Additional models to initialize besides the ones configured via default
+ * properties.
+ */
+ private List additionalModels = List.of();
+
+ public boolean isInclude() {
+ return this.include;
+ }
+
+ public void setInclude(boolean include) {
+ this.include = include;
+ }
+
+ public List getAdditionalModels() {
+ return this.additionalModels;
+ }
+
+ public void setAdditionalModels(List additionalModels) {
+ this.additionalModels = additionalModels;
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioSpeechProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioSpeechProperties.java
new file mode 100644
index 000000000..8508e5d8e
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioSpeechProperties.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.openai.autoconfigure;
+
+import org.springframework.ai.openai.OpenAiAudioSpeechOptions;
+import org.springframework.ai.openai.api.OpenAiAudioApi;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+/**
+ * Configuration properties for OpenAI audio speech.
+ *
+ * Default values for required options are model = tts_1, response format = mp3, voice =
+ * alloy, and speed = 1.
+ *
+ * @author Ahmed Yousri
+ * @author Stefan Vassilev
+ */
+@ConfigurationProperties(OpenAiAudioSpeechProperties.CONFIG_PREFIX)
+public class OpenAiAudioSpeechProperties extends OpenAiParentProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.openai.audio.speech";
+
+ public static final String DEFAULT_SPEECH_MODEL = OpenAiAudioApi.TtsModel.TTS_1.getValue();
+
+ private static final Float SPEED = 1.0f;
+
+ private static final OpenAiAudioApi.SpeechRequest.Voice VOICE = OpenAiAudioApi.SpeechRequest.Voice.ALLOY;
+
+ private static final OpenAiAudioApi.SpeechRequest.AudioResponseFormat DEFAULT_RESPONSE_FORMAT = OpenAiAudioApi.SpeechRequest.AudioResponseFormat.MP3;
+
+ /**
+ * Enable OpenAI audio speech model.
+ */
+ private boolean enabled = true;
+
+ @NestedConfigurationProperty
+ private OpenAiAudioSpeechOptions options = OpenAiAudioSpeechOptions.builder()
+ .model(DEFAULT_SPEECH_MODEL)
+ .responseFormat(DEFAULT_RESPONSE_FORMAT)
+ .voice(VOICE)
+ .speed(SPEED)
+ .build();
+
+ public OpenAiAudioSpeechOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(OpenAiAudioSpeechOptions options) {
+ this.options = options;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioTranscriptionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioTranscriptionProperties.java
new file mode 100644
index 000000000..eb4a967f0
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioTranscriptionProperties.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.openai.autoconfigure;
+
+import org.springframework.ai.openai.OpenAiAudioTranscriptionOptions;
+import org.springframework.ai.openai.api.OpenAiAudioApi;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+@ConfigurationProperties(OpenAiAudioTranscriptionProperties.CONFIG_PREFIX)
+public class OpenAiAudioTranscriptionProperties extends OpenAiParentProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.openai.audio.transcription";
+
+ public static final String DEFAULT_TRANSCRIPTION_MODEL = OpenAiAudioApi.WhisperModel.WHISPER_1.getValue();
+
+ private static final Double DEFAULT_TEMPERATURE = 0.7;
+
+ private static final OpenAiAudioApi.TranscriptResponseFormat DEFAULT_RESPONSE_FORMAT = OpenAiAudioApi.TranscriptResponseFormat.TEXT;
+
+ /**
+ * Enable OpenAI audio transcription model.
+ */
+ private boolean enabled = true;
+
+ @NestedConfigurationProperty
+ private OpenAiAudioTranscriptionOptions options = OpenAiAudioTranscriptionOptions.builder()
+ .model(DEFAULT_TRANSCRIPTION_MODEL)
+ .temperature(DEFAULT_TEMPERATURE.floatValue())
+ .responseFormat(DEFAULT_RESPONSE_FORMAT)
+ .build();
+
+ public OpenAiAudioTranscriptionOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(OpenAiAudioTranscriptionOptions options) {
+ this.options = options;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAutoConfiguration.java
new file mode 100644
index 000000000..1d6ddc51a
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAutoConfiguration.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright 2023-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.openai.autoconfigure;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import io.micrometer.observation.ObservationRegistry;
+import org.jetbrains.annotations.NotNull;
+
+import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration;
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.chat.observation.ChatModelObservationConvention;
+import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
+import org.springframework.ai.image.observation.ImageModelObservationConvention;
+import org.springframework.ai.model.SimpleApiKey;
+import org.springframework.ai.model.function.DefaultFunctionCallbackResolver;
+import org.springframework.ai.model.function.FunctionCallbackResolver;
+import org.springframework.ai.model.tool.ToolCallingManager;
+import org.springframework.ai.openai.OpenAiAudioSpeechModel;
+import org.springframework.ai.openai.OpenAiAudioTranscriptionModel;
+import org.springframework.ai.openai.OpenAiChatModel;
+import org.springframework.ai.openai.OpenAiEmbeddingModel;
+import org.springframework.ai.openai.OpenAiImageModel;
+import org.springframework.ai.openai.OpenAiModerationModel;
+import org.springframework.ai.openai.api.OpenAiApi;
+import org.springframework.ai.openai.api.OpenAiAudioApi;
+import org.springframework.ai.openai.api.OpenAiImageApi;
+import org.springframework.ai.openai.api.OpenAiModerationApi;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.retry.support.RetryTemplate;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.MultiValueMap;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.ResponseErrorHandler;
+import org.springframework.web.client.RestClient;
+import org.springframework.web.reactive.function.client.WebClient;
+
+/**
+ * {@link AutoConfiguration Auto-configuration} for OpenAI.
+ *
+ * @author Christian Tzolov
+ * @author Stefan Vassilev
+ * @author Thomas Vitale
+ * @author Ilayaperumal Gopinathan
+ */
+@AutoConfiguration(after = { RestClientAutoConfiguration.class, WebClientAutoConfiguration.class,
+ SpringAiRetryAutoConfiguration.class, ToolCallingAutoConfiguration.class })
+@ConditionalOnClass(OpenAiApi.class)
+@EnableConfigurationProperties({ OpenAiConnectionProperties.class, OpenAiChatProperties.class,
+ OpenAiEmbeddingProperties.class, OpenAiImageProperties.class, OpenAiAudioTranscriptionProperties.class,
+ OpenAiAudioSpeechProperties.class, OpenAiModerationProperties.class })
+@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class,
+ WebClientAutoConfiguration.class, ToolCallingAutoConfiguration.class })
+public class OpenAiAutoConfiguration {
+
+ private static @NotNull ResolvedConnectionProperties resolveConnectionProperties(
+ OpenAiParentProperties commonProperties, OpenAiParentProperties modelProperties, String modelType) {
+
+ String baseUrl = StringUtils.hasText(modelProperties.getBaseUrl()) ? modelProperties.getBaseUrl()
+ : commonProperties.getBaseUrl();
+ String apiKey = StringUtils.hasText(modelProperties.getApiKey()) ? modelProperties.getApiKey()
+ : commonProperties.getApiKey();
+ String projectId = StringUtils.hasText(modelProperties.getProjectId()) ? modelProperties.getProjectId()
+ : commonProperties.getProjectId();
+ String organizationId = StringUtils.hasText(modelProperties.getOrganizationId())
+ ? modelProperties.getOrganizationId() : commonProperties.getOrganizationId();
+
+ Map> connectionHeaders = new HashMap<>();
+ if (StringUtils.hasText(projectId)) {
+ connectionHeaders.put("OpenAI-Project", List.of(projectId));
+ }
+ if (StringUtils.hasText(organizationId)) {
+ connectionHeaders.put("OpenAI-Organization", List.of(organizationId));
+ }
+
+ Assert.hasText(baseUrl,
+ "OpenAI base URL must be set. Use the connection property: spring.ai.openai.base-url or spring.ai.openai."
+ + modelType + ".base-url property.");
+ Assert.hasText(apiKey,
+ "OpenAI API key must be set. Use the connection property: spring.ai.openai.api-key or spring.ai.openai."
+ + modelType + ".api-key property.");
+
+ return new ResolvedConnectionProperties(baseUrl, apiKey, CollectionUtils.toMultiValueMap(connectionHeaders));
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = OpenAiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+ public OpenAiChatModel openAiChatModel(OpenAiConnectionProperties commonProperties,
+ OpenAiChatProperties chatProperties, ObjectProvider restClientBuilderProvider,
+ ObjectProvider webClientBuilderProvider, ToolCallingManager toolCallingManager,
+ RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler,
+ ObjectProvider observationRegistry,
+ ObjectProvider observationConvention) {
+
+ var openAiApi = openAiApi(chatProperties, commonProperties,
+ restClientBuilderProvider.getIfAvailable(RestClient::builder),
+ webClientBuilderProvider.getIfAvailable(WebClient::builder), responseErrorHandler, "chat");
+
+ var chatModel = OpenAiChatModel.builder()
+ .openAiApi(openAiApi)
+ .defaultOptions(chatProperties.getOptions())
+ .toolCallingManager(toolCallingManager)
+ .retryTemplate(retryTemplate)
+ .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
+ .build();
+
+ observationConvention.ifAvailable(chatModel::setObservationConvention);
+
+ return chatModel;
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = OpenAiEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+ public OpenAiEmbeddingModel openAiEmbeddingModel(OpenAiConnectionProperties commonProperties,
+ OpenAiEmbeddingProperties embeddingProperties, ObjectProvider restClientBuilderProvider,
+ ObjectProvider webClientBuilderProvider, RetryTemplate retryTemplate,
+ ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry,
+ ObjectProvider observationConvention) {
+
+ var openAiApi = openAiApi(embeddingProperties, commonProperties,
+ restClientBuilderProvider.getIfAvailable(RestClient::builder),
+ webClientBuilderProvider.getIfAvailable(WebClient::builder), responseErrorHandler, "embedding");
+
+ var embeddingModel = new OpenAiEmbeddingModel(openAiApi, embeddingProperties.getMetadataMode(),
+ embeddingProperties.getOptions(), retryTemplate,
+ observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
+
+ observationConvention.ifAvailable(embeddingModel::setObservationConvention);
+
+ return embeddingModel;
+ }
+
+ private OpenAiApi openAiApi(OpenAiChatProperties chatProperties, OpenAiConnectionProperties commonProperties,
+ RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder,
+ ResponseErrorHandler responseErrorHandler, String modelType) {
+
+ ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, chatProperties,
+ modelType);
+
+ return OpenAiApi.builder()
+ .baseUrl(resolved.baseUrl())
+ .apiKey(new SimpleApiKey(resolved.apiKey()))
+ .headers(resolved.headers())
+ .completionsPath(chatProperties.getCompletionsPath())
+ .embeddingsPath(OpenAiEmbeddingProperties.DEFAULT_EMBEDDINGS_PATH)
+ .restClientBuilder(restClientBuilder)
+ .webClientBuilder(webClientBuilder)
+ .responseErrorHandler(responseErrorHandler)
+ .build();
+ }
+
+ private OpenAiApi openAiApi(OpenAiEmbeddingProperties embeddingProperties,
+ OpenAiConnectionProperties commonProperties, RestClient.Builder restClientBuilder,
+ WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler, String modelType) {
+
+ ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, embeddingProperties,
+ modelType);
+
+ return OpenAiApi.builder()
+ .baseUrl(resolved.baseUrl())
+ .apiKey(new SimpleApiKey(resolved.apiKey()))
+ .headers(resolved.headers())
+ .completionsPath(OpenAiChatProperties.DEFAULT_COMPLETIONS_PATH)
+ .embeddingsPath(embeddingProperties.getEmbeddingsPath())
+ .restClientBuilder(restClientBuilder)
+ .webClientBuilder(webClientBuilder)
+ .responseErrorHandler(responseErrorHandler)
+ .build();
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = OpenAiImageProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+ public OpenAiImageModel openAiImageModel(OpenAiConnectionProperties commonProperties,
+ OpenAiImageProperties imageProperties, ObjectProvider restClientBuilderProvider,
+ RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler,
+ ObjectProvider observationRegistry,
+ ObjectProvider observationConvention) {
+
+ ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, imageProperties, "image");
+
+ var openAiImageApi = OpenAiImageApi.builder()
+ .baseUrl(resolved.baseUrl())
+ .apiKey(new SimpleApiKey(resolved.apiKey()))
+ .headers(resolved.headers())
+ .restClientBuilder(restClientBuilderProvider.getIfAvailable(RestClient::builder))
+ .responseErrorHandler(responseErrorHandler)
+ .build();
+ var imageModel = new OpenAiImageModel(openAiImageApi, imageProperties.getOptions(), retryTemplate,
+ observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
+
+ observationConvention.ifAvailable(imageModel::setObservationConvention);
+
+ return imageModel;
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = OpenAiAudioTranscriptionProperties.CONFIG_PREFIX, name = "enabled",
+ havingValue = "true", matchIfMissing = true)
+ public OpenAiAudioTranscriptionModel openAiAudioTranscriptionModel(OpenAiConnectionProperties commonProperties,
+ OpenAiAudioTranscriptionProperties transcriptionProperties, RetryTemplate retryTemplate,
+ ObjectProvider restClientBuilderProvider,
+ ObjectProvider webClientBuilderProvider, ResponseErrorHandler responseErrorHandler) {
+
+ ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, transcriptionProperties,
+ "transcription");
+
+ 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))
+ .responseErrorHandler(responseErrorHandler)
+ .build();
+
+ return new OpenAiAudioTranscriptionModel(openAiAudioApi, transcriptionProperties.getOptions(), retryTemplate);
+
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public OpenAiModerationModel openAiModerationClient(OpenAiConnectionProperties commonProperties,
+ OpenAiModerationProperties moderationProperties, RetryTemplate retryTemplate,
+ ObjectProvider restClientBuilderProvider, ResponseErrorHandler responseErrorHandler) {
+
+ ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, moderationProperties,
+ "moderation");
+
+ var openAiModerationApi = OpenAiModerationApi.builder()
+ .baseUrl(resolved.baseUrl)
+ .apiKey(new SimpleApiKey(resolved.apiKey()))
+ .headers(resolved.headers())
+ .restClientBuilder(restClientBuilderProvider.getIfAvailable(RestClient::builder))
+ .responseErrorHandler(responseErrorHandler)
+ .build();
+ return new OpenAiModerationModel(openAiModerationApi, retryTemplate)
+ .withDefaultOptions(moderationProperties.getOptions());
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = OpenAiAudioSpeechProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+ public OpenAiAudioSpeechModel openAiAudioSpeechClient(OpenAiConnectionProperties commonProperties,
+ OpenAiAudioSpeechProperties speechProperties, RetryTemplate retryTemplate,
+ ObjectProvider restClientBuilderProvider,
+ ObjectProvider webClientBuilderProvider, ResponseErrorHandler responseErrorHandler) {
+
+ ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, speechProperties,
+ "speach");
+
+ 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))
+ .responseErrorHandler(responseErrorHandler)
+ .build();
+
+ return new OpenAiAudioSpeechModel(openAiAudioApi, speechProperties.getOptions());
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) {
+ DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver();
+ manager.setApplicationContext(context);
+ return manager;
+ }
+
+ private record ResolvedConnectionProperties(String baseUrl, String apiKey, MultiValueMap headers) {
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiChatProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiChatProperties.java
new file mode 100644
index 000000000..3cef1db88
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiChatProperties.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.openai.autoconfigure;
+
+import org.springframework.ai.openai.OpenAiChatOptions;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+@ConfigurationProperties(OpenAiChatProperties.CONFIG_PREFIX)
+public class OpenAiChatProperties extends OpenAiParentProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.openai.chat";
+
+ public static final String DEFAULT_CHAT_MODEL = "gpt-4o-mini";
+
+ public static final String DEFAULT_COMPLETIONS_PATH = "/v1/chat/completions";
+
+ private static final Double DEFAULT_TEMPERATURE = 0.7;
+
+ /**
+ * Enable OpenAI chat model.
+ */
+ private boolean enabled = true;
+
+ private String completionsPath = DEFAULT_COMPLETIONS_PATH;
+
+ @NestedConfigurationProperty
+ private OpenAiChatOptions options = OpenAiChatOptions.builder()
+ .model(DEFAULT_CHAT_MODEL)
+ .temperature(DEFAULT_TEMPERATURE)
+ .build();
+
+ public OpenAiChatOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(OpenAiChatOptions options) {
+ this.options = options;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public String getCompletionsPath() {
+ return this.completionsPath;
+ }
+
+ public void setCompletionsPath(String completionsPath) {
+ this.completionsPath = completionsPath;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiConnectionProperties.java
new file mode 100644
index 000000000..41f00e303
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiConnectionProperties.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.openai.autoconfigure;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties(OpenAiConnectionProperties.CONFIG_PREFIX)
+public class OpenAiConnectionProperties extends OpenAiParentProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.openai";
+
+ public static final String DEFAULT_BASE_URL = "https://api.openai.com";
+
+ public OpenAiConnectionProperties() {
+ super.setBaseUrl(DEFAULT_BASE_URL);
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiEmbeddingProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiEmbeddingProperties.java
new file mode 100644
index 000000000..c70e82c66
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiEmbeddingProperties.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.openai.autoconfigure;
+
+import org.springframework.ai.document.MetadataMode;
+import org.springframework.ai.openai.OpenAiEmbeddingOptions;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+@ConfigurationProperties(OpenAiEmbeddingProperties.CONFIG_PREFIX)
+public class OpenAiEmbeddingProperties extends OpenAiParentProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.openai.embedding";
+
+ public static final String DEFAULT_EMBEDDING_MODEL = "text-embedding-ada-002";
+
+ public static final String DEFAULT_EMBEDDINGS_PATH = "/v1/embeddings";
+
+ /**
+ * Enable OpenAI embedding model.
+ */
+ private boolean enabled = true;
+
+ private MetadataMode metadataMode = MetadataMode.EMBED;
+
+ private String embeddingsPath = DEFAULT_EMBEDDINGS_PATH;
+
+ @NestedConfigurationProperty
+ private OpenAiEmbeddingOptions options = OpenAiEmbeddingOptions.builder().model(DEFAULT_EMBEDDING_MODEL).build();
+
+ public OpenAiEmbeddingOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(OpenAiEmbeddingOptions options) {
+ this.options = options;
+ }
+
+ public MetadataMode getMetadataMode() {
+ return this.metadataMode;
+ }
+
+ public void setMetadataMode(MetadataMode metadataMode) {
+ this.metadataMode = metadataMode;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public String getEmbeddingsPath() {
+ return this.embeddingsPath;
+ }
+
+ public void setEmbeddingsPath(String embeddingsPath) {
+ this.embeddingsPath = embeddingsPath;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiImageProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiImageProperties.java
new file mode 100644
index 000000000..ac7b11c80
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiImageProperties.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.openai.autoconfigure;
+
+import org.springframework.ai.openai.OpenAiImageOptions;
+import org.springframework.ai.openai.api.OpenAiImageApi;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+/**
+ * OpenAI Image autoconfiguration properties.
+ *
+ * @author Thomas Vitale
+ * @since 0.8.0
+ */
+@ConfigurationProperties(OpenAiImageProperties.CONFIG_PREFIX)
+public class OpenAiImageProperties extends OpenAiParentProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.openai.image";
+
+ public static final String DEFAULT_IMAGE_MODEL = OpenAiImageApi.ImageModel.DALL_E_3.getValue();
+
+ /**
+ * Enable OpenAI image model.
+ */
+ private boolean enabled = true;
+
+ /**
+ * Options for OpenAI Image API.
+ */
+ @NestedConfigurationProperty
+ private OpenAiImageOptions options = OpenAiImageOptions.builder().model(DEFAULT_IMAGE_MODEL).build();
+
+ public OpenAiImageOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(OpenAiImageOptions options) {
+ this.options = options;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiModerationProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiModerationProperties.java
new file mode 100644
index 000000000..a268f93bd
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiModerationProperties.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.openai.autoconfigure;
+
+import org.springframework.ai.openai.OpenAiModerationOptions;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+/**
+ * OpenAI Moderation autoconfiguration properties.
+ *
+ * @author Ahmed Yousri
+ * @since 0.9.0
+ */
+@ConfigurationProperties(OpenAiModerationProperties.CONFIG_PREFIX)
+public class OpenAiModerationProperties extends OpenAiParentProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.openai.moderation";
+
+ /**
+ * Options for OpenAI Moderation API.
+ */
+ @NestedConfigurationProperty
+ private OpenAiModerationOptions options = OpenAiModerationOptions.builder().build();
+
+ public OpenAiModerationOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(OpenAiModerationOptions options) {
+ this.options = options;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiParentProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiParentProperties.java
new file mode 100644
index 000000000..9b3f29a0e
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiParentProperties.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.openai.autoconfigure;
+
+/**
+ * Internal parent properties for the OpenAI properties.
+ *
+ * @author Christian Tzolov
+ * @since 0.8.0
+ */
+class OpenAiParentProperties {
+
+ private String apiKey;
+
+ private String baseUrl;
+
+ private String projectId;
+
+ private String organizationId;
+
+ public String getApiKey() {
+ return this.apiKey;
+ }
+
+ public void setApiKey(String apiKey) {
+ this.apiKey = apiKey;
+ }
+
+ public String getBaseUrl() {
+ return this.baseUrl;
+ }
+
+ public void setBaseUrl(String baseUrl) {
+ this.baseUrl = baseUrl;
+ }
+
+ public String getProjectId() {
+ return this.projectId;
+ }
+
+ public void setProjectId(String projectId) {
+ this.projectId = projectId;
+ }
+
+ public String getOrganizationId() {
+ return this.organizationId;
+ }
+
+ public void setOrganizationId(String organizationId) {
+ this.organizationId = organizationId;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlAutoConfiguration.java
new file mode 100644
index 000000000..ae0092ee8
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlAutoConfiguration.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.postgresml.autoconfigure;
+
+import org.springframework.ai.postgresml.PostgresMlEmbeddingModel;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+/**
+ * Auto-configuration class for PostgresMlEmbeddingModel.
+ *
+ * @author Utkarsh Srivastava
+ * @author Christian Tzolov
+ */
+@AutoConfiguration(after = JdbcTemplateAutoConfiguration.class)
+@ConditionalOnClass(PostgresMlEmbeddingModel.class)
+@EnableConfigurationProperties(PostgresMlEmbeddingProperties.class)
+public class PostgresMlAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = PostgresMlEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+ public PostgresMlEmbeddingModel postgresMlEmbeddingModel(JdbcTemplate jdbcTemplate,
+ PostgresMlEmbeddingProperties embeddingProperties) {
+
+ return new PostgresMlEmbeddingModel(jdbcTemplate, embeddingProperties.getOptions(),
+ embeddingProperties.isCreateExtension());
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlEmbeddingProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlEmbeddingProperties.java
new file mode 100644
index 000000000..5cd3ca0df
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlEmbeddingProperties.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.postgresml.autoconfigure;
+
+import java.util.Map;
+
+import org.springframework.ai.document.MetadataMode;
+import org.springframework.ai.postgresml.PostgresMlEmbeddingModel;
+import org.springframework.ai.postgresml.PostgresMlEmbeddingOptions;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+import org.springframework.util.Assert;
+
+/**
+ * Configuration properties for Postgres ML.
+ *
+ * @author Utkarsh Srivastava
+ * @author Christian Tzolov
+ */
+@ConfigurationProperties(PostgresMlEmbeddingProperties.CONFIG_PREFIX)
+public class PostgresMlEmbeddingProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.postgresml.embedding";
+
+ /**
+ * Enable Postgres ML embedding model.
+ */
+ private boolean enabled = true;
+
+ /**
+ * Create the extensions required for embedding
+ */
+ private boolean createExtension;
+
+ @NestedConfigurationProperty
+ private PostgresMlEmbeddingOptions options = PostgresMlEmbeddingOptions.builder()
+ .transformer(PostgresMlEmbeddingModel.DEFAULT_TRANSFORMER_MODEL)
+ .vectorType(PostgresMlEmbeddingModel.VectorType.PG_ARRAY)
+ .kwargs(Map.of())
+ .metadataMode(MetadataMode.EMBED)
+ .build();
+
+ public PostgresMlEmbeddingOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(PostgresMlEmbeddingOptions options) {
+ Assert.notNull(options, "options must not be null.");
+ Assert.notNull(options.getTransformer(), "transformer must not be null.");
+ Assert.notNull(options.getVectorType(), "vectorType must not be null.");
+ Assert.notNull(options.getKwargs(), "kwargs must not be null.");
+ Assert.notNull(options.getMetadataMode(), "metadataMode must not be null.");
+
+ this.options = options;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public boolean isCreateExtension() {
+ return this.createExtension;
+ }
+
+ public void setCreateExtension(boolean createExtension) {
+ this.createExtension = createExtension;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanAutoConfiguration.java
new file mode 100644
index 000000000..a48870d9d
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanAutoConfiguration.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.qianfan.autoconfigure;
+
+import io.micrometer.observation.ObservationRegistry;
+
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.chat.observation.ChatModelObservationConvention;
+import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
+import org.springframework.ai.image.observation.ImageModelObservationConvention;
+import org.springframework.ai.model.function.DefaultFunctionCallbackResolver;
+import org.springframework.ai.model.function.FunctionCallbackResolver;
+import org.springframework.ai.qianfan.QianFanChatModel;
+import org.springframework.ai.qianfan.QianFanEmbeddingModel;
+import org.springframework.ai.qianfan.QianFanImageModel;
+import org.springframework.ai.qianfan.api.QianFanApi;
+import org.springframework.ai.qianfan.api.QianFanImageApi;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.retry.support.RetryTemplate;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.ResponseErrorHandler;
+import org.springframework.web.client.RestClient;
+
+/**
+ * {@link AutoConfiguration Auto-configuration} for QianFan Chat, Embedding, and Image
+ * Models.
+ *
+ * @author Geng Rong
+ */
+@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class })
+@ConditionalOnClass(QianFanApi.class)
+@EnableConfigurationProperties({ QianFanConnectionProperties.class, QianFanChatProperties.class,
+ QianFanEmbeddingProperties.class, QianFanImageProperties.class })
+public class QianFanAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = QianFanChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+ public QianFanChatModel qianFanChatModel(QianFanConnectionProperties commonProperties,
+ QianFanChatProperties chatProperties, ObjectProvider restClientBuilderProvider,
+ RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler,
+ ObjectProvider observationRegistry,
+ ObjectProvider observationConvention) {
+
+ var qianFanApi = qianFanApi(chatProperties.getBaseUrl(), commonProperties.getBaseUrl(),
+ chatProperties.getApiKey(), commonProperties.getApiKey(), chatProperties.getSecretKey(),
+ commonProperties.getSecretKey(), restClientBuilderProvider.getIfAvailable(RestClient::builder),
+ responseErrorHandler);
+
+ var chatModel = new QianFanChatModel(qianFanApi, chatProperties.getOptions(), retryTemplate,
+ observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
+
+ observationConvention.ifAvailable(chatModel::setObservationConvention);
+
+ return chatModel;
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = QianFanEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+ public QianFanEmbeddingModel qianFanEmbeddingModel(QianFanConnectionProperties commonProperties,
+ QianFanEmbeddingProperties embeddingProperties,
+ ObjectProvider restClientBuilderProvider, RetryTemplate retryTemplate,
+ ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry,
+ ObjectProvider observationConvention) {
+
+ var qianFanApi = qianFanApi(embeddingProperties.getBaseUrl(), commonProperties.getBaseUrl(),
+ embeddingProperties.getApiKey(), commonProperties.getApiKey(), embeddingProperties.getSecretKey(),
+ commonProperties.getSecretKey(), restClientBuilderProvider.getIfAvailable(RestClient::builder),
+ responseErrorHandler);
+
+ var embeddingModel = new QianFanEmbeddingModel(qianFanApi, embeddingProperties.getMetadataMode(),
+ embeddingProperties.getOptions(), retryTemplate,
+ observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
+
+ observationConvention.ifAvailable(embeddingModel::setObservationConvention);
+
+ return embeddingModel;
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = QianFanImageProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+ public QianFanImageModel qianFanImageModel(QianFanConnectionProperties commonProperties,
+ QianFanImageProperties imageProperties, ObjectProvider restClientBuilderProvider,
+ RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler,
+ ObjectProvider observationRegistry,
+ ObjectProvider observationConvention) {
+
+ String apiKey = StringUtils.hasText(imageProperties.getApiKey()) ? imageProperties.getApiKey()
+ : commonProperties.getApiKey();
+
+ String secretKey = StringUtils.hasText(imageProperties.getSecretKey()) ? imageProperties.getSecretKey()
+ : commonProperties.getSecretKey();
+
+ String baseUrl = StringUtils.hasText(imageProperties.getBaseUrl()) ? imageProperties.getBaseUrl()
+ : commonProperties.getBaseUrl();
+
+ Assert.hasText(apiKey, "QianFan API key must be set. Use the property: spring.ai.qianfan.api-key");
+ Assert.hasText(secretKey, "QianFan secret key must be set. Use the property: spring.ai.qianfan.secret-key");
+ Assert.hasText(baseUrl, "QianFan base URL must be set. Use the property: spring.ai.qianfan.base-url");
+
+ var qianFanImageApi = new QianFanImageApi(baseUrl, apiKey, secretKey,
+ restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler);
+
+ var imageModel = new QianFanImageModel(qianFanImageApi, imageProperties.getOptions(), retryTemplate,
+ observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
+
+ observationConvention.ifAvailable(imageModel::setObservationConvention);
+
+ return imageModel;
+ }
+
+ private QianFanApi qianFanApi(String baseUrl, String commonBaseUrl, String apiKey, String commonApiKey,
+ String secretKey, String commonSecretKey, RestClient.Builder restClientBuilder,
+ ResponseErrorHandler responseErrorHandler) {
+
+ String resolvedBaseUrl = StringUtils.hasText(baseUrl) ? baseUrl : commonBaseUrl;
+ Assert.hasText(resolvedBaseUrl, "QianFan base URL must be set");
+
+ String resolvedApiKey = StringUtils.hasText(apiKey) ? apiKey : commonApiKey;
+ Assert.hasText(resolvedApiKey, "QianFan API key must be set");
+
+ String resolvedSecretKey = StringUtils.hasText(secretKey) ? secretKey : commonSecretKey;
+ Assert.hasText(resolvedSecretKey, "QianFan Secret key must be set");
+
+ return new QianFanApi(resolvedBaseUrl, resolvedApiKey, resolvedSecretKey, restClientBuilder,
+ responseErrorHandler);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) {
+ DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver();
+ manager.setApplicationContext(context);
+ return manager;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanChatProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanChatProperties.java
new file mode 100644
index 000000000..9ca6b16cb
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanChatProperties.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.qianfan.autoconfigure;
+
+import org.springframework.ai.qianfan.QianFanChatOptions;
+import org.springframework.ai.qianfan.api.QianFanApi;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+/**
+ * Configuration properties for QianFan chat model.
+ *
+ * @author Geng Rong
+ */
+@ConfigurationProperties(QianFanChatProperties.CONFIG_PREFIX)
+public class QianFanChatProperties extends QianFanParentProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.qianfan.chat";
+
+ public static final String DEFAULT_CHAT_MODEL = QianFanApi.ChatModel.ERNIE_Speed_8K.value;
+
+ private static final Double DEFAULT_TEMPERATURE = 0.7;
+
+ /**
+ * Enable QianFan chat client.
+ */
+ private boolean enabled = true;
+
+ @NestedConfigurationProperty
+ private QianFanChatOptions options = QianFanChatOptions.builder()
+ .model(DEFAULT_CHAT_MODEL)
+ .temperature(DEFAULT_TEMPERATURE)
+ .build();
+
+ public QianFanChatOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(QianFanChatOptions options) {
+ this.options = options;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanConnectionProperties.java
new file mode 100644
index 000000000..e26662ef6
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanConnectionProperties.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.qianfan.autoconfigure;
+
+import org.springframework.ai.qianfan.api.QianFanConstants;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties(QianFanConnectionProperties.CONFIG_PREFIX)
+public class QianFanConnectionProperties extends QianFanParentProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.qianfan";
+
+ public static final String DEFAULT_BASE_URL = QianFanConstants.DEFAULT_BASE_URL;
+
+ public QianFanConnectionProperties() {
+ super.setBaseUrl(DEFAULT_BASE_URL);
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanEmbeddingProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanEmbeddingProperties.java
new file mode 100644
index 000000000..d71bd12b3
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanEmbeddingProperties.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.qianfan.autoconfigure;
+
+import org.springframework.ai.document.MetadataMode;
+import org.springframework.ai.qianfan.QianFanEmbeddingOptions;
+import org.springframework.ai.qianfan.api.QianFanApi;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+/**
+ * Configuration properties for QianFan embedding model.
+ *
+ * @author Geng Rong
+ */
+@ConfigurationProperties(QianFanEmbeddingProperties.CONFIG_PREFIX)
+public class QianFanEmbeddingProperties extends QianFanParentProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.qianfan.embedding";
+
+ /**
+ * Enable QianFan embedding client.
+ */
+ private boolean enabled = true;
+
+ private MetadataMode metadataMode = MetadataMode.EMBED;
+
+ @NestedConfigurationProperty
+ private QianFanEmbeddingOptions options = QianFanEmbeddingOptions.builder()
+ .model(QianFanApi.DEFAULT_EMBEDDING_MODEL)
+ .build();
+
+ public QianFanEmbeddingOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(QianFanEmbeddingOptions options) {
+ this.options = options;
+ }
+
+ public MetadataMode getMetadataMode() {
+ return this.metadataMode;
+ }
+
+ public void setMetadataMode(MetadataMode metadataMode) {
+ this.metadataMode = metadataMode;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanImageProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanImageProperties.java
new file mode 100644
index 000000000..3341b760b
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanImageProperties.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.qianfan.autoconfigure;
+
+import org.springframework.ai.qianfan.QianFanImageOptions;
+import org.springframework.ai.qianfan.api.QianFanImageApi;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+/**
+ * QianFan Image autoconfiguration properties.
+ *
+ * @author Geng Rong
+ */
+@ConfigurationProperties(QianFanImageProperties.CONFIG_PREFIX)
+public class QianFanImageProperties extends QianFanParentProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.qianfan.image";
+
+ public static final String DEFAULT_IMAGE_MODEL = QianFanImageApi.ImageModel.Stable_Diffusion_XL.getValue();
+
+ /**
+ * Enable QianFan image model.
+ */
+ private boolean enabled = true;
+
+ /**
+ * Options for QianFan Image API.
+ */
+ @NestedConfigurationProperty
+ private QianFanImageOptions options = QianFanImageOptions.builder().model(DEFAULT_IMAGE_MODEL).build();
+
+ public QianFanImageOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(QianFanImageOptions options) {
+ this.options = options;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanParentProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanParentProperties.java
new file mode 100644
index 000000000..424cbadc1
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanParentProperties.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.qianfan.autoconfigure;
+
+/**
+ * @author Geng Rong
+ */
+class QianFanParentProperties {
+
+ private String apiKey;
+
+ private String secretKey;
+
+ private String baseUrl;
+
+ public String getApiKey() {
+ return this.apiKey;
+ }
+
+ public void setApiKey(String apiKey) {
+ this.apiKey = apiKey;
+ }
+
+ public String getSecretKey() {
+ return this.secretKey;
+ }
+
+ public void setSecretKey(String secretKey) {
+ this.secretKey = secretKey;
+ }
+
+ public String getBaseUrl() {
+ return this.baseUrl;
+ }
+
+ public void setBaseUrl(String baseUrl) {
+ this.baseUrl = baseUrl;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiConnectionProperties.java
new file mode 100644
index 000000000..d1ec0261d
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiConnectionProperties.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.stabilityai.autoconfigure;
+
+import org.springframework.ai.stabilityai.api.StabilityAiApi;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties(StabilityAiConnectionProperties.CONFIG_PREFIX)
+public class StabilityAiConnectionProperties extends StabilityAiParentProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.stabilityai";
+
+ public static final String DEFAULT_BASE_URL = StabilityAiApi.DEFAULT_BASE_URL;
+
+ public StabilityAiConnectionProperties() {
+ super.setBaseUrl(DEFAULT_BASE_URL);
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageAutoConfiguration.java
new file mode 100644
index 000000000..7c143a941
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageAutoConfiguration.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.stabilityai.autoconfigure;
+
+import org.springframework.ai.stabilityai.StabilityAiImageModel;
+import org.springframework.ai.stabilityai.api.StabilityAiApi;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.RestClient;
+
+/**
+ * {@link AutoConfiguration Auto-configuration} for StabilityAI Image Model.
+ *
+ * @author Mark Pollack
+ * @author Christian Tzolov
+ * @since 0.8.0
+ */
+@AutoConfiguration(after = { RestClientAutoConfiguration.class })
+@ConditionalOnClass(StabilityAiApi.class)
+@EnableConfigurationProperties({ StabilityAiConnectionProperties.class, StabilityAiImageProperties.class })
+public class StabilityAiImageAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ public StabilityAiApi stabilityAiApi(StabilityAiConnectionProperties commonProperties,
+ StabilityAiImageProperties imageProperties, ObjectProvider restClientBuilderProvider) {
+
+ String apiKey = StringUtils.hasText(imageProperties.getApiKey()) ? imageProperties.getApiKey()
+ : commonProperties.getApiKey();
+
+ String baseUrl = StringUtils.hasText(imageProperties.getBaseUrl()) ? imageProperties.getBaseUrl()
+ : commonProperties.getBaseUrl();
+
+ 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));
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = StabilityAiImageProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+ public StabilityAiImageModel stabilityAiImageModel(StabilityAiApi stabilityAiApi,
+ StabilityAiImageProperties stabilityAiImageProperties) {
+ return new StabilityAiImageModel(stabilityAiApi, stabilityAiImageProperties.getOptions());
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageProperties.java
new file mode 100644
index 000000000..dc78af948
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageProperties.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.stabilityai.autoconfigure;
+
+import org.springframework.ai.stabilityai.api.StabilityAiImageOptions;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+/**
+ * Configuration properties for Stability AI image model.
+ *
+ * @author Mark Pollack
+ * @author Christian Tzolov
+ * @since 0.8.0
+ */
+@ConfigurationProperties(StabilityAiImageProperties.CONFIG_PREFIX)
+public class StabilityAiImageProperties extends StabilityAiParentProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.stabilityai.image";
+
+ /**
+ * Enable Stability image model.
+ */
+ private boolean enabled = true;
+
+ @NestedConfigurationProperty
+ private StabilityAiImageOptions options = StabilityAiImageOptions.builder().build(); // stable-diffusion-v1-6
+
+ // is
+ // default
+ // model
+
+ public StabilityAiImageOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(StabilityAiImageOptions options) {
+ this.options = options;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiParentProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiParentProperties.java
new file mode 100644
index 000000000..f75cc2b09
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiParentProperties.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.stabilityai.autoconfigure;
+
+/**
+ * Internal parent properties for the StabilityAI properties.
+ *
+ * @author Mark Pollack
+ * @since 0.8.0
+ */
+class StabilityAiParentProperties {
+
+ private String apiKey;
+
+ private String baseUrl;
+
+ public String getApiKey() {
+ return this.apiKey;
+ }
+
+ public void setApiKey(String apiKey) {
+ this.apiKey = apiKey;
+ }
+
+ public String getBaseUrl() {
+ return this.baseUrl;
+ }
+
+ public void setBaseUrl(String baseUrl) {
+ this.baseUrl = baseUrl;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfiguration.java
new file mode 100644
index 000000000..b653b67d7
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfiguration.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2023-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.tool.autoconfigure;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.micrometer.observation.ObservationRegistry;
+
+import org.springframework.ai.chat.model.ChatModel;
+import org.springframework.ai.model.function.FunctionCallback;
+import org.springframework.ai.model.tool.ToolCallingManager;
+import org.springframework.ai.tool.ToolCallbackProvider;
+import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
+import org.springframework.ai.tool.execution.ToolExecutionExceptionProcessor;
+import org.springframework.ai.tool.resolution.DelegatingToolCallbackResolver;
+import org.springframework.ai.tool.resolution.SpringBeanToolCallbackResolver;
+import org.springframework.ai.tool.resolution.StaticToolCallbackResolver;
+import org.springframework.ai.tool.resolution.ToolCallbackResolver;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.support.GenericApplicationContext;
+
+/**
+ * Auto-configuration for common tool calling features of {@link ChatModel}.
+ *
+ * @author Thomas Vitale
+ * @author Christian Tzolov
+ * @since 1.0.0
+ */
+@AutoConfiguration
+@ConditionalOnClass(ChatModel.class)
+public class ToolCallingAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ ToolCallbackResolver toolCallbackResolver(GenericApplicationContext applicationContext,
+ List functionCallbacks, List tcbProviders) {
+
+ List allFunctionAndToolCallbacks = new ArrayList<>(functionCallbacks);
+ tcbProviders.stream().map(pr -> List.of(pr.getToolCallbacks())).forEach(allFunctionAndToolCallbacks::addAll);
+
+ var staticToolCallbackResolver = new StaticToolCallbackResolver(allFunctionAndToolCallbacks);
+
+ var springBeanToolCallbackResolver = SpringBeanToolCallbackResolver.builder()
+ .applicationContext(applicationContext)
+ .build();
+
+ return new DelegatingToolCallbackResolver(List.of(staticToolCallbackResolver, springBeanToolCallbackResolver));
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ ToolExecutionExceptionProcessor toolExecutionExceptionProcessor() {
+ return new DefaultToolExecutionExceptionProcessor(false);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ ToolCallingManager toolCallingManager(ToolCallbackResolver toolCallbackResolver,
+ ToolExecutionExceptionProcessor toolExecutionExceptionProcessor,
+ ObjectProvider observationRegistry) {
+ return ToolCallingManager.builder()
+ .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
+ .toolCallbackResolver(toolCallbackResolver)
+ .toolExecutionExceptionProcessor(toolExecutionExceptionProcessor)
+ .build();
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelAutoConfiguration.java
new file mode 100644
index 000000000..495428766
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelAutoConfiguration.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.transformers.autoconfigure;
+
+import ai.djl.huggingface.tokenizers.HuggingFaceTokenizer;
+import ai.onnxruntime.OrtSession;
+import io.micrometer.observation.ObservationRegistry;
+
+import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
+import org.springframework.ai.transformers.TransformersEmbeddingModel;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+
+/**
+ * {@link AutoConfiguration Auto-configuration} for Transformers Embedding Model.
+ *
+ * @author Christian Tzolov
+ */
+@AutoConfiguration
+@EnableConfigurationProperties({ TransformersEmbeddingModelProperties.class })
+@ConditionalOnClass({ OrtSession.class, HuggingFaceTokenizer.class, TransformersEmbeddingModel.class })
+public class TransformersEmbeddingModelAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = TransformersEmbeddingModelProperties.CONFIG_PREFIX, name = "enabled",
+ havingValue = "true", matchIfMissing = true)
+ public TransformersEmbeddingModel embeddingModel(TransformersEmbeddingModelProperties properties,
+ ObjectProvider observationRegistry,
+ ObjectProvider observationConvention) {
+
+ TransformersEmbeddingModel embeddingModel = new TransformersEmbeddingModel(properties.getMetadataMode(),
+ observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
+
+ embeddingModel.setDisableCaching(!properties.getCache().isEnabled());
+ embeddingModel.setResourceCacheDirectory(properties.getCache().getDirectory());
+
+ embeddingModel.setTokenizerResource(properties.getTokenizer().getUri());
+ embeddingModel.setTokenizerOptions(properties.getTokenizer().getOptions());
+
+ embeddingModel.setModelResource(properties.getOnnx().getModelUri());
+
+ embeddingModel.setGpuDeviceId(properties.getOnnx().getGpuDeviceId());
+
+ embeddingModel.setModelOutputName(properties.getOnnx().getModelOutputName());
+
+ observationConvention.ifAvailable(embeddingModel::setObservationConvention);
+
+ return embeddingModel;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelProperties.java
new file mode 100644
index 000000000..a4535f8ac
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelProperties.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.transformers.autoconfigure;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import ai.djl.huggingface.tokenizers.HuggingFaceTokenizer;
+
+import org.springframework.ai.document.Document;
+import org.springframework.ai.document.MetadataMode;
+import org.springframework.ai.transformers.TransformersEmbeddingModel;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+/**
+ * Configuration properties for the Transformer Embedding model.
+ *
+ * @author Christian Tzolov
+ */
+@ConfigurationProperties(TransformersEmbeddingModelProperties.CONFIG_PREFIX)
+public class TransformersEmbeddingModelProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.embedding.transformer";
+
+ public static final String DEFAULT_CACHE_DIRECTORY = new File(System.getProperty("java.io.tmpdir"),
+ "spring-ai-onnx-generative")
+ .getAbsolutePath();
+
+ @NestedConfigurationProperty
+ private final Tokenizer tokenizer = new Tokenizer();
+
+ /**
+ * Controls caching of remote, large resources to local file system.
+ */
+ @NestedConfigurationProperty
+ private final Cache cache = new Cache();
+
+ @NestedConfigurationProperty
+ private final Onnx onnx = new Onnx();
+
+ /**
+ * Enable the Transformer Embedding model.
+ */
+ private boolean enabled = true;
+
+ /**
+ * Specifies what parts of the {@link Document}'s content and metadata will be used
+ * for computing the embeddings. Applicable for the
+ * {@link TransformersEmbeddingModel#embed(Document)} method only. Has no effect on
+ * the {@link TransformersEmbeddingModel#embed(String)} or
+ * {@link TransformersEmbeddingModel#embed(List)}. Defaults to
+ * {@link MetadataMode#NONE}.
+ */
+ private MetadataMode metadataMode = MetadataMode.NONE;
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public Cache getCache() {
+ return this.cache;
+ }
+
+ public Onnx getOnnx() {
+ return this.onnx;
+ }
+
+ public Tokenizer getTokenizer() {
+ return this.tokenizer;
+ }
+
+ public MetadataMode getMetadataMode() {
+ return this.metadataMode;
+ }
+
+ public void setMetadataMode(MetadataMode metadataMode) {
+ this.metadataMode = metadataMode;
+ }
+
+ /**
+ * Configurations for the {@link HuggingFaceTokenizer} used to convert sentences into
+ * tokens.
+ */
+ public static class Tokenizer {
+
+ /**
+ * URI of a pre-trained HuggingFaceTokenizer created by the ONNX engine (e.g.
+ * tokenizer.json).
+ */
+ private String uri = TransformersEmbeddingModel.DEFAULT_ONNX_TOKENIZER_URI;
+
+ /**
+ * HuggingFaceTokenizer options such as 'addSpecialTokens', 'modelMaxLength',
+ * 'truncation', 'padding', 'maxLength', 'stride' and 'padToMultipleOf'. Leave
+ * empty to fall back to the defaults.
+ */
+ @NestedConfigurationProperty
+ private Map options = new HashMap<>();
+
+ public String getUri() {
+ return this.uri;
+ }
+
+ public void setUri(String uri) {
+ this.uri = uri;
+ }
+
+ public Map getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(Map options) {
+ this.options = options;
+ }
+
+ }
+
+ public static class Cache {
+
+ /**
+ * Enable the Resource caching.
+ */
+ private boolean enabled = true;
+
+ /**
+ * Resource cache directory. Used to cache remote resources, such as the ONNX
+ * models, to the local file system. Applicable only for cache.enabled == true.
+ * Defaults to {java.io.tmpdir}/spring-ai-onnx-generative.
+ */
+ private String directory = DEFAULT_CACHE_DIRECTORY;
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public String getDirectory() {
+ return this.directory;
+ }
+
+ public void setDirectory(String directory) {
+ this.directory = directory;
+ }
+
+ }
+
+ public static class Onnx {
+
+ /**
+ * Existing, pre-trained ONNX generative. Commonly exported from
+ * https://sbert.net/docs/pretrained_models.html. Defaults to
+ * sentence-transformers/all-MiniLM-L6-v2.
+ */
+ private String modelUri = TransformersEmbeddingModel.DEFAULT_ONNX_MODEL_URI;
+
+ /**
+ * Defaults to: 'last_hidden_state'.
+ */
+ private String modelOutputName = TransformersEmbeddingModel.DEFAULT_MODEL_OUTPUT_NAME;
+
+ /**
+ * Run on a GPU or with another provider (optional).
+ * https://onnxruntime.ai/docs/get-started/with-java.html#run-on-a-gpu-or-with-another-provider-optional
+ *
+ * The GPU device ID to execute on. Only applicable if >= 0. Ignored otherwise.
+ */
+ private int gpuDeviceId = -1;
+
+ public String getModelUri() {
+ return this.modelUri;
+ }
+
+ public void setModelUri(String modelUri) {
+ this.modelUri = modelUri;
+ }
+
+ public int getGpuDeviceId() {
+ return this.gpuDeviceId;
+ }
+
+ public void setGpuDeviceId(int gpuDeviceId) {
+ this.gpuDeviceId = gpuDeviceId;
+ }
+
+ public String getModelOutputName() {
+ return this.modelOutputName;
+ }
+
+ public void setModelOutputName(String modelOutputName) {
+ this.modelOutputName = modelOutputName;
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiEmbeddingAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiEmbeddingAutoConfiguration.java
new file mode 100644
index 000000000..ab57b118f
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiEmbeddingAutoConfiguration.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.vertexai.autoconfigure.embedding;
+
+import java.io.IOException;
+
+import com.google.cloud.vertexai.VertexAI;
+import io.micrometer.observation.ObservationRegistry;
+
+import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.vertexai.embedding.VertexAiEmbeddingConnectionDetails;
+import org.springframework.ai.vertexai.embedding.multimodal.VertexAiMultimodalEmbeddingModel;
+import org.springframework.ai.vertexai.embedding.text.VertexAiTextEmbeddingModel;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.retry.support.RetryTemplate;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Auto-configuration for Vertex AI Gemini Chat.
+ *
+ * @author Christian Tzolov
+ * @author Mark Pollack
+ * @since 1.0.0
+ */
+@AutoConfiguration(after = { SpringAiRetryAutoConfiguration.class })
+@ConditionalOnClass({ VertexAI.class, VertexAiTextEmbeddingModel.class })
+@EnableConfigurationProperties({ VertexAiEmbeddingConnectionProperties.class, VertexAiTextEmbeddingProperties.class,
+ VertexAiMultimodalEmbeddingProperties.class })
+@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class })
+public class VertexAiEmbeddingAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ public VertexAiEmbeddingConnectionDetails connectionDetails(
+ VertexAiEmbeddingConnectionProperties connectionProperties) {
+
+ Assert.hasText(connectionProperties.getProjectId(), "Vertex AI project-id must be set!");
+ Assert.hasText(connectionProperties.getLocation(), "Vertex AI location must be set!");
+
+ var connectionBuilder = VertexAiEmbeddingConnectionDetails.builder()
+ .projectId(connectionProperties.getProjectId())
+ .location(connectionProperties.getLocation());
+
+ if (StringUtils.hasText(connectionProperties.getApiEndpoint())) {
+ connectionBuilder.apiEndpoint(connectionProperties.getApiEndpoint());
+ }
+
+ return connectionBuilder.build();
+
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = VertexAiTextEmbeddingProperties.CONFIG_PREFIX, name = "enabled",
+ havingValue = "true", matchIfMissing = true)
+ public VertexAiTextEmbeddingModel textEmbedding(VertexAiEmbeddingConnectionDetails connectionDetails,
+ VertexAiTextEmbeddingProperties textEmbeddingProperties, RetryTemplate retryTemplate,
+ ObjectProvider observationRegistry,
+ ObjectProvider observationConvention) {
+
+ var embeddingModel = new VertexAiTextEmbeddingModel(connectionDetails, textEmbeddingProperties.getOptions(),
+ retryTemplate, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
+
+ observationConvention.ifAvailable(embeddingModel::setObservationConvention);
+
+ return embeddingModel;
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = VertexAiMultimodalEmbeddingProperties.CONFIG_PREFIX, name = "enabled",
+ havingValue = "true", matchIfMissing = true)
+ public VertexAiMultimodalEmbeddingModel multimodalEmbedding(VertexAiEmbeddingConnectionDetails connectionDetails,
+ VertexAiMultimodalEmbeddingProperties multimodalEmbeddingProperties) throws IOException {
+
+ return new VertexAiMultimodalEmbeddingModel(connectionDetails, multimodalEmbeddingProperties.getOptions());
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiEmbeddingConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiEmbeddingConnectionProperties.java
new file mode 100644
index 000000000..a55cb3774
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiEmbeddingConnectionProperties.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.vertexai.autoconfigure.embedding;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.core.io.Resource;
+
+/**
+ * Configuration properties for Vertex AI Embedding.
+ *
+ * @author Christian Tzolov
+ * @since 1.0.0
+ */
+@ConfigurationProperties(VertexAiEmbeddingConnectionProperties.CONFIG_PREFIX)
+public class VertexAiEmbeddingConnectionProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.vertex.ai.embedding";
+
+ /**
+ * Vertex AI Gemini project ID.
+ */
+ private String projectId;
+
+ /**
+ * Vertex AI Gemini location.
+ */
+ private String location;
+
+ /**
+ * URI to Vertex AI Gemini credentials (optional)
+ */
+ private Resource credentialsUri;
+
+ /**
+ * Vertex AI Gemini API endpoint.
+ */
+ private String apiEndpoint;
+
+ public String getProjectId() {
+ return this.projectId;
+ }
+
+ public void setProjectId(String projectId) {
+ this.projectId = projectId;
+ }
+
+ public String getLocation() {
+ return this.location;
+ }
+
+ public void setLocation(String location) {
+ this.location = location;
+ }
+
+ public Resource getCredentialsUri() {
+ return this.credentialsUri;
+ }
+
+ public void setCredentialsUri(Resource credentialsUri) {
+ this.credentialsUri = credentialsUri;
+ }
+
+ public String getApiEndpoint() {
+ return this.apiEndpoint;
+ }
+
+ public void setApiEndpoint(String apiEndpoint) {
+ this.apiEndpoint = apiEndpoint;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiMultimodalEmbeddingProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiMultimodalEmbeddingProperties.java
new file mode 100644
index 000000000..e1bb8e2c3
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiMultimodalEmbeddingProperties.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.vertexai.autoconfigure.embedding;
+
+import org.springframework.ai.vertexai.embedding.multimodal.VertexAiMultimodalEmbeddingOptions;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Configuration properties for Vertex AI Gemini Chat.
+ *
+ * @author Christian Tzolov
+ * @since 1.0.0
+ */
+@ConfigurationProperties(VertexAiMultimodalEmbeddingProperties.CONFIG_PREFIX)
+public class VertexAiMultimodalEmbeddingProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.vertex.ai.embedding.multimodal";
+
+ private boolean enabled = true;
+
+ /**
+ * Vertex AI Text Embedding API options.
+ */
+ private VertexAiMultimodalEmbeddingOptions options = VertexAiMultimodalEmbeddingOptions.builder()
+ .model(VertexAiMultimodalEmbeddingOptions.DEFAULT_MODEL_NAME)
+ .build();
+
+ public VertexAiMultimodalEmbeddingOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(VertexAiMultimodalEmbeddingOptions options) {
+ this.options = options;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiTextEmbeddingProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiTextEmbeddingProperties.java
new file mode 100644
index 000000000..737a4a6bc
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiTextEmbeddingProperties.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.vertexai.autoconfigure.embedding;
+
+import org.springframework.ai.vertexai.embedding.text.VertexAiTextEmbeddingOptions;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Configuration properties for Vertex AI Gemini Chat.
+ *
+ * @author Christian Tzolov
+ * @since 1.0.0
+ */
+@ConfigurationProperties(VertexAiTextEmbeddingProperties.CONFIG_PREFIX)
+public class VertexAiTextEmbeddingProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.vertex.ai.embedding.text";
+
+ private boolean enabled = true;
+
+ /**
+ * Vertex AI Text Embedding API options.
+ */
+ private VertexAiTextEmbeddingOptions options = VertexAiTextEmbeddingOptions.builder()
+ .taskType(VertexAiTextEmbeddingOptions.TaskType.RETRIEVAL_DOCUMENT)
+ .model(VertexAiTextEmbeddingOptions.DEFAULT_MODEL_NAME)
+ .build();
+
+ public VertexAiTextEmbeddingOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(VertexAiTextEmbeddingOptions options) {
+ this.options = options;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiAutoConfiguration.java
new file mode 100644
index 000000000..4a059efa2
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiAutoConfiguration.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.vertexai.autoconfigure.gemini;
+
+import java.io.IOException;
+
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.cloud.vertexai.VertexAI;
+import io.micrometer.observation.ObservationRegistry;
+
+import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration;
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.chat.observation.ChatModelObservationConvention;
+import org.springframework.ai.model.tool.ToolCallingManager;
+import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.retry.support.RetryTemplate;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Auto-configuration for Vertex AI Gemini Chat.
+ *
+ * @author Christian Tzolov
+ * @author Soby Chacko
+ * @author Mark Pollack
+ * @since 1.0.0
+ */
+@AutoConfiguration(after = { SpringAiRetryAutoConfiguration.class, ToolCallingAutoConfiguration.class })
+@ConditionalOnClass({ VertexAI.class, VertexAiGeminiChatModel.class })
+@EnableConfigurationProperties({ VertexAiGeminiChatProperties.class, VertexAiGeminiConnectionProperties.class })
+@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, ToolCallingAutoConfiguration.class })
+public class VertexAiGeminiAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ public VertexAI vertexAi(VertexAiGeminiConnectionProperties connectionProperties) throws IOException {
+
+ Assert.hasText(connectionProperties.getProjectId(), "Vertex AI project-id must be set!");
+ Assert.hasText(connectionProperties.getLocation(), "Vertex AI location must be set!");
+ Assert.notNull(connectionProperties.getTransport(), "Vertex AI transport must be set!");
+
+ var vertexAIBuilder = new VertexAI.Builder().setProjectId(connectionProperties.getProjectId())
+ .setLocation(connectionProperties.getLocation())
+ .setTransport(com.google.cloud.vertexai.Transport.valueOf(connectionProperties.getTransport().name()));
+
+ if (StringUtils.hasText(connectionProperties.getApiEndpoint())) {
+ vertexAIBuilder.setApiEndpoint(connectionProperties.getApiEndpoint());
+ }
+ if (!CollectionUtils.isEmpty(connectionProperties.getScopes())) {
+ vertexAIBuilder.setScopes(connectionProperties.getScopes());
+ }
+
+ if (connectionProperties.getCredentialsUri() != null) {
+ GoogleCredentials credentials = GoogleCredentials
+ .fromStream(connectionProperties.getCredentialsUri().getInputStream());
+
+ vertexAIBuilder.setCredentials(credentials);
+ }
+ return vertexAIBuilder.build();
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = VertexAiGeminiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+ public VertexAiGeminiChatModel vertexAiGeminiChat(VertexAI vertexAi, VertexAiGeminiChatProperties chatProperties,
+ ToolCallingManager toolCallingManager, ApplicationContext context, RetryTemplate retryTemplate,
+ ObjectProvider observationRegistry,
+ ObjectProvider observationConvention) {
+
+ VertexAiGeminiChatModel chatModel = VertexAiGeminiChatModel.builder()
+ .vertexAI(vertexAi)
+ .defaultOptions(chatProperties.getOptions())
+ .toolCallingManager(toolCallingManager)
+ .retryTemplate(retryTemplate)
+ .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
+ .build();
+
+ observationConvention.ifAvailable(chatModel::setObservationConvention);
+
+ return chatModel;
+ }
+
+ /**
+ * Because of the OPEN_API_SCHEMA type, the FunctionCallbackResolver instance must
+ * different from the other JSON schema types.
+ */
+ // private FunctionCallbackResolver springAiFunctionManager(ApplicationContext
+ // context) {
+ // DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver();
+ // manager.setSchemaType(SchemaType.OPEN_API_SCHEMA);
+ // manager.setApplicationContext(context);
+ // return manager;
+ // }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiChatProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiChatProperties.java
new file mode 100644
index 000000000..1770e5ed1
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiChatProperties.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.vertexai.autoconfigure.gemini;
+
+import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel;
+import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatOptions;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Configuration properties for Vertex AI Gemini Chat.
+ *
+ * @author Christian Tzolov
+ * @since 0.8.0
+ */
+@ConfigurationProperties(VertexAiGeminiChatProperties.CONFIG_PREFIX)
+public class VertexAiGeminiChatProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.vertex.ai.gemini.chat";
+
+ public static final String DEFAULT_MODEL = VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH.getValue();
+
+ /**
+ * Vertex AI Gemini API generative options.
+ */
+ private VertexAiGeminiChatOptions options = VertexAiGeminiChatOptions.builder()
+ .temperature(0.7)
+ .candidateCount(1)
+ .model(DEFAULT_MODEL)
+ .build();
+
+ public VertexAiGeminiChatOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(VertexAiGeminiChatOptions options) {
+ this.options = options;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiConnectionProperties.java
new file mode 100644
index 000000000..7adf07052
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiConnectionProperties.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.vertexai.autoconfigure.gemini;
+
+import java.util.List;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.core.io.Resource;
+
+/**
+ * Configuration properties for Vertex AI Gemini Chat.
+ *
+ * @author Christian Tzolov
+ * @since 0.8.0
+ */
+@ConfigurationProperties(VertexAiGeminiConnectionProperties.CONFIG_PREFIX)
+public class VertexAiGeminiConnectionProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.vertex.ai.gemini";
+
+ /**
+ * Vertex AI Gemini project ID.
+ */
+ private String projectId;
+
+ /**
+ * Vertex AI Gemini location.
+ */
+ private String location;
+
+ /**
+ * URI to Vertex AI Gemini credentials (optional)
+ */
+ private Resource credentialsUri;
+
+ /**
+ * Vertex AI Gemini API endpoint.
+ */
+ private String apiEndpoint;
+
+ /**
+ *
+ */
+ private List scopes = List.of();
+
+ private Transport transport = Transport.GRPC;
+
+ public String getProjectId() {
+ return this.projectId;
+ }
+
+ public void setProjectId(String projectId) {
+ this.projectId = projectId;
+ }
+
+ public String getLocation() {
+ return this.location;
+ }
+
+ public void setLocation(String location) {
+ this.location = location;
+ }
+
+ public Resource getCredentialsUri() {
+ return this.credentialsUri;
+ }
+
+ public void setCredentialsUri(Resource credentialsUri) {
+ this.credentialsUri = credentialsUri;
+ }
+
+ public String getApiEndpoint() {
+ return this.apiEndpoint;
+ }
+
+ public void setApiEndpoint(String apiEndpoint) {
+ this.apiEndpoint = apiEndpoint;
+ }
+
+ public List getScopes() {
+ return this.scopes;
+ }
+
+ public void setScopes(List scopes) {
+ this.scopes = scopes;
+ }
+
+ public Transport getTransport() {
+ return this.transport;
+ }
+
+ public void setTransport(Transport transport) {
+ this.transport = transport;
+ }
+
+ public enum Transport {
+
+ /** When used, the clients will send REST requests to the backing service. */
+ REST,
+ /**
+ * When used, the clients will send gRPC to the backing service. This is usually
+ * more efficient and is the default transport.
+ */
+ GRPC
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiAutoConfiguration.java
new file mode 100644
index 000000000..fbb20c697
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiAutoConfiguration.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.watsonxai.autoconfigure;
+
+import org.springframework.ai.watsonx.WatsonxAiChatModel;
+import org.springframework.ai.watsonx.WatsonxAiEmbeddingModel;
+import org.springframework.ai.watsonx.api.WatsonxAiApi;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.web.client.RestClient;
+
+/**
+ * WatsonX.ai autoconfiguration class.
+ *
+ * @author Pablo Sanchidrian Herrera
+ * @author John Jario Moreno Rojas
+ * @author Christian Tzolov
+ * @since 1.0.0
+ */
+@AutoConfiguration(after = RestClientAutoConfiguration.class)
+@ConditionalOnClass(WatsonxAiApi.class)
+@EnableConfigurationProperties({ WatsonxAiConnectionProperties.class, WatsonxAiChatProperties.class,
+ WatsonxAiEmbeddingProperties.class })
+@ConditionalOnProperty(prefix = WatsonxAiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+public class WatsonxAiAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ public WatsonxAiApi watsonxApi(WatsonxAiConnectionProperties properties,
+ ObjectProvider restClientBuilderProvider) {
+ return new WatsonxAiApi(properties.getBaseUrl(), properties.getStreamEndpoint(), properties.getTextEndpoint(),
+ properties.getEmbeddingEndpoint(), properties.getProjectId(), properties.getIAMToken(),
+ restClientBuilderProvider.getIfAvailable(RestClient::builder));
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = WatsonxAiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+ public WatsonxAiChatModel watsonxChatModel(WatsonxAiApi watsonxApi, WatsonxAiChatProperties chatProperties) {
+ return new WatsonxAiChatModel(watsonxApi, chatProperties.getOptions());
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = WatsonxAiEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+ public WatsonxAiEmbeddingModel watsonxAiEmbeddingModel(WatsonxAiApi watsonxApi,
+ WatsonxAiEmbeddingProperties properties) {
+ return new WatsonxAiEmbeddingModel(watsonxApi, properties.getOptions());
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiChatProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiChatProperties.java
new file mode 100644
index 000000000..cff01d7fc
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiChatProperties.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.watsonxai.autoconfigure;
+
+import java.util.List;
+
+import org.springframework.ai.watsonx.WatsonxAiChatOptions;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+/**
+ * Chat properties for Watsonx.AI Chat.
+ *
+ * @author Christian Tzolov
+ * @author Alexandros Pappas
+ * @since 1.0.0
+ */
+@ConfigurationProperties(WatsonxAiChatProperties.CONFIG_PREFIX)
+public class WatsonxAiChatProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.watsonx.ai.chat";
+
+ /**
+ * Enable Watsonx.AI chat model.
+ */
+ private boolean enabled = true;
+
+ /**
+ * Watsonx AI generative options.
+ */
+ @NestedConfigurationProperty
+ private WatsonxAiChatOptions options = WatsonxAiChatOptions.builder()
+ .model("google/flan-ul2")
+ .temperature(0.7)
+ .topP(1.0)
+ .topK(50)
+ .decodingMethod("greedy")
+ .maxNewTokens(20)
+ .minNewTokens(0)
+ .repetitionPenalty(1.0)
+ .stopSequences(List.of())
+ .build();
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public WatsonxAiChatOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(WatsonxAiChatOptions options) {
+ this.options = options;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiConnectionProperties.java
new file mode 100644
index 000000000..a49ab048c
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiConnectionProperties.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.watsonxai.autoconfigure;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * WatsonX.ai connection autoconfiguration properties.
+ *
+ * @author Pablo Sanchidrian Herrera
+ * @author John Jario Moreno Rojas
+ * @since 1.0.0
+ */
+@ConfigurationProperties(WatsonxAiConnectionProperties.CONFIG_PREFIX)
+public class WatsonxAiConnectionProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.watsonx.ai";
+
+ private String baseUrl = "https://us-south.ml.cloud.ibm.com/";
+
+ private String streamEndpoint = "ml/v1/text/generation_stream?version=2023-05-29";
+
+ private String textEndpoint = "ml/v1/text/generation?version=2023-05-29";
+
+ private String embeddingEndpoint = "ml/v1/text/embeddings?version=2023-05-29";
+
+ private String projectId;
+
+ private String IAMToken;
+
+ public String getBaseUrl() {
+ return this.baseUrl;
+ }
+
+ public void setBaseUrl(String baseUrl) {
+ this.baseUrl = baseUrl;
+ }
+
+ public String getStreamEndpoint() {
+ return this.streamEndpoint;
+ }
+
+ public void setStreamEndpoint(String streamEndpoint) {
+ this.streamEndpoint = streamEndpoint;
+ }
+
+ public String getTextEndpoint() {
+ return this.textEndpoint;
+ }
+
+ public void setTextEndpoint(String textEndpoint) {
+ this.textEndpoint = textEndpoint;
+ }
+
+ public String getEmbeddingEndpoint() {
+ return this.embeddingEndpoint;
+ }
+
+ public void setEmbeddingEndpoint(String embeddingEndpoint) {
+ this.embeddingEndpoint = embeddingEndpoint;
+ }
+
+ public String getProjectId() {
+ return this.projectId;
+ }
+
+ public void setProjectId(String projectId) {
+ this.projectId = projectId;
+ }
+
+ public String getIAMToken() {
+ return this.IAMToken;
+ }
+
+ public void setIAMToken(String IAMToken) {
+ this.IAMToken = IAMToken;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiEmbeddingProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiEmbeddingProperties.java
new file mode 100644
index 000000000..1691951f0
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiEmbeddingProperties.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.watsonxai.autoconfigure;
+
+import org.springframework.ai.watsonx.WatsonxAiEmbeddingOptions;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+/**
+ * Watsonx.ai Embedding autoconfiguration properties.
+ *
+ * @author Pablo Sanchidrian Herrera
+ * @since 1.0.0
+ */
+@ConfigurationProperties(WatsonxAiEmbeddingProperties.CONFIG_PREFIX)
+public class WatsonxAiEmbeddingProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.watsonx.ai.embedding";
+
+ /**
+ * Enable Watsonx.ai embedding model.
+ */
+ private boolean enabled = true;
+
+ /**
+ * Client lever Watsonx.ai embedding options. Use this property to configure the
+ * model. The null values are ignored defaulting to the defaults.
+ */
+ @NestedConfigurationProperty
+ private WatsonxAiEmbeddingOptions options = WatsonxAiEmbeddingOptions.create()
+ .withModel(WatsonxAiEmbeddingOptions.DEFAULT_MODEL);
+
+ public String getModel() {
+ return this.options.getModel();
+ }
+
+ public void setModel(String model) {
+ this.options.setModel(model);
+ }
+
+ public WatsonxAiEmbeddingOptions getOptions() {
+ return this.options;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfiguration.java
new file mode 100644
index 000000000..db27df995
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfiguration.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.zhipuai.autoconfigure;
+
+import java.util.List;
+
+import io.micrometer.observation.ObservationRegistry;
+
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.chat.observation.ChatModelObservationConvention;
+import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
+import org.springframework.ai.model.function.DefaultFunctionCallbackResolver;
+import org.springframework.ai.model.function.FunctionCallback;
+import org.springframework.ai.model.function.FunctionCallbackResolver;
+import org.springframework.ai.zhipuai.ZhiPuAiChatModel;
+import org.springframework.ai.zhipuai.ZhiPuAiEmbeddingModel;
+import org.springframework.ai.zhipuai.ZhiPuAiImageModel;
+import org.springframework.ai.zhipuai.api.ZhiPuAiApi;
+import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.retry.support.RetryTemplate;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.ResponseErrorHandler;
+import org.springframework.web.client.RestClient;
+
+/**
+ * {@link AutoConfiguration Auto-configuration} for ZhiPuAI.
+ *
+ * @author Geng Rong
+ */
+@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class })
+@ConditionalOnClass(ZhiPuAiApi.class)
+@EnableConfigurationProperties({ ZhiPuAiConnectionProperties.class, ZhiPuAiChatProperties.class,
+ ZhiPuAiEmbeddingProperties.class, ZhiPuAiImageProperties.class })
+public class ZhiPuAiAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = ZhiPuAiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+ public ZhiPuAiChatModel zhiPuAiChatModel(ZhiPuAiConnectionProperties commonProperties,
+ ZhiPuAiChatProperties chatProperties, ObjectProvider restClientBuilderProvider,
+ List toolFunctionCallbacks, FunctionCallbackResolver functionCallbackResolver,
+ RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler,
+ ObjectProvider observationRegistry,
+ ObjectProvider observationConvention) {
+
+ var zhiPuAiApi = zhiPuAiApi(chatProperties.getBaseUrl(), commonProperties.getBaseUrl(),
+ chatProperties.getApiKey(), commonProperties.getApiKey(),
+ restClientBuilderProvider.getIfAvailable(RestClient::builder), responseErrorHandler);
+
+ var chatModel = new ZhiPuAiChatModel(zhiPuAiApi, chatProperties.getOptions(), functionCallbackResolver,
+ toolFunctionCallbacks, retryTemplate, observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
+
+ observationConvention.ifAvailable(chatModel::setObservationConvention);
+
+ return chatModel;
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = ZhiPuAiEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+ public ZhiPuAiEmbeddingModel zhiPuAiEmbeddingModel(ZhiPuAiConnectionProperties commonProperties,
+ ZhiPuAiEmbeddingProperties embeddingProperties, RestClient.Builder restClientBuilder,
+ RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler,
+ ObjectProvider observationRegistry,
+ ObjectProvider observationConvention) {
+
+ var zhiPuAiApi = zhiPuAiApi(embeddingProperties.getBaseUrl(), commonProperties.getBaseUrl(),
+ embeddingProperties.getApiKey(), commonProperties.getApiKey(), restClientBuilder, responseErrorHandler);
+
+ var embeddingModel = new ZhiPuAiEmbeddingModel(zhiPuAiApi, embeddingProperties.getMetadataMode(),
+ embeddingProperties.getOptions(), retryTemplate,
+ observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
+
+ observationConvention.ifAvailable(embeddingModel::setObservationConvention);
+
+ return embeddingModel;
+ }
+
+ private ZhiPuAiApi zhiPuAiApi(String baseUrl, String commonBaseUrl, String apiKey, String commonApiKey,
+ RestClient.Builder restClientBuilder, ResponseErrorHandler responseErrorHandler) {
+
+ String resolvedBaseUrl = StringUtils.hasText(baseUrl) ? baseUrl : commonBaseUrl;
+ Assert.hasText(resolvedBaseUrl, "ZhiPuAI base URL must be set");
+
+ String resolvedApiKey = StringUtils.hasText(apiKey) ? apiKey : commonApiKey;
+ Assert.hasText(resolvedApiKey, "ZhiPuAI API key must be set");
+
+ return new ZhiPuAiApi(resolvedBaseUrl, resolvedApiKey, restClientBuilder, responseErrorHandler);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = ZhiPuAiImageProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+ public ZhiPuAiImageModel zhiPuAiImageModel(ZhiPuAiConnectionProperties commonProperties,
+ ZhiPuAiImageProperties imageProperties, RestClient.Builder restClientBuilder, RetryTemplate retryTemplate,
+ ResponseErrorHandler responseErrorHandler) {
+
+ String apiKey = StringUtils.hasText(imageProperties.getApiKey()) ? imageProperties.getApiKey()
+ : commonProperties.getApiKey();
+
+ String baseUrl = StringUtils.hasText(imageProperties.getBaseUrl()) ? imageProperties.getBaseUrl()
+ : commonProperties.getBaseUrl();
+
+ Assert.hasText(apiKey, "ZhiPuAI API key must be set");
+ Assert.hasText(baseUrl, "ZhiPuAI base URL must be set");
+
+ var zhiPuAiImageApi = new ZhiPuAiImageApi(baseUrl, apiKey, restClientBuilder, responseErrorHandler);
+
+ return new ZhiPuAiImageModel(zhiPuAiImageApi, imageProperties.getOptions(), retryTemplate);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) {
+ DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver();
+ manager.setApplicationContext(context);
+ return manager;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiChatProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiChatProperties.java
new file mode 100644
index 000000000..e5f44f475
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiChatProperties.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.zhipuai.autoconfigure;
+
+import org.springframework.ai.zhipuai.ZhiPuAiChatOptions;
+import org.springframework.ai.zhipuai.api.ZhiPuAiApi;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+/**
+ * Configuration properties for ZhiPuAI chat model.
+ *
+ * @author Geng Rong
+ */
+@ConfigurationProperties(ZhiPuAiChatProperties.CONFIG_PREFIX)
+public class ZhiPuAiChatProperties extends ZhiPuAiParentProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.zhipuai.chat";
+
+ public static final String DEFAULT_CHAT_MODEL = ZhiPuAiApi.ChatModel.GLM_4_Air.value;
+
+ private static final Double DEFAULT_TEMPERATURE = 0.7;
+
+ /**
+ * Enable ZhiPuAI chat model.
+ */
+ private boolean enabled = true;
+
+ @NestedConfigurationProperty
+ private ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder()
+ .model(DEFAULT_CHAT_MODEL)
+ .temperature(DEFAULT_TEMPERATURE)
+ .build();
+
+ public ZhiPuAiChatOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(ZhiPuAiChatOptions options) {
+ this.options = options;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiConnectionProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiConnectionProperties.java
new file mode 100644
index 000000000..3e3aee144
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiConnectionProperties.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.zhipuai.autoconfigure;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties(ZhiPuAiConnectionProperties.CONFIG_PREFIX)
+public class ZhiPuAiConnectionProperties extends ZhiPuAiParentProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.zhipuai";
+
+ public static final String DEFAULT_BASE_URL = "https://open.bigmodel.cn/api/paas";
+
+ public ZhiPuAiConnectionProperties() {
+ super.setBaseUrl(DEFAULT_BASE_URL);
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiEmbeddingProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiEmbeddingProperties.java
new file mode 100644
index 000000000..300b0a23d
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiEmbeddingProperties.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.zhipuai.autoconfigure;
+
+import org.springframework.ai.document.MetadataMode;
+import org.springframework.ai.zhipuai.ZhiPuAiEmbeddingOptions;
+import org.springframework.ai.zhipuai.api.ZhiPuAiApi;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+/**
+ * Configuration properties for ZhiPuAI embedding model.
+ *
+ * @author Geng Rong
+ */
+@ConfigurationProperties(ZhiPuAiEmbeddingProperties.CONFIG_PREFIX)
+public class ZhiPuAiEmbeddingProperties extends ZhiPuAiParentProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.zhipuai.embedding";
+
+ public static final String DEFAULT_EMBEDDING_MODEL = ZhiPuAiApi.EmbeddingModel.Embedding_2.value;
+
+ /**
+ * Enable ZhiPuAI embedding model.
+ */
+ private boolean enabled = true;
+
+ private MetadataMode metadataMode = MetadataMode.EMBED;
+
+ @NestedConfigurationProperty
+ private ZhiPuAiEmbeddingOptions options = ZhiPuAiEmbeddingOptions.builder().model(DEFAULT_EMBEDDING_MODEL).build();
+
+ public ZhiPuAiEmbeddingOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(ZhiPuAiEmbeddingOptions options) {
+ this.options = options;
+ }
+
+ public MetadataMode getMetadataMode() {
+ return this.metadataMode;
+ }
+
+ public void setMetadataMode(MetadataMode metadataMode) {
+ this.metadataMode = metadataMode;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiImageProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiImageProperties.java
new file mode 100644
index 000000000..bfcceee5e
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiImageProperties.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.zhipuai.autoconfigure;
+
+import org.springframework.ai.zhipuai.ZhiPuAiImageOptions;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+/**
+ * Configuration properties for ZhiPuAI chat model.
+ *
+ * @author Geng Rong
+ */
+@ConfigurationProperties(ZhiPuAiImageProperties.CONFIG_PREFIX)
+public class ZhiPuAiImageProperties extends ZhiPuAiParentProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.zhipuai.image";
+
+ /**
+ * Enable ZhiPuAI image model.
+ */
+ private boolean enabled = true;
+
+ /**
+ * Options for ZhiPuAI Image API.
+ */
+ @NestedConfigurationProperty
+ private ZhiPuAiImageOptions options = ZhiPuAiImageOptions.builder().build();
+
+ public ZhiPuAiImageOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(ZhiPuAiImageOptions options) {
+ this.options = options;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiParentProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiParentProperties.java
new file mode 100644
index 000000000..5a8e3f193
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiParentProperties.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.zhipuai.autoconfigure;
+
+/**
+ * @author Geng Rong
+ */
+class ZhiPuAiParentProperties {
+
+ private String apiKey;
+
+ private String baseUrl;
+
+ public String getApiKey() {
+ return this.apiKey;
+ }
+
+ public void setApiKey(String apiKey) {
+ this.apiKey = apiKey;
+ }
+
+ public String getBaseUrl() {
+ return this.baseUrl;
+ }
+
+ public void setBaseUrl(String baseUrl) {
+ this.baseUrl = baseUrl;
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryAutoConfiguration.java
new file mode 100644
index 000000000..f44ec9613
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryAutoConfiguration.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.retry.autoconfigure;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.ai.retry.NonTransientAiException;
+import org.springframework.ai.retry.RetryUtils;
+import org.springframework.ai.retry.TransientAiException;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.lang.NonNull;
+import org.springframework.retry.RetryCallback;
+import org.springframework.retry.RetryContext;
+import org.springframework.retry.RetryListener;
+import org.springframework.retry.support.RetryTemplate;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StreamUtils;
+import org.springframework.web.client.ResponseErrorHandler;
+
+/**
+ * {@link AutoConfiguration Auto-configuration} for AI Retry.
+ *
+ * @author Christian Tzolov
+ */
+@AutoConfiguration
+@ConditionalOnClass(RetryUtils.class)
+@EnableConfigurationProperties({ SpringAiRetryProperties.class })
+public class SpringAiRetryAutoConfiguration {
+
+ private static final Logger logger = LoggerFactory.getLogger(SpringAiRetryAutoConfiguration.class);
+
+ @Bean
+ @ConditionalOnMissingBean
+ public RetryTemplate retryTemplate(SpringAiRetryProperties properties) {
+ return RetryTemplate.builder()
+ .maxAttempts(properties.getMaxAttempts())
+ .retryOn(TransientAiException.class)
+ .exponentialBackoff(properties.getBackoff().getInitialInterval(), properties.getBackoff().getMultiplier(),
+ properties.getBackoff().getMaxInterval())
+ .withListener(new RetryListener() {
+
+ @Override
+ public void onError(RetryContext context,
+ RetryCallback callback, Throwable throwable) {
+ logger.warn("Retry error. Retry count:" + context.getRetryCount(), throwable);
+ }
+ })
+ .build();
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public ResponseErrorHandler responseErrorHandler(SpringAiRetryProperties properties) {
+
+ return new ResponseErrorHandler() {
+
+ @Override
+ public boolean hasError(@NonNull ClientHttpResponse response) throws IOException {
+ return response.getStatusCode().isError();
+ }
+
+ @Override
+ public void handleError(@NonNull ClientHttpResponse response) throws IOException {
+ if (response.getStatusCode().isError()) {
+ String error = StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8);
+ String message = String.format("%s - %s", response.getStatusCode().value(), error);
+
+ // Explicitly configured transient codes
+ if (properties.getOnHttpCodes().contains(response.getStatusCode().value())) {
+ throw new TransientAiException(message);
+ }
+
+ // onClientErrors - If true, do not throw a NonTransientAiException,
+ // and do not attempt retry for 4xx client error codes, false by
+ // default.
+ if (!properties.isOnClientErrors() && response.getStatusCode().is4xxClientError()) {
+ throw new NonTransientAiException(message);
+ }
+
+ // Explicitly configured non-transient codes
+ if (!CollectionUtils.isEmpty(properties.getExcludeOnHttpCodes())
+ && properties.getExcludeOnHttpCodes().contains(response.getStatusCode().value())) {
+ throw new NonTransientAiException(message);
+ }
+ throw new TransientAiException(message);
+ }
+ }
+ };
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryProperties.java
new file mode 100644
index 000000000..385103a94
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryProperties.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.retry.autoconfigure;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+/**
+ * Properties for AI Retry.
+ *
+ * @author Christian Tzolov
+ */
+@ConfigurationProperties(SpringAiRetryProperties.CONFIG_PREFIX)
+public class SpringAiRetryProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.retry";
+
+ /**
+ * Maximum number of retry attempts.
+ */
+ private int maxAttempts = 10;
+
+ /**
+ * Exponential Backoff properties.
+ */
+ @NestedConfigurationProperty
+ private Backoff backoff = new Backoff();
+
+ /**
+ * If false, throw a NonTransientAiException, and do not attempt retry for 4xx client
+ * error codes. False by default. If true, throw a TransientAiException, and attempt
+ * retry for 4xx client.
+ */
+ private boolean onClientErrors = false;
+
+ /**
+ * List of HTTP status codes that should not trigger a retry (e.g. throw
+ * NonTransientAiException).
+ */
+ private List excludeOnHttpCodes = new ArrayList<>();
+
+ /**
+ * List of HTTP status codes that should trigger a retry.
+ */
+ private List onHttpCodes = new ArrayList<>();
+
+ public int getMaxAttempts() {
+ return this.maxAttempts;
+ }
+
+ public void setMaxAttempts(int maxAttempts) {
+ this.maxAttempts = maxAttempts;
+ }
+
+ public Backoff getBackoff() {
+ return this.backoff;
+ }
+
+ public List getExcludeOnHttpCodes() {
+ return this.excludeOnHttpCodes;
+ }
+
+ public void setExcludeOnHttpCodes(List onHttpCodes) {
+ this.excludeOnHttpCodes = onHttpCodes;
+ }
+
+ public boolean isOnClientErrors() {
+ return this.onClientErrors;
+ }
+
+ public void setOnClientErrors(boolean onClientErrors) {
+ this.onClientErrors = onClientErrors;
+ }
+
+ public List getOnHttpCodes() {
+ return this.onHttpCodes;
+ }
+
+ public void setOnHttpCodes(List onHttpCodes) {
+ this.onHttpCodes = onHttpCodes;
+ }
+
+ /**
+ * Exponential Backoff properties.
+ */
+ public static class Backoff {
+
+ /**
+ * Initial sleep duration.
+ */
+ private Duration initialInterval = Duration.ofMillis(2000);
+
+ /**
+ * Backoff interval multiplier.
+ */
+ private int multiplier = 5;
+
+ /**
+ * Maximum backoff duration.
+ */
+ private Duration maxInterval = Duration.ofMillis(3 * 60000);
+
+ public Duration getInitialInterval() {
+ return this.initialInterval;
+ }
+
+ public void setInitialInterval(Duration initialInterval) {
+ this.initialInterval = initialInterval;
+ }
+
+ public int getMultiplier() {
+ return this.multiplier;
+ }
+
+ public void setMultiplier(int multiplier) {
+ this.multiplier = multiplier;
+ }
+
+ public Duration getMaxInterval() {
+ return this.maxInterval;
+ }
+
+ public void setMaxInterval(Duration maxInterval) {
+ this.maxInterval = maxInterval;
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index 132c474a9..5462fc55d 100644
--- a/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -14,21 +14,21 @@
# limitations under the License.
#
-org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration
-org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiAutoConfiguration
-org.springframework.ai.autoconfigure.oci.genai.OCIGenAiAutoConfiguration
-org.springframework.ai.autoconfigure.stabilityai.StabilityAiImageAutoConfiguration
-org.springframework.ai.autoconfigure.transformers.TransformersEmbeddingModelAutoConfiguration
-org.springframework.ai.autoconfigure.huggingface.HuggingfaceChatAutoConfiguration
-org.springframework.ai.autoconfigure.vertexai.gemini.VertexAiGeminiAutoConfiguration
-org.springframework.ai.autoconfigure.bedrock.cohere.BedrockCohereEmbeddingAutoConfiguration
-org.springframework.ai.autoconfigure.bedrock.titan.BedrockTitanEmbeddingAutoConfiguration
-org.springframework.ai.autoconfigure.bedrock.converse.BedrockConverseProxyChatAutoConfiguration
+org.springframework.ai.model.openai.autoconfigure.OpenAiAutoConfiguration
+org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration
+org.springframework.ai.model.oci.genai.autoconfigure.OCIGenAiAutoConfiguration
+org.springframework.ai.model.stabilityai.autoconfigure.StabilityAiImageAutoConfiguration
+org.springframework.ai.model.transformers.autoconfigure.TransformersEmbeddingModelAutoConfiguration
+org.springframework.ai.model.huggingface.autoconfigure.HuggingfaceChatAutoConfiguration
+gemini.org.springframework.ai.model.vertexai.autoconfigure.VertexAiGeminiAutoConfiguration
+cohere.org.springframework.ai.model.bedrock.autoconfigure.BedrockCohereEmbeddingAutoConfiguration
+titan.org.springframework.ai.model.bedrock.autoconfigure.BedrockTitanEmbeddingAutoConfiguration
+converse.org.springframework.ai.model.bedrock.autoconfigure.BedrockConverseProxyChatAutoConfiguration
org.springframework.ai.autoconfigure.chat.observation.ChatObservationAutoConfiguration
org.springframework.ai.autoconfigure.embedding.observation.EmbeddingObservationAutoConfiguration
org.springframework.ai.autoconfigure.image.observation.ImageObservationAutoConfiguration
-org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration
-org.springframework.ai.autoconfigure.mistralai.MistralAiAutoConfiguration
+org.springframework.ai.model.ollama.autoconfigure.OllamaAutoConfiguration
+org.springframework.ai.model.mistralai.autoconfigure.MistralAiAutoConfiguration
org.springframework.ai.autoconfigure.vectorstore.oracle.OracleVectorStoreAutoConfiguration
org.springframework.ai.autoconfigure.vectorstore.pgvector.PgVectorStoreAutoConfiguration
org.springframework.ai.autoconfigure.vectorstore.pinecone.PineconeVectorStoreAutoConfiguration
@@ -41,22 +41,22 @@ org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoCon
org.springframework.ai.autoconfigure.vectorstore.hanadb.HanaCloudVectorStoreAutoConfiguration
org.springframework.ai.autoconfigure.vectorstore.cosmosdb.CosmosDBVectorStoreAutoConfiguration
org.springframework.ai.autoconfigure.vectorstore.mariadb.MariaDbStoreAutoConfiguration
-org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration
-org.springframework.ai.autoconfigure.postgresml.PostgresMlAutoConfiguration
+org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration
+org.springframework.ai.model.postgresml.autoconfigure.PostgresMlAutoConfiguration
org.springframework.ai.autoconfigure.vectorstore.mongo.MongoDBAtlasVectorStoreAutoConfiguration
-org.springframework.ai.autoconfigure.anthropic.AnthropicAutoConfiguration
-org.springframework.ai.autoconfigure.watsonxai.WatsonxAiAutoConfiguration
+org.springframework.ai.model.anthropic.autoconfigure.AnthropicAutoConfiguration
+org.springframework.ai.model.watsonxai.autoconfigure.WatsonxAiAutoConfiguration
org.springframework.ai.autoconfigure.vectorstore.elasticsearch.ElasticsearchVectorStoreAutoConfiguration
org.springframework.ai.autoconfigure.vectorstore.gemfire.GemFireVectorStoreAutoConfiguration
org.springframework.ai.autoconfigure.vectorstore.cassandra.CassandraVectorStoreAutoConfiguration
-org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiAutoConfiguration
-org.springframework.ai.autoconfigure.chat.client.ChatClientAutoConfiguration
+org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiAutoConfiguration
+org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration
org.springframework.ai.autoconfigure.vectorstore.typesense.TypesenseVectorStoreAutoConfiguration
org.springframework.ai.autoconfigure.vectorstore.opensearch.OpenSearchVectorStoreAutoConfiguration
-org.springframework.ai.autoconfigure.moonshot.MoonshotAutoConfiguration
-org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration
-org.springframework.ai.autoconfigure.minimax.MiniMaxAutoConfiguration
-org.springframework.ai.autoconfigure.vertexai.embedding.VertexAiEmbeddingAutoConfiguration
-org.springframework.ai.autoconfigure.chat.memory.cassandra.CassandraChatMemoryAutoConfiguration
+org.springframework.ai.model.moonshot.autoconfigure.MoonshotAutoConfiguration
+org.springframework.ai.model.qianfan.autoconfigure.QianFanAutoConfiguration
+org.springframework.ai.model.minimax.autoconfigure.MiniMaxAutoConfiguration
+embedding.org.springframework.ai.model.vertexai.autoconfigure.VertexAiEmbeddingAutoConfiguration
+cassandra.org.springframework.ai.model.chat.memory.autoconfigure.CassandraChatMemoryAutoConfiguration
org.springframework.ai.autoconfigure.vectorstore.observation.VectorStoreObservationAutoConfiguration
org.springframework.ai.autoconfigure.vectorstore.couchbase.CouchbaseSearchVectorStoreAutoConfiguration
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/elasticsearch/ElasticsearchVectorStoreAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/elasticsearch/ElasticsearchVectorStoreAutoConfigurationIT.java
index cb7d1d990..a0c5ee9b7 100644
--- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/elasticsearch/ElasticsearchVectorStoreAutoConfigurationIT.java
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/elasticsearch/ElasticsearchVectorStoreAutoConfigurationIT.java
@@ -29,8 +29,8 @@ import org.testcontainers.elasticsearch.ElasticsearchContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
-import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
-import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.model.openai.autoconfigure.OpenAiAutoConfiguration;
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.ai.document.Document;
import org.springframework.ai.observation.conventions.VectorStoreProvider;
import org.springframework.ai.vectorstore.elasticsearch.ElasticsearchVectorStore;
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/hanadb/HanaCloudVectorStoreAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/hanadb/HanaCloudVectorStoreAutoConfigurationIT.java
index 98f4def3e..fa63d27ce 100644
--- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/hanadb/HanaCloudVectorStoreAutoConfigurationIT.java
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/hanadb/HanaCloudVectorStoreAutoConfigurationIT.java
@@ -23,8 +23,8 @@ import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
-import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
-import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.model.openai.autoconfigure.OpenAiAutoConfiguration;
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.boot.autoconfigure.AutoConfigurations;
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/mongo/MongoDBAtlasVectorStoreAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/mongo/MongoDBAtlasVectorStoreAutoConfigurationIT.java
index c374f1165..904dfbf18 100644
--- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/mongo/MongoDBAtlasVectorStoreAutoConfigurationIT.java
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/mongo/MongoDBAtlasVectorStoreAutoConfigurationIT.java
@@ -27,8 +27,8 @@ import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.mongodb.MongoDBAtlasLocalContainer;
-import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
-import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.model.openai.autoconfigure.OpenAiAutoConfiguration;
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.ai.document.Document;
import org.springframework.ai.observation.conventions.VectorStoreProvider;
import org.springframework.ai.vectorstore.SearchRequest;
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/opensearch/AwsOpenSearchVectorStoreAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/opensearch/AwsOpenSearchVectorStoreAutoConfigurationIT.java
index 5b14d68e8..da0206bf1 100644
--- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/opensearch/AwsOpenSearchVectorStoreAutoConfigurationIT.java
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/opensearch/AwsOpenSearchVectorStoreAutoConfigurationIT.java
@@ -32,7 +32,7 @@ import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
-import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.transformers.TransformersEmbeddingModel;
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/opensearch/OpenSearchVectorStoreAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/opensearch/OpenSearchVectorStoreAutoConfigurationIT.java
index 0a4ea7d0b..0dfb5e11a 100644
--- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/opensearch/OpenSearchVectorStoreAutoConfigurationIT.java
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vectorstore/opensearch/OpenSearchVectorStoreAutoConfigurationIT.java
@@ -31,7 +31,7 @@ import org.testcontainers.utility.DockerImageName;
import software.amazon.awssdk.http.apache.ApacheHttpClient;
import software.amazon.awssdk.regions.Region;
-import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.observation.conventions.VectorStoreProvider;
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicAutoConfigurationIT.java
new file mode 100644
index 000000000..a46ff4022
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicAutoConfigurationIT.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.anthropic.autoconfigure;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.anthropic.AnthropicChatModel;
+import org.springframework.ai.anthropic.AnthropicChatOptions;
+import org.springframework.ai.anthropic.api.AnthropicApi;
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.model.Generation;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnabledIfEnvironmentVariable(named = "ANTHROPIC_API_KEY", matches = ".*")
+public class AnthropicAutoConfigurationIT {
+
+ private static final Log logger = LogFactory.getLog(AnthropicAutoConfigurationIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.anthropic.apiKey=" + System.getenv("ANTHROPIC_API_KEY"))
+ .withConfiguration(AutoConfigurations.of(AnthropicAutoConfiguration.class));
+
+ @Test
+ void call() {
+ this.contextRunner.run(context -> {
+ AnthropicChatModel chatModel = context.getBean(AnthropicChatModel.class);
+ String response = chatModel.call("Hello");
+ assertThat(response).isNotEmpty();
+ logger.info("Response: " + response);
+ });
+ }
+
+ @Test
+ void callWith8KResponseContext() {
+ this.contextRunner
+ .withPropertyValues("spring.ai.anthropic.beta-version=" + AnthropicApi.BETA_MAX_TOKENS,
+ "spring.ai.anthropic.chat.options.model=" + AnthropicApi.ChatModel.CLAUDE_3_5_SONNET.getValue())
+ .run(context -> {
+ AnthropicChatModel chatModel = context.getBean(AnthropicChatModel.class);
+ var optoins = AnthropicChatOptions.builder().maxTokens(8192).build();
+ var response = chatModel.call(new Prompt("Tell me a joke", optoins));
+ assertThat(response.getResult().getOutput().getText()).isNotEmpty();
+ logger.info("Response: " + response);
+ });
+ }
+
+ @Test
+ void stream() {
+ this.contextRunner.run(context -> {
+ AnthropicChatModel chatModel = context.getBean(AnthropicChatModel.class);
+ Flux responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello")));
+
+ String response = responseFlux.collectList()
+ .block()
+ .stream()
+ .map(ChatResponse::getResults)
+ .flatMap(List::stream)
+ .map(Generation::getOutput)
+ .map(AssistantMessage::getText)
+ .collect(Collectors.joining());
+
+ assertThat(response).isNotEmpty();
+ logger.info("Response: " + response);
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicPropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicPropertiesTests.java
new file mode 100644
index 000000000..ceb714227
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicPropertiesTests.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.anthropic.autoconfigure;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.anthropic.AnthropicChatModel;
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit Tests for {@link AnthropicChatProperties}, {@link AnthropicConnectionProperties}.
+ */
+public class AnthropicPropertiesTests {
+
+ @Test
+ public void connectionProperties() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.anthropic.base-url=TEST_BASE_URL",
+ "spring.ai.anthropic.api-key=abc123",
+ "spring.ai.anthropic.version=6666",
+ "spring.ai.anthropic.beta-version=7777",
+ "spring.ai.anthropic.chat.options.model=MODEL_XYZ",
+ "spring.ai.anthropic.chat.options.temperature=0.55")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, AnthropicAutoConfiguration.class))
+ .run(context -> {
+ var chatProperties = context.getBean(AnthropicChatProperties.class);
+ var connectionProperties = context.getBean(AnthropicConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+ assertThat(connectionProperties.getVersion()).isEqualTo("6666");
+ assertThat(connectionProperties.getBetaVersion()).isEqualTo("7777");
+
+ assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55);
+ // enabled is true by default
+ assertThat(chatProperties.isEnabled()).isTrue();
+ });
+ }
+
+ @Test
+ public void chatOptionsTest() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.anthropic.api-key=API_KEY",
+ "spring.ai.anthropic.base-url=TEST_BASE_URL",
+
+ "spring.ai.anthropic.chat.options.model=MODEL_XYZ",
+ "spring.ai.anthropic.chat.options.max-tokens=123",
+ "spring.ai.anthropic.chat.options.metadata.user-id=MyUserId",
+ "spring.ai.anthropic.chat.options.stop_sequences=boza,koza",
+
+ "spring.ai.anthropic.chat.options.temperature=0.55",
+ "spring.ai.anthropic.chat.options.top-p=0.56",
+ "spring.ai.anthropic.chat.options.top-k=100"
+ )
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, AnthropicAutoConfiguration.class))
+ .run(context -> {
+ var chatProperties = context.getBean(AnthropicChatProperties.class);
+ var connectionProperties = context.getBean(AnthropicConnectionProperties.class);
+
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+ assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY");
+ assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(chatProperties.getOptions().getMaxTokens()).isEqualTo(123);
+ assertThat(chatProperties.getOptions().getStopSequences()).contains("boza", "koza");
+ assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55);
+ assertThat(chatProperties.getOptions().getTopP()).isEqualTo(0.56);
+ assertThat(chatProperties.getOptions().getTopK()).isEqualTo(100);
+
+ assertThat(chatProperties.getOptions().getMetadata().userId()).isEqualTo("MyUserId");
+ });
+ }
+
+ @Test
+ public void chatCompletionDisabled() {
+
+ // It is enabled by default
+ new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, AnthropicAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(AnthropicChatProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(AnthropicChatModel.class)).isNotEmpty();
+ });
+
+ // Explicitly enable the chat auto-configuration.
+ new ApplicationContextRunner().withPropertyValues("spring.ai.anthropic.chat.enabled=true")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, AnthropicAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(AnthropicChatProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(AnthropicChatModel.class)).isNotEmpty();
+ });
+
+ // Explicitly disable the chat auto-configuration.
+ new ApplicationContextRunner().withPropertyValues("spring.ai.anthropic.chat.enabled=false")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, AnthropicAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(AnthropicChatProperties.class)).isEmpty();
+ assertThat(context.getBeansOfType(AnthropicChatModel.class)).isEmpty();
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/tool/FunctionCallWithFunctionBeanIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java
similarity index 92%
rename from spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/tool/FunctionCallWithFunctionBeanIT.java
rename to spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java
index 6ed3e2d14..fb4e53e86 100644
--- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/tool/FunctionCallWithFunctionBeanIT.java
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package org.springframework.ai.autoconfigure.anthropic.tool;
+package org.springframework.ai.model.anthropic.autoconfigure.tool;
import java.util.List;
import java.util.function.Function;
@@ -27,9 +27,9 @@ import org.slf4j.LoggerFactory;
import org.springframework.ai.anthropic.AnthropicChatModel;
import org.springframework.ai.anthropic.AnthropicChatOptions;
import org.springframework.ai.anthropic.api.AnthropicApi;
-import org.springframework.ai.autoconfigure.anthropic.AnthropicAutoConfiguration;
-import org.springframework.ai.autoconfigure.anthropic.tool.MockWeatherService.Request;
-import org.springframework.ai.autoconfigure.anthropic.tool.MockWeatherService.Response;
+import org.springframework.ai.model.anthropic.autoconfigure.AnthropicAutoConfiguration;
+import org.springframework.ai.model.anthropic.autoconfigure.tool.MockWeatherService.Request;
+import org.springframework.ai.model.anthropic.autoconfigure.tool.MockWeatherService.Response;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/tool/FunctionCallWithPromptFunctionIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithPromptFunctionIT.java
similarity index 94%
rename from spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/tool/FunctionCallWithPromptFunctionIT.java
rename to spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithPromptFunctionIT.java
index 4b01cdb63..27b8d2dab 100644
--- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/tool/FunctionCallWithPromptFunctionIT.java
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithPromptFunctionIT.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package org.springframework.ai.autoconfigure.anthropic.tool;
+package org.springframework.ai.model.anthropic.autoconfigure.tool;
import java.util.List;
@@ -26,7 +26,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.ai.anthropic.AnthropicChatModel;
import org.springframework.ai.anthropic.AnthropicChatOptions;
import org.springframework.ai.anthropic.api.AnthropicApi;
-import org.springframework.ai.autoconfigure.anthropic.AnthropicAutoConfiguration;
+import org.springframework.ai.model.anthropic.autoconfigure.AnthropicAutoConfiguration;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/tool/MockWeatherService.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/MockWeatherService.java
similarity index 97%
rename from spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/tool/MockWeatherService.java
rename to spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/MockWeatherService.java
index 4d39e27f4..645ab7060 100644
--- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/tool/MockWeatherService.java
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/MockWeatherService.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package org.springframework.ai.autoconfigure.anthropic.tool;
+package org.springframework.ai.model.anthropic.autoconfigure.tool;
import java.util.function.Function;
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationIT.java
new file mode 100644
index 000000000..4cd87697c
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationIT.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.azure.openai.autoconfigure;
+
+import java.lang.reflect.Field;
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
+
+import com.azure.ai.openai.OpenAIClient;
+import com.azure.ai.openai.OpenAIClientBuilder;
+import com.azure.ai.openai.implementation.OpenAIClientImpl;
+import com.azure.core.http.HttpHeader;
+import com.azure.core.http.HttpHeaderName;
+import com.azure.core.http.HttpMethod;
+import com.azure.core.http.HttpPipeline;
+import com.azure.core.http.HttpRequest;
+import com.azure.core.http.HttpResponse;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+
+import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAIClientBuilderCustomizer;
+
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration;
+import org.springframework.ai.azure.openai.AzureOpenAiAudioTranscriptionModel;
+import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
+import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel;
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.messages.Message;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.model.Generation;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.chat.prompt.SystemPromptTemplate;
+import org.springframework.ai.embedding.EmbeddingResponse;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+import org.springframework.util.ReflectionUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Christian Tzolov
+ * @author Piotr Olaszewski
+ * @author Soby Chacko
+ * @author Manuel Andreo Garcia
+ * @since 0.8.0
+ */
+@EnabledIfEnvironmentVariable(named = "AZURE_OPENAI_API_KEY", matches = ".+")
+@EnabledIfEnvironmentVariable(named = "AZURE_OPENAI_ENDPOINT", matches = ".+")
+class AzureOpenAiAutoConfigurationIT {
+
+ private static String CHAT_MODEL_NAME = "gpt-4o";
+
+ private static String EMBEDDING_MODEL_NAME = "text-embedding-ada-002";
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.azure.openai.api-key=" + System.getenv("AZURE_OPENAI_API_KEY"),
+ "spring.ai.azure.openai.endpoint=" + System.getenv("AZURE_OPENAI_ENDPOINT"),
+
+ "spring.ai.azure.openai.chat.options.deployment-name=" + CHAT_MODEL_NAME,
+ "spring.ai.azure.openai.chat.options.temperature=0.8",
+ "spring.ai.azure.openai.chat.options.maxTokens=123",
+
+ "spring.ai.azure.openai.embedding.options.deployment-name=" + EMBEDDING_MODEL_NAME,
+ "spring.ai.azure.openai.audio.transcription.options.deployment-name=" + System.getenv("AZURE_OPENAI_TRANSCRIPTION_DEPLOYMENT_NAME")
+ // @formatter:on
+ ).withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class));
+
+ private final Message systemMessage = new SystemPromptTemplate("""
+ You are a helpful AI assistant. Your name is {name}.
+ You are an AI assistant that helps people find information.
+ Your name is {name}
+ You should reply to the user's request with your name and also in the style of a {voice}.
+ """).createMessage(Map.of("name", "Bob", "voice", "pirate"));
+
+ private final UserMessage userMessage = new UserMessage(
+ "Tell me about 3 famous pirates from the Golden Age of Piracy and why they did.");
+
+ @Test
+ void chatCompletion() {
+ this.contextRunner.run(context -> {
+ AzureOpenAiChatModel chatModel = context.getBean(AzureOpenAiChatModel.class);
+ ChatResponse response = chatModel.call(new Prompt(List.of(this.userMessage, this.systemMessage)));
+ assertThat(response.getResult().getOutput().getText()).contains("Blackbeard");
+ });
+ }
+
+ @Test
+ void httpRequestContainsUserAgentAndCustomHeaders() {
+ this.contextRunner
+ .withPropertyValues("spring.ai.azure.openai.custom-headers.foo=bar",
+ "spring.ai.azure.openai.custom-headers.fizz=buzz")
+ .run(context -> {
+ OpenAIClientBuilder openAIClientBuilder = context.getBean(OpenAIClientBuilder.class);
+ OpenAIClient openAIClient = openAIClientBuilder.buildClient();
+ Field serviceClientField = ReflectionUtils.findField(OpenAIClient.class, "serviceClient");
+ assertThat(serviceClientField).isNotNull();
+ ReflectionUtils.makeAccessible(serviceClientField);
+ OpenAIClientImpl oaci = (OpenAIClientImpl) ReflectionUtils.getField(serviceClientField, openAIClient);
+ assertThat(oaci).isNotNull();
+ HttpPipeline httpPipeline = oaci.getHttpPipeline();
+ HttpResponse httpResponse = httpPipeline
+ .send(new HttpRequest(HttpMethod.POST, new URI(System.getenv("AZURE_OPENAI_ENDPOINT")).toURL()))
+ .block();
+ assertThat(httpResponse).isNotNull();
+ HttpHeader httpHeader = httpResponse.getRequest().getHeaders().get(HttpHeaderName.USER_AGENT);
+ assertThat(httpHeader.getValue().startsWith("spring-ai azsdk-java-azure-ai-openai/")).isTrue();
+ HttpHeader customHeader1 = httpResponse.getRequest().getHeaders().get("foo");
+ assertThat(customHeader1.getValue()).isEqualTo("bar");
+ HttpHeader customHeader2 = httpResponse.getRequest().getHeaders().get("fizz");
+ assertThat(customHeader2.getValue()).isEqualTo("buzz");
+ });
+ }
+
+ @Test
+ void chatCompletionStreaming() {
+ this.contextRunner.run(context -> {
+
+ AzureOpenAiChatModel chatModel = context.getBean(AzureOpenAiChatModel.class);
+
+ Flux response = chatModel.stream(new Prompt(List.of(this.userMessage, this.systemMessage)));
+
+ List responses = response.collectList().block();
+ assertThat(responses.size()).isGreaterThan(10);
+
+ String stitchedResponseContent = responses.stream()
+ .map(ChatResponse::getResults)
+ .flatMap(List::stream)
+ .map(Generation::getOutput)
+ .map(AssistantMessage::getText)
+ .collect(Collectors.joining());
+
+ assertThat(stitchedResponseContent).contains("Blackbeard");
+ });
+ }
+
+ @Test
+ void embedding() {
+ this.contextRunner.run(context -> {
+ AzureOpenAiEmbeddingModel embeddingModel = context.getBean(AzureOpenAiEmbeddingModel.class);
+
+ EmbeddingResponse embeddingResponse = embeddingModel
+ .embedForResponse(List.of("Hello World", "World is big and salvation is near"));
+ assertThat(embeddingResponse.getResults()).hasSize(2);
+ assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty();
+ assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0);
+ assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty();
+ assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1);
+
+ assertThat(embeddingModel.dimensions()).isEqualTo(1536);
+ });
+ }
+
+ @Test
+ @EnabledIfEnvironmentVariable(named = "AZURE_OPENAI_TRANSCRIPTION_DEPLOYMENT_NAME", matches = ".+")
+ void transcribe() {
+ this.contextRunner.run(context -> {
+ AzureOpenAiAudioTranscriptionModel transcriptionModel = context
+ .getBean(AzureOpenAiAudioTranscriptionModel.class);
+ Resource audioFile = new ClassPathResource("/speech/jfk.flac");
+ String response = transcriptionModel.call(audioFile);
+ assertThat(response).isEqualTo(
+ "And so my fellow Americans, ask not what your country can do for you, ask what you can do for your country.");
+ });
+ }
+
+ @Test
+ void chatActivation() {
+
+ // Disable the chat auto-configuration.
+ this.contextRunner.withPropertyValues("spring.ai.azure.openai.chat.enabled=false")
+ .run(context -> assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isEmpty());
+
+ // The chat auto-configuration is enabled by default.
+ this.contextRunner.run(context -> assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty());
+
+ // Explicitly enable the chat auto-configuration.
+ this.contextRunner.withPropertyValues("spring.ai.azure.openai.chat.enabled=true")
+ .run(context -> assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty());
+ }
+
+ @Test
+ void embeddingActivation() {
+
+ // Disable the embedding auto-configuration.
+ this.contextRunner.withPropertyValues("spring.ai.azure.openai.embedding.enabled=false")
+ .run(context -> assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isEmpty());
+
+ // The embedding auto-configuration is enabled by default.
+ this.contextRunner
+ .run(context -> assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty());
+
+ // Explicitly enable the embedding auto-configuration.
+ this.contextRunner.withPropertyValues("spring.ai.azure.openai.embedding.enabled=true")
+ .run(context -> assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty());
+ }
+
+ @Test
+ void audioTranscriptionActivation() {
+
+ // Disable the transcription auto-configuration.
+ this.contextRunner.withPropertyValues("spring.ai.azure.openai.audio.transcription.enabled=false")
+ .run(context -> assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isEmpty());
+
+ // The transcription auto-configuration is enabled by default.
+ this.contextRunner
+ .run(context -> assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty());
+
+ // Explicitly enable the transcription auto-configuration.
+ this.contextRunner.withPropertyValues("spring.ai.azure.openai.audio.transcription.enabled=true")
+ .run(context -> assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty());
+ }
+
+ @Test
+ void openAIClientBuilderCustomizer() {
+ AtomicBoolean firstCustomizationApplied = new AtomicBoolean(false);
+ AtomicBoolean secondCustomizationApplied = new AtomicBoolean(false);
+ this.contextRunner
+ .withBean("first", AzureOpenAIClientBuilderCustomizer.class,
+ () -> clientBuilder -> firstCustomizationApplied.set(true))
+ .withBean("second", AzureOpenAIClientBuilderCustomizer.class,
+ () -> clientBuilder -> secondCustomizationApplied.set(true))
+ .run(context -> {
+ context.getBean(OpenAIClientBuilder.class);
+ assertThat(firstCustomizationApplied.get()).isTrue();
+ assertThat(secondCustomizationApplied.get()).isTrue();
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationPropertyTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationPropertyTests.java
new file mode 100644
index 000000000..df965f34e
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfigurationPropertyTests.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.azure.openai.autoconfigure;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration;
+import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatProperties;
+import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiConnectionProperties;
+import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingProperties;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Christian Tzolov
+ * @since 0.8.0
+ */
+public class AzureOpenAiAutoConfigurationPropertyTests {
+
+ @Test
+ public void embeddingPropertiesTest() {
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.azure.openai.api-key=TEST_API_KEY",
+ "spring.ai.azure.openai.endpoint=TEST_ENDPOINT",
+ "spring.ai.azure.openai.embedding.options.deployment-name=MODEL_XYZ")
+ .withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class))
+ .run(context -> {
+ var chatProperties = context.getBean(AzureOpenAiEmbeddingProperties.class);
+ var connectionProperties = context.getBean(AzureOpenAiConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("TEST_API_KEY");
+ assertThat(connectionProperties.getEndpoint()).isEqualTo("TEST_ENDPOINT");
+
+ assertThat(chatProperties.getOptions().getDeploymentName()).isEqualTo("MODEL_XYZ");
+ });
+ }
+
+ @Test
+ public void chatPropertiesTest() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.azure.openai.api-key=API_KEY",
+ "spring.ai.azure.openai.endpoint=ENDPOINT",
+
+ "spring.ai.azure.openai.chat.options.deployment-name=MODEL_XYZ",
+ "spring.ai.azure.openai.chat.options.frequencyPenalty=-1.5",
+ "spring.ai.azure.openai.chat.options.logitBias.myTokenId=-5",
+ "spring.ai.azure.openai.chat.options.maxTokens=123",
+ "spring.ai.azure.openai.chat.options.n=10",
+ "spring.ai.azure.openai.chat.options.presencePenalty=0",
+ "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"
+ )
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class))
+ .run(context -> {
+ var chatProperties = context.getBean(AzureOpenAiChatProperties.class);
+ var connectionProperties = context.getBean(AzureOpenAiConnectionProperties.class);
+ var embeddingProperties = context.getBean(AzureOpenAiEmbeddingProperties.class);
+
+ assertThat(connectionProperties.getEndpoint()).isEqualTo("ENDPOINT");
+ assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY");
+
+ assertThat(embeddingProperties.getOptions().getDeploymentName()).isEqualTo("text-embedding-ada-002");
+
+ assertThat(chatProperties.getOptions().getDeploymentName()).isEqualTo("MODEL_XYZ");
+ assertThat(chatProperties.getOptions().getFrequencyPenalty()).isEqualTo(-1.5);
+ assertThat(chatProperties.getOptions().getLogitBias().get("myTokenId")).isEqualTo(-5);
+ assertThat(chatProperties.getOptions().getMaxTokens()).isEqualTo(123);
+ assertThat(chatProperties.getOptions().getN()).isEqualTo(10);
+ assertThat(chatProperties.getOptions().getPresencePenalty()).isEqualTo(0);
+ assertThat(chatProperties.getOptions().getStop()).contains("boza", "koza");
+ assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55);
+ assertThat(chatProperties.getOptions().getTopP()).isEqualTo(0.56);
+
+ assertThat(chatProperties.getOptions().getUser()).isEqualTo("userXYZ");
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiDirectOpenAiAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiDirectOpenAiAutoConfigurationIT.java
new file mode 100644
index 000000000..99c4ed05c
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiDirectOpenAiAutoConfigurationIT.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.azure.openai.autoconfigure;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration;
+import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
+import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel;
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.messages.Message;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.model.Generation;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.chat.prompt.SystemPromptTemplate;
+import org.springframework.ai.embedding.EmbeddingResponse;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Christian Tzolov
+ * @since 1.0.0
+ */
+@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*")
+public class AzureOpenAiDirectOpenAiAutoConfigurationIT {
+
+ private static String CHAT_MODEL_NAME = "gpt-4o";
+
+ private static String EMBEDDING_MODEL_NAME = "text-embedding-ada-002";
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.azure.openai.openai-api-key=" + System.getenv("OPENAI_API_KEY"),
+
+ "spring.ai.azure.openai.chat.options.deployment-name=" + CHAT_MODEL_NAME,
+ "spring.ai.azure.openai.chat.options.temperature=0.8",
+ "spring.ai.azure.openai.chat.options.maxTokens=123",
+ "spring.ai.azure.openai.embedding.options.deployment-name=" + EMBEDDING_MODEL_NAME
+ // @formatter:on
+ ).withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class));
+
+ private final Message systemMessage = new SystemPromptTemplate("""
+ You are a helpful AI assistant. Your name is {name}.
+ You are an AI assistant that helps people find information.
+ Your name is {name}
+ You should reply to the user's request with your name and also in the style of a {voice}.
+ """).createMessage(Map.of("name", "Bob", "voice", "pirate"));
+
+ private final UserMessage userMessage = new UserMessage(
+ "Tell me about 3 famous pirates from the Golden Age of Piracy and why they did.");
+
+ @Test
+ public void chatCompletion() {
+ this.contextRunner.run(context -> {
+ AzureOpenAiChatModel chatModel = context.getBean(AzureOpenAiChatModel.class);
+ ChatResponse response = chatModel.call(new Prompt(List.of(this.userMessage, this.systemMessage)));
+ assertThat(response.getResult().getOutput().getText()).contains("Blackbeard");
+ });
+ }
+
+ @Test
+ public void chatCompletionStreaming() {
+ this.contextRunner.run(context -> {
+
+ AzureOpenAiChatModel chatModel = context.getBean(AzureOpenAiChatModel.class);
+
+ Flux response = chatModel.stream(new Prompt(List.of(this.userMessage, this.systemMessage)));
+
+ List responses = response.collectList().block();
+ assertThat(responses.size()).isGreaterThan(10);
+
+ String stitchedResponseContent = responses.stream()
+ .map(ChatResponse::getResults)
+ .flatMap(List::stream)
+ .map(Generation::getOutput)
+ .map(AssistantMessage::getText)
+ .collect(Collectors.joining());
+
+ assertThat(stitchedResponseContent).contains("Blackbeard");
+ });
+ }
+
+ @Test
+ void embedding() {
+ this.contextRunner.run(context -> {
+ AzureOpenAiEmbeddingModel embeddingModel = context.getBean(AzureOpenAiEmbeddingModel.class);
+
+ EmbeddingResponse embeddingResponse = embeddingModel
+ .embedForResponse(List.of("Hello World", "World is big and salvation is near"));
+ assertThat(embeddingResponse.getResults()).hasSize(2);
+ assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty();
+ assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0);
+ assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty();
+ assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1);
+
+ assertThat(embeddingModel.dimensions()).isEqualTo(1536);
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/DeploymentNameUtil.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/DeploymentNameUtil.java
new file mode 100644
index 000000000..96655413b
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/DeploymentNameUtil.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.azure.openai.autoconfigure.tool;
+
+import org.springframework.util.StringUtils;
+
+public final class DeploymentNameUtil {
+
+ private DeploymentNameUtil() {
+
+ }
+
+ public static String getDeploymentName() {
+ String deploymentName = System.getenv("AZURE_OPENAI_DEPLOYMENT_NAME");
+ if (StringUtils.hasText(deploymentName)) {
+ return deploymentName;
+ }
+ else {
+ return "gpt-4o";
+ }
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java
new file mode 100644
index 000000000..5f560d174
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2023-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.azure.openai.autoconfigure.tool;
+
+import java.util.List;
+import java.util.function.Function;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration;
+import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
+import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatModel;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.model.tool.ToolCallingChatOptions;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Description;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnabledIfEnvironmentVariable(named = "AZURE_OPENAI_API_KEY", matches = ".+")
+@EnabledIfEnvironmentVariable(named = "AZURE_OPENAI_ENDPOINT", matches = ".+")
+class FunctionCallWithFunctionBeanIT {
+
+ private final Logger logger = LoggerFactory.getLogger(FunctionCallWithFunctionBeanIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.azure.openai.api-key=" + System.getenv("AZURE_OPENAI_API_KEY"),
+ "spring.ai.azure.openai.endpoint=" + System.getenv("AZURE_OPENAI_ENDPOINT"))
+ // @formatter:onn
+ .withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class))
+ .withUserConfiguration(Config.class);
+
+ @Test
+ void functionCallTest() {
+ this.contextRunner
+ .withPropertyValues(
+ "spring.ai.azure.openai.chat.options..deployment-name=" + DeploymentNameUtil.getDeploymentName())
+ .run(context -> {
+
+ ChatModel chatModel = context.getBean(AzureOpenAiChatModel.class);
+
+ UserMessage userMessage = new UserMessage(
+ "What's the weather like in San Francisco, Paris and in Tokyo? Use Multi-turn function calling.");
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
+ AzureOpenAiChatOptions.builder().function("weatherFunction").build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+
+ response = chatModel.call(new Prompt(List.of(userMessage),
+ AzureOpenAiChatOptions.builder().function("weatherFunction3").build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+
+ });
+ }
+
+ @Test
+ void functionCallWithPortableFunctionCallingOptions() {
+ this.contextRunner
+ .withPropertyValues(
+ "spring.ai.azure.openai.chat.options..deployment-name=" + DeploymentNameUtil.getDeploymentName())
+ .run(context -> {
+
+ ChatModel chatModel = context.getBean(AzureOpenAiChatModel.class);
+
+ UserMessage userMessage = new UserMessage(
+ "What's the weather like in San Francisco, Paris and in Tokyo? Use Multi-turn function calling.");
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
+ ToolCallingChatOptions.builder().toolNames("weatherFunction").build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+
+ });
+ }
+
+ @Configuration
+ static class Config {
+
+ @Bean
+ @Description("Get the weather in location")
+ public Function weatherFunction() {
+ return new MockWeatherService();
+ }
+
+ // Relies on the Request's JsonClassDescription annotation to provide the
+ // function description.
+ @Bean
+ public Function weatherFunction3() {
+ MockWeatherService weatherService = new MockWeatherService();
+ return (weatherService::apply);
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithFunctionWrapperIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithFunctionWrapperIT.java
new file mode 100644
index 000000000..b809da5dd
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithFunctionWrapperIT.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.azure.openai.autoconfigure.tool;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration;
+import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
+import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.tool.ToolCallback;
+import org.springframework.ai.tool.function.FunctionToolCallback;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnabledIfEnvironmentVariable(named = "AZURE_OPENAI_API_KEY", matches = ".+")
+@EnabledIfEnvironmentVariable(named = "AZURE_OPENAI_ENDPOINT", matches = ".+")
+public class FunctionCallWithFunctionWrapperIT {
+
+ private final Logger logger = LoggerFactory.getLogger(FunctionCallWithFunctionWrapperIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.azure.openai.api-key=" + System.getenv("AZURE_OPENAI_API_KEY"),
+ "spring.ai.azure.openai.endpoint=" + System.getenv("AZURE_OPENAI_ENDPOINT"))
+ // @formatter:onn
+ .withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class))
+ .withUserConfiguration(Config.class);
+
+ @Test
+ void functionCallTest() {
+ this.contextRunner
+ .withPropertyValues(
+ "spring.ai.azure.openai.chat.options.deployment-name=" + DeploymentNameUtil.getDeploymentName())
+ .run(context -> {
+
+ AzureOpenAiChatModel chatModel = context.getBean(AzureOpenAiChatModel.class);
+
+ UserMessage userMessage = new UserMessage(
+ "What's the weather like in San Francisco, Paris and in Tokyo?");
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
+ AzureOpenAiChatOptions.builder().function("WeatherInfo").build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).containsAnyOf("30", "10", "15");
+
+ });
+ }
+
+ @Configuration
+ static class Config {
+
+ @Bean
+ public ToolCallback weatherFunctionInfo() {
+
+ return FunctionToolCallback.builder("WeatherInfo", new MockWeatherService())
+ .description("Get the current weather in a given location")
+ .inputType(MockWeatherService.Request.class)
+ .build();
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithPromptFunctionIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithPromptFunctionIT.java
new file mode 100644
index 000000000..90e5d7a9b
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/FunctionCallWithPromptFunctionIT.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.azure.openai.autoconfigure.tool;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration;
+import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
+import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.tool.function.FunctionToolCallback;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnabledIfEnvironmentVariable(named = "AZURE_OPENAI_API_KEY", matches = ".+")
+@EnabledIfEnvironmentVariable(named = "AZURE_OPENAI_ENDPOINT", matches = ".+")
+public class FunctionCallWithPromptFunctionIT {
+
+ private final Logger logger = LoggerFactory.getLogger(FunctionCallWithPromptFunctionIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.azure.openai.api-key=" + System.getenv("AZURE_OPENAI_API_KEY"),
+ "spring.ai.azure.openai.endpoint=" + System.getenv("AZURE_OPENAI_ENDPOINT"))
+ // @formatter:onn
+ .withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class));
+
+ @Test
+ void functionCallTest() {
+ this.contextRunner
+ .withPropertyValues(
+ "spring.ai.azure.openai.chat.options.deployment-name=" + DeploymentNameUtil.getDeploymentName())
+ .run(context -> {
+
+ AzureOpenAiChatModel chatModel = context.getBean(AzureOpenAiChatModel.class);
+
+ UserMessage userMessage = new UserMessage(
+ "What's the weather like in San Francisco, in Paris and in Tokyo? Use Multi-turn function calling.");
+
+ var promptOptions = AzureOpenAiChatOptions.builder()
+ .functionCallbacks(
+ List.of(FunctionToolCallback.builder("CurrentWeatherService", new MockWeatherService())
+ .description("Get the weather in location")
+ .inputType(MockWeatherService.Request.class)
+ .build()))
+ .build();
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), promptOptions));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/MockWeatherService.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/MockWeatherService.java
new file mode 100644
index 000000000..20c1899ef
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/tool/MockWeatherService.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.azure.openai.autoconfigure.tool;
+
+import java.util.function.Function;
+
+import com.fasterxml.jackson.annotation.JsonClassDescription;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyDescription;
+
+/**
+ * Mock 3rd party weather service.
+ *
+ * @author Christian Tzolov
+ */
+public class MockWeatherService implements Function {
+
+ @Override
+ public Response apply(Request request) {
+
+ double temperature = 0;
+ if (request.location().contains("Paris")) {
+ temperature = 15;
+ }
+ else if (request.location().contains("Tokyo")) {
+ temperature = 10;
+ }
+ else if (request.location().contains("San Francisco")) {
+ temperature = 30;
+ }
+
+ return new Response(temperature, 15, 20, 2, 53, 45, Unit.C);
+ }
+
+ /**
+ * Temperature units.
+ */
+ public enum Unit {
+
+ /**
+ * Celsius.
+ */
+ C("metric"),
+ /**
+ * Fahrenheit.
+ */
+ F("imperial");
+
+ /**
+ * Human readable unit name.
+ */
+ public final String unitName;
+
+ Unit(String text) {
+ this.unitName = text;
+ }
+
+ }
+
+ /**
+ * Weather Function request.
+ */
+ @JsonInclude(Include.NON_NULL)
+ @JsonClassDescription("Weather API request")
+ public record Request(@JsonProperty(required = true,
+ value = "location") @JsonPropertyDescription("The city and state e.g. San Francisco, CA") String location,
+
+ @JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) {
+
+ }
+
+ /**
+ * Weather Function response.
+ */
+ public record Response(double temp, double feels_like, double temp_min, double temp_max, int pressure, int humidity,
+ Unit unit) {
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockAwsConnectionConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockAwsConnectionConfigurationIT.java
new file mode 100644
index 000000000..af51a1e93
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockAwsConnectionConfigurationIT.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.bedrock.autoconfigure;
+
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.auth.credentials.AwsCredentials;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.regions.providers.AwsRegionProvider;
+
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Import;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Wei Jiang
+ * @author Mark Pollack
+ * @since 1.0.0
+ */
+@RequiresAwsCredentials
+public class BedrockAwsConnectionConfigurationIT {
+
+ @Test
+ public void autoConfigureAWSCredentialAndRegionProvider() {
+ BedrockTestUtils.getContextRunner()
+ .withConfiguration(AutoConfigurations.of(TestAutoConfiguration.class))
+ .run(context -> {
+ var awsCredentialsProvider = context.getBean(AwsCredentialsProvider.class);
+ var awsRegionProvider = context.getBean(AwsRegionProvider.class);
+
+ assertThat(awsCredentialsProvider).isNotNull();
+ assertThat(awsRegionProvider).isNotNull();
+
+ var credentials = awsCredentialsProvider.resolveCredentials();
+ assertThat(credentials).isNotNull();
+ assertThat(credentials.accessKeyId()).isEqualTo(System.getenv("AWS_ACCESS_KEY_ID"));
+ assertThat(credentials.secretAccessKey()).isEqualTo(System.getenv("AWS_SECRET_ACCESS_KEY"));
+
+ assertThat(awsRegionProvider.getRegion()).isEqualTo(Region.US_EAST_1);
+ });
+ }
+
+ @Test
+ public void autoConfigureWithCustomAWSCredentialAndRegionProvider() {
+ BedrockTestUtils.getContextRunner()
+ .withConfiguration(AutoConfigurations.of(TestAutoConfiguration.class,
+ CustomAwsCredentialsProviderAndAwsRegionProviderAutoConfiguration.class))
+ .run(context -> {
+ var awsCredentialsProvider = context.getBean(AwsCredentialsProvider.class);
+ var awsRegionProvider = context.getBean(AwsRegionProvider.class);
+
+ assertThat(awsCredentialsProvider).isNotNull();
+ assertThat(awsRegionProvider).isNotNull();
+
+ var credentials = awsCredentialsProvider.resolveCredentials();
+ assertThat(credentials).isNotNull();
+ assertThat(credentials.accessKeyId()).isEqualTo("CUSTOM_ACCESS_KEY");
+ assertThat(credentials.secretAccessKey()).isEqualTo("CUSTOM_SECRET_ACCESS_KEY");
+
+ assertThat(awsRegionProvider.getRegion()).isEqualTo(Region.AWS_GLOBAL);
+ });
+ }
+
+ @EnableConfigurationProperties({ BedrockAwsConnectionProperties.class })
+ @Import(BedrockAwsConnectionConfiguration.class)
+ static class TestAutoConfiguration {
+
+ }
+
+ @AutoConfiguration
+ static class CustomAwsCredentialsProviderAndAwsRegionProviderAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ public AwsCredentialsProvider credentialsProvider() {
+ return new AwsCredentialsProvider() {
+
+ @Override
+ public AwsCredentials resolveCredentials() {
+ return new AwsCredentials() {
+
+ @Override
+ public String accessKeyId() {
+ return "CUSTOM_ACCESS_KEY";
+ }
+
+ @Override
+ public String secretAccessKey() {
+ return "CUSTOM_SECRET_ACCESS_KEY";
+ }
+
+ };
+ }
+
+ };
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public AwsRegionProvider regionProvider() {
+ return new AwsRegionProvider() {
+
+ @Override
+ public Region getRegion() {
+ return Region.AWS_GLOBAL;
+ }
+
+ };
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockTestUtils.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockTestUtils.java
new file mode 100644
index 000000000..88b12b6c3
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/BedrockTestUtils.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.bedrock.autoconfigure;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import software.amazon.awssdk.regions.Region;
+
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+public final class BedrockTestUtils {
+
+ private BedrockTestUtils() {
+ } // Prevent instantiation
+
+ public static ApplicationContextRunner getContextRunner() {
+ return new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.bedrock.aws.access-key=" + System.getenv("AWS_ACCESS_KEY_ID"),
+ "spring.ai.bedrock.aws.secret-key=" + System.getenv("AWS_SECRET_ACCESS_KEY"),
+ "spring.ai.bedrock.aws.session-token=" + System.getenv("AWS_SESSION_TOKEN"),
+ "spring.ai.bedrock.aws.region=" + Region.US_EAST_1.id())
+ .withUserConfiguration(Config.class);
+ }
+
+ public static ApplicationContextRunner getContextRunnerWithUserConfiguration() {
+ return new ApplicationContextRunner().withUserConfiguration(Config.class);
+ }
+
+ @Configuration
+ static class Config {
+
+ @Bean
+ public ObjectMapper objectMapper() {
+ return new ObjectMapper();
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/RequiresAwsCredentials.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/RequiresAwsCredentials.java
new file mode 100644
index 000000000..00f5e633b
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/RequiresAwsCredentials.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.bedrock.autoconfigure;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+
+@Target({ ElementType.TYPE, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+@EnabledIfEnvironmentVariable(named = "AWS_ACCESS_KEY_ID", matches = ".*")
+@EnabledIfEnvironmentVariable(named = "AWS_SECRET_ACCESS_KEY", matches = ".*")
+@EnabledIfEnvironmentVariable(named = "AWS_SESSION_TOKEN", matches = ".*")
+public @interface RequiresAwsCredentials {
+
+ // You can add custom properties here if needed
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingAutoConfigurationIT.java
new file mode 100644
index 000000000..f612425cf
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingAutoConfigurationIT.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.bedrock.autoconfigure.cohere;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.regions.Region;
+
+import org.springframework.ai.model.bedrock.autoconfigure.BedrockAwsConnectionProperties;
+import org.springframework.ai.model.bedrock.autoconfigure.BedrockTestUtils;
+import org.springframework.ai.model.bedrock.autoconfigure.RequiresAwsCredentials;
+import org.springframework.ai.bedrock.cohere.BedrockCohereEmbeddingModel;
+import org.springframework.ai.bedrock.cohere.api.CohereEmbeddingBedrockApi.CohereEmbeddingModel;
+import org.springframework.ai.bedrock.cohere.api.CohereEmbeddingBedrockApi.CohereEmbeddingRequest;
+import org.springframework.ai.bedrock.cohere.api.CohereEmbeddingBedrockApi.CohereEmbeddingRequest.InputType;
+import org.springframework.ai.embedding.EmbeddingResponse;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Christian Tzolov
+ * @author Mark Pollack
+ * @since 1.0.0
+ */
+@RequiresAwsCredentials
+public class BedrockCohereEmbeddingAutoConfigurationIT {
+
+ private final ApplicationContextRunner contextRunner = BedrockTestUtils.getContextRunner()
+ .withPropertyValues("spring.ai.bedrock.cohere.embedding.enabled=true",
+ "spring.ai.bedrock.cohere.embedding.model=" + CohereEmbeddingModel.COHERE_EMBED_MULTILINGUAL_V3.id(),
+ "spring.ai.bedrock.cohere.embedding.options.inputType=SEARCH_DOCUMENT",
+ "spring.ai.bedrock.cohere.embedding.options.truncate=NONE")
+ .withConfiguration(AutoConfigurations.of(BedrockCohereEmbeddingAutoConfiguration.class));
+
+ @Test
+ public void singleEmbedding() {
+ this.contextRunner.run(context -> {
+ BedrockCohereEmbeddingModel embeddingModel = context.getBean(BedrockCohereEmbeddingModel.class);
+ assertThat(embeddingModel).isNotNull();
+ EmbeddingResponse embeddingResponse = embeddingModel.embedForResponse(List.of("Hello World"));
+ assertThat(embeddingResponse.getResults()).hasSize(1);
+ assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty();
+ assertThat(embeddingModel.dimensions()).isEqualTo(1024);
+ });
+ }
+
+ @Test
+ public void batchEmbedding() {
+ this.contextRunner.run(context -> {
+
+ BedrockCohereEmbeddingModel embeddingModel = context.getBean(BedrockCohereEmbeddingModel.class);
+
+ assertThat(embeddingModel).isNotNull();
+ EmbeddingResponse embeddingResponse = embeddingModel
+ .embedForResponse(List.of("Hello World", "World is big and salvation is near"));
+ assertThat(embeddingResponse.getResults()).hasSize(2);
+ assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty();
+ assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0);
+ assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty();
+ assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1);
+
+ assertThat(embeddingModel.dimensions()).isEqualTo(1024);
+
+ });
+ }
+
+ @Test
+ public void propertiesTest() {
+
+ BedrockTestUtils.getContextRunnerWithUserConfiguration()
+ .withPropertyValues("spring.ai.bedrock.cohere.embedding.enabled=true",
+ "spring.ai.bedrock.aws.access-key=ACCESS_KEY", "spring.ai.bedrock.aws.secret-key=SECRET_KEY",
+ "spring.ai.bedrock.aws.region=" + Region.US_EAST_1.id(),
+ "spring.ai.bedrock.cohere.embedding.model=MODEL_XYZ",
+ "spring.ai.bedrock.cohere.embedding.options.inputType=CLASSIFICATION",
+ "spring.ai.bedrock.cohere.embedding.options.truncate=START")
+ .withConfiguration(AutoConfigurations.of(BedrockCohereEmbeddingAutoConfiguration.class))
+ .run(context -> {
+ var properties = context.getBean(BedrockCohereEmbeddingProperties.class);
+ var awsProperties = context.getBean(BedrockAwsConnectionProperties.class);
+
+ assertThat(properties.isEnabled()).isTrue();
+ assertThat(awsProperties.getRegion()).isEqualTo(Region.US_EAST_1.id());
+ assertThat(properties.getModel()).isEqualTo("MODEL_XYZ");
+
+ assertThat(properties.getOptions().getInputType()).isEqualTo(InputType.CLASSIFICATION);
+ assertThat(properties.getOptions().getTruncate()).isEqualTo(CohereEmbeddingRequest.Truncate.START);
+
+ assertThat(awsProperties.getAccessKey()).isEqualTo("ACCESS_KEY");
+ assertThat(awsProperties.getSecretKey()).isEqualTo("SECRET_KEY");
+ });
+ }
+
+ @Test
+ public void embeddingDisabled() {
+
+ // It is disabled by default
+ BedrockTestUtils.getContextRunnerWithUserConfiguration()
+ .withConfiguration(AutoConfigurations.of(BedrockCohereEmbeddingAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(BedrockCohereEmbeddingProperties.class)).isEmpty();
+ assertThat(context.getBeansOfType(BedrockCohereEmbeddingModel.class)).isEmpty();
+ });
+
+ // Explicitly enable the embedding auto-configuration.
+ BedrockTestUtils.getContextRunnerWithUserConfiguration()
+ .withPropertyValues("spring.ai.bedrock.cohere.embedding.enabled=true")
+ .withConfiguration(AutoConfigurations.of(BedrockCohereEmbeddingAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(BedrockCohereEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(BedrockCohereEmbeddingModel.class)).isNotEmpty();
+ });
+
+ // Explicitly disable the embedding auto-configuration.
+ BedrockTestUtils.getContextRunnerWithUserConfiguration()
+ .withPropertyValues("spring.ai.bedrock.cohere.embedding.enabled=false")
+ .withConfiguration(AutoConfigurations.of(BedrockCohereEmbeddingAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(BedrockCohereEmbeddingProperties.class)).isEmpty();
+ assertThat(context.getBeansOfType(BedrockCohereEmbeddingModel.class)).isEmpty();
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatAutoConfigurationIT.java
new file mode 100644
index 000000000..f89cda503
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatAutoConfigurationIT.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.bedrock.autoconfigure.converse;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.model.bedrock.autoconfigure.BedrockTestUtils;
+import org.springframework.ai.model.bedrock.autoconfigure.RequiresAwsCredentials;
+import org.springframework.ai.bedrock.converse.BedrockProxyChatModel;
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.model.Generation;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@RequiresAwsCredentials
+public class BedrockConverseProxyChatAutoConfigurationIT {
+
+ private static final Log logger = LogFactory.getLog(BedrockConverseProxyChatAutoConfigurationIT.class);
+
+ private final ApplicationContextRunner contextRunner = BedrockTestUtils.getContextRunner()
+ .withPropertyValues(
+ "spring.ai.bedrock.converse.chat.options.model=" + "anthropic.claude-3-5-sonnet-20240620-v1:0",
+ "spring.ai.bedrock.converse.chat.options.temperature=0.5")
+ .withConfiguration(AutoConfigurations.of(BedrockConverseProxyChatAutoConfiguration.class));
+
+ @Test
+ void call() {
+ this.contextRunner.run(context -> {
+ BedrockProxyChatModel chatModel = context.getBean(BedrockProxyChatModel.class);
+ String response = chatModel.call("Hello");
+ assertThat(response).isNotEmpty();
+ logger.info("Response: " + response);
+ });
+ }
+
+ @Test
+ void stream() {
+ this.contextRunner.run(context -> {
+ BedrockProxyChatModel chatModel = context.getBean(BedrockProxyChatModel.class);
+ Flux responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello")));
+
+ String response = responseFlux.collectList()
+ .block()
+ .stream()
+ .map(ChatResponse::getResults)
+ .flatMap(List::stream)
+ .map(Generation::getOutput)
+ .map(AssistantMessage::getText)
+ .collect(Collectors.joining());
+
+ assertThat(response).isNotEmpty();
+ logger.info("Response: " + response);
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatPropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatPropertiesTests.java
new file mode 100644
index 000000000..b71900197
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatPropertiesTests.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.bedrock.autoconfigure.converse;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Christian Tzolov
+ *
+ * Unit Tests for {@link BedrockConverseProxyChatProperties}.
+ */
+public class BedrockConverseProxyChatPropertiesTests {
+
+ @Test
+ public void chatOptionsTest() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.bedrock.converse.chat.options.model=MODEL_XYZ",
+
+ "spring.ai.bedrock.converse.chat.options.max-tokens=123",
+ "spring.ai.bedrock.converse.chat.options.metadata.user-id=MyUserId",
+ "spring.ai.bedrock.converse.chat.options.stop_sequences=boza,koza",
+
+ "spring.ai.bedrock.converse.chat.options.temperature=0.55",
+ "spring.ai.bedrock.converse.chat.options.top-p=0.56",
+ "spring.ai.bedrock.converse.chat.options.top-k=100"
+ )
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(BedrockConverseProxyChatAutoConfiguration.class))
+ .run(context -> {
+ var chatProperties = context.getBean(BedrockConverseProxyChatProperties.class);
+
+ assertThat(chatProperties.isEnabled()).isTrue();
+
+ assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(chatProperties.getOptions().getMaxTokens()).isEqualTo(123);
+ assertThat(chatProperties.getOptions().getStopSequences()).contains("boza", "koza");
+ assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55);
+ assertThat(chatProperties.getOptions().getTopP()).isEqualTo(0.56);
+ assertThat(chatProperties.getOptions().getTopK()).isEqualTo(100);
+
+ });
+ }
+
+ @Test
+ public void chatCompletionDisabled() {
+
+ // It is enabled by default
+ new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(BedrockConverseProxyChatAutoConfiguration.class))
+ .run(context -> assertThat(context.getBeansOfType(BedrockConverseProxyChatProperties.class)).isNotEmpty());
+
+ // Explicitly enable the chat auto-configuration.
+ new ApplicationContextRunner().withPropertyValues("spring.ai.bedrock.converse.chat.enabled=true")
+ .withConfiguration(AutoConfigurations.of(BedrockConverseProxyChatAutoConfiguration.class))
+ .run(context -> assertThat(context.getBeansOfType(BedrockConverseProxyChatProperties.class)).isNotEmpty());
+
+ // Explicitly disable the chat auto-configuration.
+ new ApplicationContextRunner().withPropertyValues("spring.ai.bedrock.converse.chat.enabled=false")
+ .withConfiguration(AutoConfigurations.of(BedrockConverseProxyChatAutoConfiguration.class))
+ .run(context -> assertThat(context.getBeansOfType(BedrockConverseProxyChatProperties.class)).isEmpty());
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/FunctionCallWithFunctionBeanIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/FunctionCallWithFunctionBeanIT.java
new file mode 100644
index 000000000..4ad9486a4
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/FunctionCallWithFunctionBeanIT.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2023-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.bedrock.autoconfigure.converse.tool;
+
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.model.bedrock.autoconfigure.BedrockTestUtils;
+import org.springframework.ai.model.bedrock.autoconfigure.RequiresAwsCredentials;
+import org.springframework.ai.model.bedrock.autoconfigure.converse.BedrockConverseProxyChatAutoConfiguration;
+import org.springframework.ai.bedrock.converse.BedrockProxyChatModel;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.model.tool.ToolCallingChatOptions;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Description;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@RequiresAwsCredentials
+class FunctionCallWithFunctionBeanIT {
+
+ private final Logger logger = LoggerFactory.getLogger(FunctionCallWithFunctionBeanIT.class);
+
+ private final ApplicationContextRunner contextRunner = BedrockTestUtils.getContextRunner()
+ .withConfiguration(AutoConfigurations.of(BedrockConverseProxyChatAutoConfiguration.class))
+ .withUserConfiguration(Config.class);
+
+ @Test
+ void functionCallTest() {
+
+ this.contextRunner
+ .withPropertyValues(
+ "spring.ai.bedrock.converse.chat.options.model=" + "anthropic.claude-3-5-sonnet-20240620-v1:0")
+ .run(context -> {
+
+ BedrockProxyChatModel chatModel = context.getBean(BedrockProxyChatModel.class);
+
+ var userMessage = new UserMessage(
+ "What's the weather like in San Francisco, in Paris, France and in Tokyo, Japan? Return the temperature in Celsius.");
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
+ ToolCallingChatOptions.builder().toolNames("weatherFunction").build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+
+ response = chatModel.call(new Prompt(List.of(userMessage),
+ ToolCallingChatOptions.builder().toolNames("weatherFunction3").build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+ });
+ }
+
+ @Test
+ void functionStreamTest() {
+
+ this.contextRunner
+ .withPropertyValues(
+ "spring.ai.bedrock.converse.chat.options.model=" + "anthropic.claude-3-5-sonnet-20240620-v1:0")
+ .run(context -> {
+
+ BedrockProxyChatModel chatModel = context.getBean(BedrockProxyChatModel.class);
+
+ var userMessage = new UserMessage(
+ "What's the weather like in San Francisco, in Paris, France and in Tokyo, Japan? Return the temperature in Celsius.");
+
+ Flux responses = chatModel.stream(new Prompt(List.of(userMessage),
+ ToolCallingChatOptions.builder().toolNames("weatherFunction").build()));
+
+ String content = responses.collectList()
+ .block()
+ .stream()
+ .filter(cr -> cr.getResult() != null)
+ .map(cr -> cr.getResult().getOutput().getText())
+ .collect(Collectors.joining());
+
+ logger.info("Response: {}", content);
+ assertThat(content).contains("30", "10", "15");
+
+ });
+ }
+
+ @Configuration
+ static class Config {
+
+ @Bean
+ @Description("Get the weather in location. Return temperature in 36°F or 36°C format.")
+ public Function weatherFunction() {
+ return new MockWeatherService();
+ }
+
+ // Relies on the Request's JsonClassDescription annotation to provide the
+ // function description.
+ @Bean
+ public Function weatherFunction3() {
+ MockWeatherService weatherService = new MockWeatherService();
+ return (weatherService::apply);
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/FunctionCallWithPromptFunctionIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/FunctionCallWithPromptFunctionIT.java
new file mode 100644
index 000000000..95c595370
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/FunctionCallWithPromptFunctionIT.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.bedrock.autoconfigure.converse.tool;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.ai.model.bedrock.autoconfigure.BedrockTestUtils;
+import org.springframework.ai.model.bedrock.autoconfigure.RequiresAwsCredentials;
+import org.springframework.ai.model.bedrock.autoconfigure.converse.BedrockConverseProxyChatAutoConfiguration;
+import org.springframework.ai.bedrock.converse.BedrockProxyChatModel;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.model.tool.ToolCallingChatOptions;
+import org.springframework.ai.tool.function.FunctionToolCallback;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@RequiresAwsCredentials
+public class FunctionCallWithPromptFunctionIT {
+
+ private final Logger logger = LoggerFactory.getLogger(FunctionCallWithPromptFunctionIT.class);
+
+ private final ApplicationContextRunner contextRunner = BedrockTestUtils.getContextRunner()
+ .withConfiguration(AutoConfigurations.of(BedrockConverseProxyChatAutoConfiguration.class));
+
+ @Test
+ void functionCallTest() {
+ this.contextRunner
+ .withPropertyValues(
+ "spring.ai.bedrock.converse.chat.options.model=" + "anthropic.claude-3-5-sonnet-20240620-v1:0")
+ .run(context -> {
+
+ BedrockProxyChatModel chatModel = context.getBean(BedrockProxyChatModel.class);
+
+ UserMessage userMessage = new UserMessage(
+ "What's the weather like in San Francisco, in Paris and in Tokyo? Return the temperature in Celsius.");
+
+ var promptOptions = ToolCallingChatOptions.builder()
+ .toolCallbacks(
+ List.of(FunctionToolCallback.builder("CurrentWeatherService", new MockWeatherService())
+ .description("Get the weather in location. Return temperature in 36°F or 36°C format.")
+ .inputType(MockWeatherService.Request.class)
+ .build()))
+ .build();
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), promptOptions));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/MockWeatherService.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/MockWeatherService.java
new file mode 100644
index 000000000..29081ec2f
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/tool/MockWeatherService.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.bedrock.autoconfigure.converse.tool;
+
+import java.util.function.Function;
+
+import com.fasterxml.jackson.annotation.JsonClassDescription;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyDescription;
+
+/**
+ * Mock 3rd party weather service.
+ *
+ * @author Christian Tzolov
+ */
+public class MockWeatherService implements Function {
+
+ @Override
+ public Response apply(Request request) {
+
+ double temperature = 0;
+ if (request.location().contains("Paris")) {
+ temperature = 15;
+ }
+ else if (request.location().contains("Tokyo")) {
+ temperature = 10;
+ }
+ else if (request.location().contains("San Francisco")) {
+ temperature = 30;
+ }
+
+ return new Response(temperature, 15, 20, 2, 53, 45, Unit.C);
+ }
+
+ /**
+ * Temperature units.
+ */
+ public enum Unit {
+
+ /**
+ * Celsius.
+ */
+ C("metric"),
+ /**
+ * Fahrenheit.
+ */
+ F("imperial");
+
+ /**
+ * Human readable unit name.
+ */
+ public final String unitName;
+
+ Unit(String text) {
+ this.unitName = text;
+ }
+
+ }
+
+ /**
+ * Weather Function request.
+ */
+ @JsonInclude(Include.NON_NULL)
+ @JsonClassDescription("Weather API request")
+ public record Request(@JsonProperty(required = true,
+ value = "location") @JsonPropertyDescription("The city and state e.g. San Francisco, CA") String location,
+ @JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) {
+
+ }
+
+ /**
+ * Weather Function response.
+ */
+ public record Response(double temp, double feels_like, double temp_min, double temp_max, int pressure, int humidity,
+ Unit unit) {
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfigurationIT.java
new file mode 100644
index 000000000..107bb2a6d
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfigurationIT.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.bedrock.autoconfigure.titan;
+
+import java.util.Base64;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.regions.Region;
+
+import org.springframework.ai.model.bedrock.autoconfigure.BedrockAwsConnectionProperties;
+import org.springframework.ai.model.bedrock.autoconfigure.BedrockTestUtils;
+import org.springframework.ai.model.bedrock.autoconfigure.RequiresAwsCredentials;
+import org.springframework.ai.bedrock.titan.BedrockTitanEmbeddingModel;
+import org.springframework.ai.bedrock.titan.BedrockTitanEmbeddingModel.InputType;
+import org.springframework.ai.bedrock.titan.api.TitanEmbeddingBedrockApi.TitanEmbeddingModel;
+import org.springframework.ai.embedding.EmbeddingResponse;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.core.io.DefaultResourceLoader;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Christian Tzolov
+ * @author Mark Pollack
+ * @since 1.0.0
+ */
+@RequiresAwsCredentials
+public class BedrockTitanEmbeddingAutoConfigurationIT {
+
+ private final ApplicationContextRunner contextRunner = BedrockTestUtils.getContextRunner()
+ .withPropertyValues("spring.ai.bedrock.titan.embedding.enabled=true",
+ "spring.ai.bedrock.aws.access-key=" + System.getenv("AWS_ACCESS_KEY_ID"),
+ "spring.ai.bedrock.aws.secret-key=" + System.getenv("AWS_SECRET_ACCESS_KEY"),
+ "spring.ai.bedrock.aws.region=" + Region.US_EAST_1.id(),
+ "spring.ai.bedrock.titan.embedding.model=" + TitanEmbeddingModel.TITAN_EMBED_IMAGE_V1.id())
+ .withConfiguration(AutoConfigurations.of(BedrockTitanEmbeddingAutoConfiguration.class));
+
+ @Test
+ public void singleTextEmbedding() {
+ this.contextRunner.withPropertyValues("spring.ai.bedrock.titan.embedding.inputType=TEXT").run(context -> {
+ BedrockTitanEmbeddingModel embeddingModel = context.getBean(BedrockTitanEmbeddingModel.class);
+ assertThat(embeddingModel).isNotNull();
+ EmbeddingResponse embeddingResponse = embeddingModel.embedForResponse(List.of("Hello World"));
+ assertThat(embeddingResponse.getResults()).hasSize(1);
+ assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty();
+ assertThat(embeddingModel.dimensions()).isEqualTo(1024);
+ });
+ }
+
+ @Test
+ public void singleImageEmbedding() {
+ this.contextRunner.withPropertyValues("spring.ai.bedrock.titan.embedding.inputType=IMAGE").run(context -> {
+ BedrockTitanEmbeddingModel embeddingModel = context.getBean(BedrockTitanEmbeddingModel.class);
+ assertThat(embeddingModel).isNotNull();
+
+ byte[] image = new DefaultResourceLoader().getResource("classpath:/spring_framework.png")
+ .getContentAsByteArray();
+
+ var base64Image = Base64.getEncoder().encodeToString(image);
+
+ EmbeddingResponse embeddingResponse = embeddingModel.embedForResponse(List.of(base64Image));
+
+ assertThat(embeddingResponse.getResults()).hasSize(1);
+ assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty();
+ assertThat(embeddingModel.dimensions()).isEqualTo(1024);
+ });
+ }
+
+ @Test
+ public void propertiesTest() {
+
+ BedrockTestUtils.getContextRunnerWithUserConfiguration()
+ .withPropertyValues("spring.ai.bedrock.titan.embedding.enabled=true",
+ "spring.ai.bedrock.aws.access-key=ACCESS_KEY", "spring.ai.bedrock.aws.secret-key=SECRET_KEY",
+ "spring.ai.bedrock.aws.region=" + Region.US_EAST_1.id(),
+ "spring.ai.bedrock.titan.embedding.model=MODEL_XYZ",
+ "spring.ai.bedrock.titan.embedding.inputType=TEXT")
+ .withConfiguration(AutoConfigurations.of(BedrockTitanEmbeddingAutoConfiguration.class))
+ .run(context -> {
+ var properties = context.getBean(BedrockTitanEmbeddingProperties.class);
+ var awsProperties = context.getBean(BedrockAwsConnectionProperties.class);
+
+ assertThat(properties.isEnabled()).isTrue();
+ assertThat(awsProperties.getRegion()).isEqualTo(Region.US_EAST_1.id());
+ assertThat(properties.getModel()).isEqualTo("MODEL_XYZ");
+
+ assertThat(properties.getInputType()).isEqualTo(InputType.TEXT);
+
+ assertThat(awsProperties.getAccessKey()).isEqualTo("ACCESS_KEY");
+ assertThat(awsProperties.getSecretKey()).isEqualTo("SECRET_KEY");
+ });
+ }
+
+ @Test
+ public void embeddingDisabled() {
+
+ // It is disabled by default
+ BedrockTestUtils.getContextRunnerWithUserConfiguration()
+ .withConfiguration(AutoConfigurations.of(BedrockTitanEmbeddingAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(BedrockTitanEmbeddingProperties.class)).isEmpty();
+ assertThat(context.getBeansOfType(BedrockTitanEmbeddingModel.class)).isEmpty();
+ });
+
+ // Explicitly enable the embedding auto-configuration.
+ BedrockTestUtils.getContextRunnerWithUserConfiguration()
+ .withPropertyValues("spring.ai.bedrock.titan.embedding.enabled=true")
+ .withConfiguration(AutoConfigurations.of(BedrockTitanEmbeddingAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(BedrockTitanEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(BedrockTitanEmbeddingModel.class)).isNotEmpty();
+ });
+
+ // Explicitly disable the embedding auto-configuration.
+ BedrockTestUtils.getContextRunnerWithUserConfiguration()
+ .withPropertyValues("spring.ai.bedrock.titan.embedding.enabled=false")
+ .withConfiguration(AutoConfigurations.of(BedrockTitanEmbeddingAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(BedrockTitanEmbeddingProperties.class)).isEmpty();
+ assertThat(context.getBeansOfType(BedrockTitanEmbeddingModel.class)).isEmpty();
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/chat/client/ChatClientAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientAutoConfigurationIT.java
similarity index 94%
rename from spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/chat/client/ChatClientAutoConfigurationIT.java
rename to spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientAutoConfigurationIT.java
index ae04bbd86..5906eb3b9 100644
--- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/chat/client/ChatClientAutoConfigurationIT.java
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientAutoConfigurationIT.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package org.springframework.ai.autoconfigure.chat.client;
+package org.springframework.ai.model.chat.client.autoconfigure;
import java.util.List;
@@ -23,8 +23,8 @@ import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
-import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
-import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.model.openai.autoconfigure.OpenAiAutoConfiguration;
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.ChatClientCustomizer;
import org.springframework.boot.autoconfigure.AutoConfigurations;
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientObservationAutoConfigurationTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientObservationAutoConfigurationTests.java
new file mode 100644
index 000000000..7fd0ee9f8
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientObservationAutoConfigurationTests.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.chat.client.autoconfigure;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.chat.client.observation.ChatClientInputContentObservationFilter;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit tests for {@link ChatClientAutoConfiguration} observability support.
+ *
+ * @author Christian Tzolov
+ */
+class ChatClientObservationAutoConfigurationTests {
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(ChatClientAutoConfiguration.class));
+
+ @Test
+ void inputContentFilterDefault() {
+ this.contextRunner
+ .run(context -> assertThat(context).doesNotHaveBean(ChatClientInputContentObservationFilter.class));
+ }
+
+ @Test
+ void inputContentFilterEnabled() {
+ this.contextRunner.withPropertyValues("spring.ai.chat.client.observations.include-input=true")
+ .run(context -> assertThat(context).hasSingleBean(ChatClientInputContentObservationFilter.class));
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryAutoConfigurationIT.java
new file mode 100644
index 000000000..a41904290
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryAutoConfigurationIT.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.chat.memory.autoconfigure.cassandra;
+
+import java.time.Duration;
+import java.util.List;
+
+import com.datastax.driver.core.utils.UUIDs;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.CassandraContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+import org.springframework.ai.chat.memory.cassandra.CassandraChatMemory;
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.messages.MessageType;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Mick Semb Wever
+ * @author Jihoon Kim
+ * @since 1.0.0
+ */
+@Testcontainers
+class CassandraChatMemoryAutoConfigurationIT {
+
+ static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("cassandra");
+
+ @Container
+ static CassandraContainer cassandraContainer = new CassandraContainer(DEFAULT_IMAGE_NAME.withTag("5.0"));
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(
+ AutoConfigurations.of(CassandraChatMemoryAutoConfiguration.class, CassandraAutoConfiguration.class))
+ .withPropertyValues("spring.ai.chat.memory.cassandra.keyspace=test_autoconfigure");
+
+ @Test
+ void addAndGet() {
+ this.contextRunner.withPropertyValues("spring.cassandra.contactPoints=" + getContactPointHost())
+ .withPropertyValues("spring.cassandra.port=" + getContactPointPort())
+ .withPropertyValues("spring.cassandra.localDatacenter=" + cassandraContainer.getLocalDatacenter())
+ .withPropertyValues("spring.ai.chat.memory.cassandra.time-to-live=" + getTimeToLive())
+ .run(context -> {
+ CassandraChatMemory memory = context.getBean(CassandraChatMemory.class);
+
+ String sessionId = UUIDs.timeBased().toString();
+ assertThat(memory.get(sessionId, Integer.MAX_VALUE)).isEmpty();
+
+ memory.add(sessionId, new UserMessage("test question"));
+
+ assertThat(memory.get(sessionId, Integer.MAX_VALUE)).hasSize(1);
+ assertThat(memory.get(sessionId, Integer.MAX_VALUE).get(0).getMessageType())
+ .isEqualTo(MessageType.USER);
+ assertThat(memory.get(sessionId, Integer.MAX_VALUE).get(0).getText()).isEqualTo("test question");
+
+ memory.clear(sessionId);
+ assertThat(memory.get(sessionId, Integer.MAX_VALUE)).isEmpty();
+
+ memory.add(sessionId, List.of(new UserMessage("test question"), new AssistantMessage("test answer")));
+
+ assertThat(memory.get(sessionId, Integer.MAX_VALUE)).hasSize(2);
+ assertThat(memory.get(sessionId, Integer.MAX_VALUE).get(1).getMessageType())
+ .isEqualTo(MessageType.USER);
+ assertThat(memory.get(sessionId, Integer.MAX_VALUE).get(1).getText()).isEqualTo("test question");
+ assertThat(memory.get(sessionId, Integer.MAX_VALUE).get(0).getMessageType())
+ .isEqualTo(MessageType.ASSISTANT);
+ assertThat(memory.get(sessionId, Integer.MAX_VALUE).get(0).getText()).isEqualTo("test answer");
+
+ CassandraChatMemoryProperties properties = context.getBean(CassandraChatMemoryProperties.class);
+ assertThat(properties.getTimeToLive()).isEqualTo(getTimeToLive());
+ });
+ }
+
+ @Test
+ void compareTimeToLive_ISO8601Format() {
+ this.contextRunner.withPropertyValues("spring.cassandra.contactPoints=" + getContactPointHost())
+ .withPropertyValues("spring.cassandra.port=" + getContactPointPort())
+ .withPropertyValues("spring.cassandra.localDatacenter=" + cassandraContainer.getLocalDatacenter())
+ .withPropertyValues("spring.ai.chat.memory.cassandra.time-to-live=" + getTimeToLiveString())
+ .run(context -> {
+ CassandraChatMemoryProperties properties = context.getBean(CassandraChatMemoryProperties.class);
+ assertThat(properties.getTimeToLive()).isEqualTo(Duration.parse(getTimeToLiveString()));
+ });
+ }
+
+ private String getContactPointHost() {
+ return cassandraContainer.getContactPoint().getHostString();
+ }
+
+ private String getContactPointPort() {
+ return String.valueOf(cassandraContainer.getContactPoint().getPort());
+ }
+
+ private Duration getTimeToLive() {
+ return Duration.ofSeconds(12000);
+ }
+
+ private String getTimeToLiveString() {
+ return "PT1M";
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/chat/memory/cassandra/CassandraChatMemoryPropertiesTest.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryPropertiesTest.java
similarity index 96%
rename from spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/chat/memory/cassandra/CassandraChatMemoryPropertiesTest.java
rename to spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryPropertiesTest.java
index 9d779b9b5..dab9eb806 100644
--- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/chat/memory/cassandra/CassandraChatMemoryPropertiesTest.java
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/memory/autoconfigure/cassandra/CassandraChatMemoryPropertiesTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package org.springframework.ai.autoconfigure.chat.memory.cassandra;
+package org.springframework.ai.model.chat.memory.autoconfigure.cassandra;
import java.time.Duration;
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatAutoConfigurationIT.java
new file mode 100644
index 000000000..db991fe08
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatAutoConfigurationIT.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.huggingface.autoconfigure;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.model.Generation;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.huggingface.HuggingfaceChatModel;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnabledIfEnvironmentVariable(named = "HUGGINGFACE_API_KEY", matches = ".+")
+@EnabledIfEnvironmentVariable(named = "HUGGINGFACE_CHAT_URL", matches = ".+")
+public class HuggingfaceChatAutoConfigurationIT {
+
+ private static final Log logger = LogFactory.getLog(HuggingfaceChatAutoConfigurationIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.huggingface.chat.api-key=" + System.getenv("HUGGINGFACE_API_KEY"),
+ "spring.ai.huggingface.chat.url=" + System.getenv("HUGGINGFACE_CHAT_URL"))
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(HuggingfaceChatAutoConfiguration.class));
+
+ @Test
+ void generate() {
+ this.contextRunner.run(context -> {
+ HuggingfaceChatModel chatModel = context.getBean(HuggingfaceChatModel.class);
+ String response = chatModel.call("Hello");
+ assertThat(response).isNotEmpty();
+ logger.info("Response: " + response);
+ });
+ }
+
+ @Disabled("Until streaming support is added")
+ @Test
+ void generateStreaming() {
+ this.contextRunner.run(context -> {
+ HuggingfaceChatModel chatModel = context.getBean(HuggingfaceChatModel.class);
+ Flux responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello")));
+
+ String response = responseFlux.collectList()
+ .block()
+ .stream()
+ .map(ChatResponse::getResults)
+ .flatMap(List::stream)
+ .map(Generation::getOutput)
+ .map(AssistantMessage::getText)
+ .collect(Collectors.joining());
+
+ assertThat(response).isNotEmpty();
+ logger.info("Response: " + response);
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/FunctionCallbackInPromptIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/FunctionCallbackInPromptIT.java
new file mode 100644
index 000000000..f86e76185
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/FunctionCallbackInPromptIT.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.minimax.autoconfigure;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.model.Generation;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.minimax.MiniMaxChatModel;
+import org.springframework.ai.minimax.MiniMaxChatOptions;
+import org.springframework.ai.model.function.FunctionCallback;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Geng Rong
+ */
+@EnabledIfEnvironmentVariable(named = "MINIMAX_API_KEY", matches = ".*")
+public class FunctionCallbackInPromptIT {
+
+ private final Logger logger = LoggerFactory.getLogger(FunctionCallbackInPromptIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.minimax.apiKey=" + System.getenv("MINIMAX_API_KEY"))
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class));
+
+ @Test
+ void functionCallTest() {
+ this.contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6.5s-chat").run(context -> {
+
+ MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class);
+
+ UserMessage userMessage = new UserMessage(
+ "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius.");
+
+ var promptOptions = MiniMaxChatOptions.builder()
+ .functionCallbacks(List.of(FunctionCallback.builder()
+ .function("CurrentWeatherService", new MockWeatherService())
+ .description("Get the weather in location")
+ .inputType(MockWeatherService.Request.class)
+ .build()))
+ .build();
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), promptOptions));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+ });
+ }
+
+ @Test
+ void streamingFunctionCallTest() {
+
+ this.contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6.5s-chat").run(context -> {
+
+ MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class);
+
+ UserMessage userMessage = new UserMessage(
+ "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius.");
+
+ var promptOptions = MiniMaxChatOptions.builder()
+ .functionCallbacks(List.of(FunctionCallback.builder()
+ .function("CurrentWeatherService", new MockWeatherService())
+ .description("Get the weather in location")
+ .inputType(MockWeatherService.Request.class)
+ .build()))
+ .build();
+
+ Flux response = chatModel.stream(new Prompt(List.of(userMessage), promptOptions));
+
+ String content = response.collectList()
+ .block()
+ .stream()
+ .map(ChatResponse::getResults)
+ .flatMap(List::stream)
+ .map(Generation::getOutput)
+ .map(AssistantMessage::getText)
+ .collect(Collectors.joining());
+ logger.info("Response: {}", content);
+
+ assertThat(content).containsAnyOf("30.0", "30");
+ assertThat(content).containsAnyOf("10.0", "10");
+ assertThat(content).containsAnyOf("15.0", "15");
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackWithPlainFunctionBeanIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/FunctionCallbackWithPlainFunctionBeanIT.java
similarity index 98%
rename from spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackWithPlainFunctionBeanIT.java
rename to spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/FunctionCallbackWithPlainFunctionBeanIT.java
index 520f5f810..6d511b709 100644
--- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackWithPlainFunctionBeanIT.java
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/FunctionCallbackWithPlainFunctionBeanIT.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package org.springframework.ai.autoconfigure.minimax;
+package org.springframework.ai.model.minimax.autoconfigure;
import java.util.List;
import java.util.function.Function;
@@ -26,7 +26,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
-import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfigurationIT.java
new file mode 100644
index 000000000..2b82ba7d4
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfigurationIT.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.minimax.autoconfigure;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.embedding.EmbeddingResponse;
+import org.springframework.ai.minimax.MiniMaxChatModel;
+import org.springframework.ai.minimax.MiniMaxEmbeddingModel;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Geng Rong
+ */
+@EnabledIfEnvironmentVariable(named = "MINIMAX_API_KEY", matches = ".*")
+public class MiniMaxAutoConfigurationIT {
+
+ private static final Log logger = LogFactory.getLog(MiniMaxAutoConfigurationIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.minimax.apiKey=" + System.getenv("MINIMAX_API_KEY"))
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class));
+
+ @Test
+ void generate() {
+ this.contextRunner.run(context -> {
+ MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class);
+ String response = chatModel.call("Hello");
+ assertThat(response).isNotEmpty();
+ logger.info("Response: " + response);
+ });
+ }
+
+ @Test
+ void generateStreaming() {
+ this.contextRunner.run(context -> {
+ MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class);
+ Flux responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello")));
+ String response = responseFlux.collectList()
+ .block()
+ .stream()
+ .map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText())
+ .collect(Collectors.joining());
+
+ assertThat(response).isNotEmpty();
+ logger.info("Response: " + response);
+ });
+ }
+
+ @Test
+ void embedding() {
+ this.contextRunner.run(context -> {
+ MiniMaxEmbeddingModel embeddingModel = context.getBean(MiniMaxEmbeddingModel.class);
+
+ EmbeddingResponse embeddingResponse = embeddingModel
+ .embedForResponse(List.of("Hello World", "World is big and salvation is near"));
+ assertThat(embeddingResponse.getResults()).hasSize(2);
+ assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty();
+ assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0);
+ assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty();
+ assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1);
+
+ assertThat(embeddingModel.dimensions()).isEqualTo(1536);
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxFunctionCallbackIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxFunctionCallbackIT.java
new file mode 100644
index 000000000..5bf102959
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxFunctionCallbackIT.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.minimax.autoconfigure;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.model.Generation;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.minimax.MiniMaxChatModel;
+import org.springframework.ai.minimax.MiniMaxChatOptions;
+import org.springframework.ai.model.function.FunctionCallback;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Geng Rong
+ */
+@EnabledIfEnvironmentVariable(named = "MINIMAX_API_KEY", matches = ".*")
+public class MiniMaxFunctionCallbackIT {
+
+ private final Logger logger = LoggerFactory.getLogger(MiniMaxFunctionCallbackIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.minimax.apiKey=" + System.getenv("MINIMAX_API_KEY"))
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class))
+ .withUserConfiguration(Config.class);
+
+ @Test
+ void functionCallTest() {
+ this.contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6.5s-chat").run(context -> {
+
+ MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class);
+
+ UserMessage userMessage = new UserMessage(
+ "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius.");
+
+ ChatResponse response = chatModel
+ .call(new Prompt(List.of(userMessage), MiniMaxChatOptions.builder().function("WeatherInfo").build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+
+ });
+ }
+
+ @Test
+ void streamFunctionCallTest() {
+ this.contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6.5s-chat").run(context -> {
+
+ MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class);
+
+ UserMessage userMessage = new UserMessage(
+ "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius.");
+
+ Flux response = chatModel
+ .stream(new Prompt(List.of(userMessage), MiniMaxChatOptions.builder().function("WeatherInfo").build()));
+
+ String content = response.collectList()
+ .block()
+ .stream()
+ .map(ChatResponse::getResults)
+ .flatMap(List::stream)
+ .map(Generation::getOutput)
+ .map(AssistantMessage::getText)
+ .collect(Collectors.joining());
+ logger.info("Response: {}", content);
+
+ assertThat(content).containsAnyOf("30.0", "30");
+ assertThat(content).containsAnyOf("10.0", "10");
+ assertThat(content).containsAnyOf("15.0", "15");
+
+ });
+ }
+
+ @Configuration
+ static class Config {
+
+ @Bean
+ public FunctionCallback weatherFunctionInfo() {
+
+ return FunctionCallback.builder()
+ .function("WeatherInfo", new MockWeatherService())
+ .description("Get the weather in location")
+ .inputType(MockWeatherService.Request.class)
+ .build();
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxPropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxPropertiesTests.java
new file mode 100644
index 000000000..b0a75f743
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxPropertiesTests.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.minimax.autoconfigure;
+
+import org.junit.jupiter.api.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.skyscreamer.jsonassert.JSONCompareMode;
+
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.minimax.MiniMaxChatModel;
+import org.springframework.ai.minimax.MiniMaxEmbeddingModel;
+import org.springframework.ai.minimax.api.MiniMaxApi;
+import org.springframework.ai.model.ModelOptionsUtils;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit Tests for {@link MiniMaxConnectionProperties}, {@link MiniMaxChatProperties} and
+ * {@link MiniMaxEmbeddingProperties}.
+ *
+ * @author Geng Rong
+ */
+public class MiniMaxPropertiesTests {
+
+ @Test
+ public void chatProperties() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.minimax.base-url=TEST_BASE_URL",
+ "spring.ai.minimax.api-key=abc123",
+ "spring.ai.minimax.chat.options.model=MODEL_XYZ",
+ "spring.ai.minimax.chat.options.temperature=0.55")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class))
+ .run(context -> {
+ var chatProperties = context.getBean(MiniMaxChatProperties.class);
+ var connectionProperties = context.getBean(MiniMaxConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(chatProperties.getApiKey()).isNull();
+ assertThat(chatProperties.getBaseUrl()).isNull();
+
+ assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55);
+ });
+ }
+
+ @Test
+ public void chatOverrideConnectionProperties() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.minimax.base-url=TEST_BASE_URL",
+ "spring.ai.minimax.api-key=abc123",
+ "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")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class))
+ .run(context -> {
+ var chatProperties = context.getBean(MiniMaxChatProperties.class);
+ var connectionProperties = context.getBean(MiniMaxConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(chatProperties.getApiKey()).isEqualTo("456");
+ assertThat(chatProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2");
+
+ assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55);
+ });
+ }
+
+ @Test
+ public void embeddingProperties() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.minimax.base-url=TEST_BASE_URL",
+ "spring.ai.minimax.api-key=abc123",
+ "spring.ai.minimax.embedding.options.model=MODEL_XYZ")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class))
+ .run(context -> {
+ var embeddingProperties = context.getBean(MiniMaxEmbeddingProperties.class);
+ var connectionProperties = context.getBean(MiniMaxConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(embeddingProperties.getApiKey()).isNull();
+ assertThat(embeddingProperties.getBaseUrl()).isNull();
+
+ assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ });
+ }
+
+ @Test
+ public void embeddingOverrideConnectionProperties() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.minimax.base-url=TEST_BASE_URL",
+ "spring.ai.minimax.api-key=abc123",
+ "spring.ai.minimax.embedding.base-url=TEST_BASE_URL2",
+ "spring.ai.minimax.embedding.api-key=456",
+ "spring.ai.minimax.embedding.options.model=MODEL_XYZ")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class))
+ .run(context -> {
+ var embeddingProperties = context.getBean(MiniMaxEmbeddingProperties.class);
+ var connectionProperties = context.getBean(MiniMaxConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(embeddingProperties.getApiKey()).isEqualTo("456");
+ assertThat(embeddingProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2");
+
+ assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ });
+ }
+
+ @Test
+ public void chatOptionsTest() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.minimax.api-key=API_KEY",
+ "spring.ai.minimax.base-url=TEST_BASE_URL",
+
+ "spring.ai.minimax.chat.options.model=MODEL_XYZ",
+ "spring.ai.minimax.chat.options.frequencyPenalty=-1.5",
+ "spring.ai.minimax.chat.options.logitBias.myTokenId=-5",
+ "spring.ai.minimax.chat.options.maxTokens=123",
+ "spring.ai.minimax.chat.options.n=10",
+ "spring.ai.minimax.chat.options.presencePenalty=0",
+ "spring.ai.minimax.chat.options.responseFormat.type=json",
+ "spring.ai.minimax.chat.options.seed=66",
+ "spring.ai.minimax.chat.options.stop=boza,koza",
+ "spring.ai.minimax.chat.options.temperature=0.55",
+ "spring.ai.minimax.chat.options.topP=0.56",
+
+ // "spring.ai.minimax.chat.options.toolChoice.functionName=toolChoiceFunctionName",
+ "spring.ai.minimax.chat.options.toolChoice=" + ModelOptionsUtils.toJsonString(MiniMaxApi.ChatCompletionRequest.ToolChoiceBuilder.function("toolChoiceFunctionName")),
+
+ "spring.ai.minimax.chat.options.tools[0].function.name=myFunction1",
+ "spring.ai.minimax.chat.options.tools[0].function.description=function description",
+ "spring.ai.minimax.chat.options.tools[0].function.jsonSchema=" + """
+ {
+ "type": "object",
+ "properties": {
+ "location": {
+ "type": "string",
+ "description": "The city and state e.g. San Francisco, CA"
+ },
+ "lat": {
+ "type": "number",
+ "description": "The city latitude"
+ },
+ "lon": {
+ "type": "number",
+ "description": "The city longitude"
+ },
+ "unit": {
+ "type": "string",
+ "enum": ["c", "f"]
+ }
+ },
+ "required": ["location", "lat", "lon", "unit"]
+ }
+ """
+ )
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class))
+ .run(context -> {
+ var chatProperties = context.getBean(MiniMaxChatProperties.class);
+ var connectionProperties = context.getBean(MiniMaxConnectionProperties.class);
+ var embeddingProperties = context.getBean(MiniMaxEmbeddingProperties.class);
+
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+ assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY");
+
+ assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("embo-01");
+
+ assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(chatProperties.getOptions().getFrequencyPenalty()).isEqualTo(-1.5);
+ assertThat(chatProperties.getOptions().getMaxTokens()).isEqualTo(123);
+ assertThat(chatProperties.getOptions().getN()).isEqualTo(10);
+ assertThat(chatProperties.getOptions().getPresencePenalty()).isEqualTo(0);
+ assertThat(chatProperties.getOptions().getResponseFormat())
+ .isEqualTo(new MiniMaxApi.ChatCompletionRequest.ResponseFormat("json"));
+ assertThat(chatProperties.getOptions().getSeed()).isEqualTo(66);
+ assertThat(chatProperties.getOptions().getStop()).contains("boza", "koza");
+ assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55);
+ assertThat(chatProperties.getOptions().getTopP()).isEqualTo(0.56);
+
+ JSONAssert.assertEquals("{\"type\":\"function\",\"function\":{\"name\":\"toolChoiceFunctionName\"}}",
+ chatProperties.getOptions().getToolChoice(), JSONCompareMode.LENIENT);
+
+ assertThat(chatProperties.getOptions().getTools()).hasSize(1);
+ var tool = chatProperties.getOptions().getTools().get(0);
+ assertThat(tool.getType()).isEqualTo(MiniMaxApi.FunctionTool.Type.FUNCTION);
+ var function = tool.getFunction();
+ assertThat(function.getName()).isEqualTo("myFunction1");
+ assertThat(function.getDescription()).isEqualTo("function description");
+ assertThat(function.getParameters()).isNotEmpty();
+ });
+ }
+
+ @Test
+ public void embeddingOptionsTest() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.minimax.api-key=API_KEY",
+ "spring.ai.minimax.base-url=TEST_BASE_URL",
+
+ "spring.ai.minimax.embedding.options.model=MODEL_XYZ",
+ "spring.ai.minimax.embedding.options.encodingFormat=MyEncodingFormat"
+ )
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class))
+ .run(context -> {
+ var connectionProperties = context.getBean(MiniMaxConnectionProperties.class);
+ var embeddingProperties = context.getBean(MiniMaxEmbeddingProperties.class);
+
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+ assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY");
+
+ assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ });
+ }
+
+ @Test
+ void embeddingActivation() {
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL",
+ "spring.ai.minimax.embedding.enabled=false")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isNotEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL",
+ "spring.ai.minimax.embedding.enabled=true")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isNotEmpty();
+ });
+ }
+
+ @Test
+ void chatActivation() {
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL",
+ "spring.ai.minimax.chat.enabled=false")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(MiniMaxChatProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(MiniMaxChatModel.class)).isEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(MiniMaxChatProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(MiniMaxChatModel.class)).isNotEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL",
+ "spring.ai.minimax.chat.enabled=true")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(MiniMaxChatProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(MiniMaxChatModel.class)).isNotEmpty();
+ });
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MockWeatherService.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MockWeatherService.java
new file mode 100644
index 000000000..3c95cf669
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MockWeatherService.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.minimax.autoconfigure;
+
+import java.util.function.Function;
+
+import com.fasterxml.jackson.annotation.JsonClassDescription;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyDescription;
+
+/**
+ * Mock 3rd party weather service.
+ *
+ * @author Geng Rong
+ */
+public class MockWeatherService implements Function {
+
+ @Override
+ public Response apply(Request request) {
+
+ double temperature = 0;
+ if (request.location().contains("Paris")) {
+ temperature = 15;
+ }
+ else if (request.location().contains("Tokyo")) {
+ temperature = 10;
+ }
+ else if (request.location().contains("San Francisco")) {
+ temperature = 30;
+ }
+
+ return new Response(temperature, 15, 20, 2, 53, 45, Unit.C);
+ }
+
+ /**
+ * Temperature units.
+ */
+ public enum Unit {
+
+ /**
+ * Celsius.
+ */
+ C("metric"),
+ /**
+ * Fahrenheit.
+ */
+ F("imperial");
+
+ /**
+ * Human readable unit name.
+ */
+ public final String unitName;
+
+ Unit(String text) {
+ this.unitName = text;
+ }
+
+ }
+
+ /**
+ * Weather Function request.
+ */
+ @JsonInclude(Include.NON_NULL)
+ @JsonClassDescription("Get the weather in location")
+ public record Request(@JsonProperty(required = true,
+ value = "location") @JsonPropertyDescription("The city and state e.g. San Francisco, CA") String location,
+ @JsonProperty(required = true, value = "lat") @JsonPropertyDescription("The city latitude") double lat,
+ @JsonProperty(required = true, value = "lon") @JsonPropertyDescription("The city longitude") double lon,
+ @JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) {
+
+ }
+
+ /**
+ * Weather Function response.
+ */
+ public record Response(double temp, double feels_like, double temp_min, double temp_max, int pressure, int humidity,
+ Unit unit) {
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfigurationIT.java
new file mode 100644
index 000000000..b7bb85226
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfigurationIT.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.mistralai.autoconfigure;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.embedding.EmbeddingResponse;
+import org.springframework.ai.mistralai.MistralAiChatModel;
+import org.springframework.ai.mistralai.MistralAiEmbeddingModel;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Christian Tzolov
+ * @since 0.8.1
+ */
+@EnabledIfEnvironmentVariable(named = "MISTRAL_AI_API_KEY", matches = ".*")
+public class MistralAiAutoConfigurationIT {
+
+ private static final Log logger = LogFactory.getLog(MistralAiAutoConfigurationIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.mistralai.apiKey=" + System.getenv("MISTRAL_AI_API_KEY"))
+ .withConfiguration(AutoConfigurations.of(MistralAiAutoConfiguration.class));
+
+ @Test
+ void generate() {
+ this.contextRunner.run(context -> {
+ MistralAiChatModel chatModel = context.getBean(MistralAiChatModel.class);
+ String response = chatModel.call("Hello");
+ assertThat(response).isNotEmpty();
+ logger.info("Response: " + response);
+ });
+ }
+
+ @Test
+ void generateStreaming() {
+ this.contextRunner.run(context -> {
+ MistralAiChatModel chatModel = context.getBean(MistralAiChatModel.class);
+ Flux responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello")));
+ String response = responseFlux.collectList()
+ .block()
+ .stream()
+ .map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText())
+ .collect(Collectors.joining());
+
+ assertThat(response).isNotEmpty();
+ logger.info("Response: " + response);
+ });
+ }
+
+ @Test
+ void embedding() {
+ this.contextRunner.run(context -> {
+ MistralAiEmbeddingModel embeddingModel = context.getBean(MistralAiEmbeddingModel.class);
+
+ EmbeddingResponse embeddingResponse = embeddingModel
+ .embedForResponse(List.of("Hello World", "World is big and salvation is near"));
+ assertThat(embeddingResponse.getResults()).hasSize(2);
+ assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty();
+ assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0);
+ assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty();
+ assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1);
+
+ assertThat(embeddingModel.dimensions()).isEqualTo(1024);
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiPropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiPropertiesTests.java
new file mode 100644
index 000000000..dd4eae9ab
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiPropertiesTests.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.mistralai.autoconfigure;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.mistralai.api.MistralAiApi;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit Tests for {@link MistralAiCommonProperties}, {@link MistralAiEmbeddingProperties}.
+ */
+public class MistralAiPropertiesTests {
+
+ @Test
+ public void embeddingProperties() {
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.mistralai.base-url=TEST_BASE_URL", "spring.ai.mistralai.api-key=abc123",
+ "spring.ai.mistralai.embedding.options.model=MODEL_XYZ")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MistralAiAutoConfiguration.class))
+ .run(context -> {
+ var embeddingProperties = context.getBean(MistralAiEmbeddingProperties.class);
+ var connectionProperties = context.getBean(MistralAiCommonProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(embeddingProperties.getApiKey()).isNull();
+ assertThat(embeddingProperties.getBaseUrl()).isEqualTo(MistralAiCommonProperties.DEFAULT_BASE_URL);
+
+ assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ });
+ }
+
+ @Test
+ public void chatOptionsTest() {
+
+ new ApplicationContextRunner().withPropertyValues("spring.ai.mistralai.base-url=TEST_BASE_URL",
+ "spring.ai.mistralai.chat.options.tools[0].function.name=myFunction1",
+ "spring.ai.mistralai.chat.options.tools[0].function.description=function description",
+ "spring.ai.mistralai.chat.options.tools[0].function.jsonSchema=" + """
+ {
+ "type": "object",
+ "properties": {
+ "location": {
+ "type": "string",
+ "description": "The city and state e.g. San Francisco, CA"
+ },
+ "lat": {
+ "type": "number",
+ "description": "The city latitude"
+ },
+ "lon": {
+ "type": "number",
+ "description": "The city longitude"
+ },
+ "unit": {
+ "type": "string",
+ "enum": ["c", "f"]
+ }
+ },
+ "required": ["location", "lat", "lon", "unit"]
+ }
+ """,
+
+ "spring.ai.mistralai.api-key=abc123", "spring.ai.mistralai.embedding.base-url=TEST_BASE_URL2",
+ "spring.ai.mistralai.embedding.api-key=456", "spring.ai.mistralai.embedding.options.model=MODEL_XYZ")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MistralAiAutoConfiguration.class))
+ .run(context -> {
+
+ var chatProperties = context.getBean(MistralAiChatProperties.class);
+
+ var tool = chatProperties.getOptions().getTools().get(0);
+ assertThat(tool.getType()).isEqualTo(MistralAiApi.FunctionTool.Type.FUNCTION);
+ var function = tool.getFunction();
+ assertThat(function.getName()).isEqualTo("myFunction1");
+ assertThat(function.getDescription()).isEqualTo("function description");
+ assertThat(function.getParameters()).isNotEmpty();
+ });
+ }
+
+ @Test
+ public void embeddingOverrideConnectionProperties() {
+
+ new ApplicationContextRunner().withPropertyValues("spring.ai.mistralai.base-url=TEST_BASE_URL",
+ "spring.ai.mistralai.api-key=abc123", "spring.ai.mistralai.embedding.base-url=TEST_BASE_URL2",
+ "spring.ai.mistralai.embedding.api-key=456", "spring.ai.mistralai.embedding.options.model=MODEL_XYZ")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MistralAiAutoConfiguration.class))
+ .run(context -> {
+ var embeddingProperties = context.getBean(MistralAiEmbeddingProperties.class);
+ var connectionProperties = context.getBean(MistralAiCommonProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(embeddingProperties.getApiKey()).isEqualTo("456");
+ assertThat(embeddingProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2");
+
+ assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ });
+ }
+
+ @Test
+ public void embeddingOptionsTest() {
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.mistralai.api-key=API_KEY", "spring.ai.mistralai.base-url=TEST_BASE_URL",
+
+ "spring.ai.mistralai.embedding.options.model=MODEL_XYZ",
+ "spring.ai.mistralai.embedding.options.encodingFormat=MyEncodingFormat")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MistralAiAutoConfiguration.class))
+ .run(context -> {
+ var connectionProperties = context.getBean(MistralAiCommonProperties.class);
+ var embeddingProperties = context.getBean(MistralAiEmbeddingProperties.class);
+
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+ assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY");
+
+ assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(embeddingProperties.getOptions().getEncodingFormat()).isEqualTo("MyEncodingFormat");
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusBeanIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusBeanIT.java
new file mode 100644
index 000000000..bd7ed874a
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusBeanIT.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.mistralai.autoconfigure.tool;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.ai.model.mistralai.autoconfigure.MistralAiAutoConfiguration;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.mistralai.MistralAiChatModel;
+import org.springframework.ai.mistralai.MistralAiChatOptions;
+import org.springframework.ai.mistralai.api.MistralAiApi;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Description;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnabledIfEnvironmentVariable(named = "MISTRAL_AI_API_KEY", matches = ".*")
+class PaymentStatusBeanIT {
+
+ // Assuming we have the following data
+ public static final Map DATA = Map.of("T1001", new StatusDate("Paid", "2021-10-05"), "T1002",
+ new StatusDate("Unpaid", "2021-10-06"), "T1003", new StatusDate("Paid", "2021-10-07"), "T1004",
+ new StatusDate("Paid", "2021-10-05"), "T1005", new StatusDate("Pending", "2021-10-08"));
+
+ private final Logger logger = LoggerFactory.getLogger(PaymentStatusBeanIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.mistralai.apiKey=" + System.getenv("MISTRAL_AI_API_KEY"))
+ .withConfiguration(AutoConfigurations.of(MistralAiAutoConfiguration.class))
+ .withUserConfiguration(Config.class);
+
+ @Test
+ void functionCallTest() {
+
+ this.contextRunner
+ .withPropertyValues("spring.ai.mistralai.chat.options.model=" + MistralAiApi.ChatModel.LARGE.getValue())
+ .run(context -> {
+
+ MistralAiChatModel chatModel = context.getBean(MistralAiChatModel.class);
+
+ ChatResponse response = chatModel
+ .call(new Prompt(List.of(new UserMessage("What's the status of my transaction with id T1001?")),
+ MistralAiChatOptions.builder()
+ .function("retrievePaymentStatus")
+ .function("retrievePaymentDate")
+ .build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).containsIgnoringCase("T1001");
+ assertThat(response.getResult().getOutput().getText()).containsIgnoringCase("paid");
+ });
+ }
+
+ record StatusDate(String status, String date) {
+
+ }
+
+ @Configuration
+ static class Config {
+
+ @Bean
+ @Description("Get payment status of a transaction")
+ public Function retrievePaymentStatus() {
+ return transaction -> new Status(DATA.get(transaction.transactionId).status());
+ }
+
+ @Bean
+ @Description("Get payment date of a transaction")
+ public Function retrievePaymentDate() {
+ return transaction -> new Date(DATA.get(transaction.transactionId).date());
+ }
+
+ public record Transaction(@JsonProperty(required = true, value = "transaction_id") String transactionId) {
+
+ }
+
+ public record Status(@JsonProperty(required = true, value = "status") String status) {
+
+ }
+
+ public record Date(@JsonProperty(required = true, value = "date") String date) {
+
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusBeanOpenAiIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusBeanOpenAiIT.java
new file mode 100644
index 000000000..9c4b1c71a
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusBeanOpenAiIT.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.mistralai.autoconfigure.tool;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.ai.model.openai.autoconfigure.OpenAiAutoConfiguration;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.mistralai.api.MistralAiApi;
+import org.springframework.ai.openai.OpenAiChatModel;
+import org.springframework.ai.openai.OpenAiChatOptions;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Description;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Same test as {@link PaymentStatusBeanIT.java} but using {@link OpenAiChatModel} for
+ * Mistral AI Function Calling implementation.
+ *
+ * @author Christian Tzolov
+ */
+@EnabledIfEnvironmentVariable(named = "MISTRAL_AI_API_KEY", matches = ".*")
+class PaymentStatusBeanOpenAiIT {
+
+ // Assuming we have the following data
+ public static final Map DATA = Map.of("T1001", new StatusDate("Paid", "2021-10-05"), "T1002",
+ new StatusDate("Unpaid", "2021-10-06"), "T1003", new StatusDate("Paid", "2021-10-07"), "T1004",
+ new StatusDate("Paid", "2021-10-05"), "T1005", new StatusDate("Pending", "2021-10-08"));
+
+ private final Logger logger = LoggerFactory.getLogger(PaymentStatusBeanIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("MISTRAL_AI_API_KEY"),
+ "spring.ai.openai.chat.base-url=https://api.mistral.ai")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .withUserConfiguration(Config.class);
+
+ @Test
+ void functionCallTest() {
+
+ this.contextRunner
+ .withPropertyValues("spring.ai.openai.chat.options.model=" + MistralAiApi.ChatModel.SMALL.getValue())
+ .run(context -> {
+
+ OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class);
+
+ ChatResponse response = chatModel
+ .call(new Prompt(List.of(new UserMessage("What's the status of my transaction with id T1001?")),
+ OpenAiChatOptions.builder()
+ .function("retrievePaymentStatus")
+ .function("retrievePaymentDate")
+ .build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).containsIgnoringCase("T1001");
+ assertThat(response.getResult().getOutput().getText()).containsIgnoringCase("paid");
+ });
+ }
+
+ record StatusDate(String status, String date) {
+
+ }
+
+ @Configuration
+ static class Config {
+
+ @Bean
+ @Description("Get payment status of a transaction")
+ public Function retrievePaymentStatus() {
+ return transaction -> new Status(DATA.get(transaction.transactionId).status());
+ }
+
+ @Bean
+ @Description("Get payment date of a transaction")
+ public Function retrievePaymentDate() {
+ return transaction -> new Date(DATA.get(transaction.transactionId).date());
+ }
+
+ public record Transaction(@JsonProperty(required = true, value = "transaction_id") String transactionId) {
+
+ }
+
+ public record Status(@JsonProperty(required = true, value = "status") String status) {
+
+ }
+
+ public record Date(@JsonProperty(required = true, value = "date") String date) {
+
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusPromptIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusPromptIT.java
new file mode 100644
index 000000000..01a278a3d
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusPromptIT.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.mistralai.autoconfigure.tool;
+
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.ai.model.mistralai.autoconfigure.MistralAiAutoConfiguration;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.mistralai.MistralAiChatModel;
+import org.springframework.ai.mistralai.MistralAiChatOptions;
+import org.springframework.ai.mistralai.api.MistralAiApi;
+import org.springframework.ai.tool.function.FunctionToolCallback;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnabledIfEnvironmentVariable(named = "MISTRAL_AI_API_KEY", matches = ".*")
+public class PaymentStatusPromptIT {
+
+ // Assuming we have the following payment data.
+ public static final Map DATA = Map.of(new Transaction("T1001"),
+ new StatusDate("Paid", "2021-10-05"), new Transaction("T1002"), new StatusDate("Unpaid", "2021-10-06"),
+ new Transaction("T1003"), new StatusDate("Paid", "2021-10-07"), new Transaction("T1004"),
+ new StatusDate("Paid", "2021-10-05"), new Transaction("T1005"), new StatusDate("Pending", "2021-10-08"));
+
+ private final Logger logger = LoggerFactory.getLogger(WeatherServicePromptIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.mistralai.apiKey=" + System.getenv("MISTRAL_AI_API_KEY"))
+ .withConfiguration(AutoConfigurations.of(MistralAiAutoConfiguration.class));
+
+ @Test
+ void functionCallTest() {
+ this.contextRunner
+ .withPropertyValues("spring.ai.mistralai.chat.options.model=" + MistralAiApi.ChatModel.SMALL.getValue())
+ .run(context -> {
+
+ MistralAiChatModel chatModel = context.getBean(MistralAiChatModel.class);
+
+ UserMessage userMessage = new UserMessage("What's the status of my transaction with id T1001?");
+
+ var promptOptions = MistralAiChatOptions.builder()
+ .functionCallbacks(List.of(FunctionToolCallback
+ .builder("retrievePaymentStatus",
+ (Transaction transaction) -> new Status(DATA.get(transaction).status()))
+ .description("Get payment status of a transaction")
+ .inputType(Transaction.class)
+ .build()))
+ .build();
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), promptOptions));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).containsIgnoringCase("T1001");
+ assertThat(response.getResult().getOutput().getText()).containsIgnoringCase("paid");
+ });
+ }
+
+ public record Transaction(@JsonProperty(required = true, value = "transaction_id") String id) {
+
+ }
+
+ public record Status(@JsonProperty(required = true, value = "status") String status) {
+
+ }
+
+ record StatusDate(String status, String date) {
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/WeatherServicePromptIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/WeatherServicePromptIT.java
new file mode 100644
index 000000000..758ab779f
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/WeatherServicePromptIT.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.mistralai.autoconfigure.tool;
+
+import java.util.List;
+import java.util.function.Function;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.ai.model.mistralai.autoconfigure.MistralAiAutoConfiguration;
+import org.springframework.ai.model.mistralai.autoconfigure.tool.WeatherServicePromptIT.MyWeatherService.Request;
+import org.springframework.ai.model.mistralai.autoconfigure.tool.WeatherServicePromptIT.MyWeatherService.Response;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.mistralai.MistralAiChatModel;
+import org.springframework.ai.mistralai.MistralAiChatOptions;
+import org.springframework.ai.mistralai.api.MistralAiApi;
+import org.springframework.ai.mistralai.api.MistralAiApi.ChatCompletionRequest.ToolChoice;
+import org.springframework.ai.model.tool.ToolCallingChatOptions;
+import org.springframework.ai.tool.function.FunctionToolCallback;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Christian Tzolov
+ * @author Alexandros Pappas
+ * @since 0.8.1
+ */
+@EnabledIfEnvironmentVariable(named = "MISTRAL_AI_API_KEY", matches = ".*")
+public class WeatherServicePromptIT {
+
+ private final Logger logger = LoggerFactory.getLogger(WeatherServicePromptIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.mistralai.api-key=" + System.getenv("MISTRAL_AI_API_KEY"))
+ .withConfiguration(AutoConfigurations.of(MistralAiAutoConfiguration.class));
+
+ @Test
+ void promptFunctionCall() {
+ this.contextRunner
+ .withPropertyValues("spring.ai.mistralai.chat.options.model=" + MistralAiApi.ChatModel.LARGE.getValue())
+ .run(context -> {
+
+ MistralAiChatModel chatModel = context.getBean(MistralAiChatModel.class);
+
+ UserMessage userMessage = new UserMessage("What's the weather like in Paris? Use Celsius.");
+ // UserMessage userMessage = new UserMessage("What's the weather like in
+ // San Francisco, Tokyo, and
+ // Paris?");
+
+ var promptOptions = MistralAiChatOptions.builder()
+ .toolChoice(ToolChoice.AUTO)
+ .functionCallbacks(
+ List.of(FunctionToolCallback.builder("CurrentWeatherService", new MyWeatherService())
+ .description("Get the current weather in requested location")
+ .inputType(MyWeatherService.Request.class)
+ .build()))
+ .build();
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), promptOptions));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).containsAnyOf("15", "15.0");
+ });
+ }
+
+ @Test
+ void functionCallWithPortableFunctionCallingOptions() {
+ this.contextRunner
+ .withPropertyValues("spring.ai.mistralai.chat.options.model=" + MistralAiApi.ChatModel.LARGE.getValue())
+ .run(context -> {
+
+ MistralAiChatModel chatModel = context.getBean(MistralAiChatModel.class);
+
+ UserMessage userMessage = new UserMessage("What's the weather like in Paris? Use Celsius.");
+
+ ToolCallingChatOptions functionOptions = ToolCallingChatOptions.builder()
+ .toolCallbacks(List.of(FunctionToolCallback.builder("CurrentWeatherService", new MyWeatherService())
+ .description("Get the current weather in requested location")
+ .inputType(MyWeatherService.Request.class)
+ .build()))
+
+ .build();
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), functionOptions));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).containsAnyOf("15", "15.0");
+ });
+ }
+
+ public static class MyWeatherService implements Function {
+
+ @Override
+ public Response apply(Request request) {
+ if (request.location().contains("Paris")) {
+ return new Response(15, request.unit());
+ }
+ else if (request.location().contains("Tokyo")) {
+ return new Response(10, request.unit());
+ }
+ else if (request.location().contains("San Francisco")) {
+ return new Response(30, request.unit());
+ }
+ throw new IllegalArgumentException("Invalid request: " + request);
+ }
+
+ // @formatter:off
+ public enum Unit { C, F }
+
+ @JsonInclude(Include.NON_NULL)
+ public record Request(
+ @JsonProperty(required = true, value = "location") String location,
+ @JsonProperty(required = true, value = "unit") Unit unit) { }
+ // @formatter:on
+
+ public record Response(double temperature, Unit unit) {
+
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotAutoConfigurationIT.java
new file mode 100644
index 000000000..24f2fced3
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotAutoConfigurationIT.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.moonshot.autoconfigure;
+
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.moonshot.MoonshotChatModel;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Geng Rong
+ */
+@EnabledIfEnvironmentVariable(named = "MOONSHOT_API_KEY", matches = ".*")
+public class MoonshotAutoConfigurationIT {
+
+ private static final Log logger = LogFactory.getLog(MoonshotAutoConfigurationIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.moonshot.apiKey=" + System.getenv("MOONSHOT_API_KEY"))
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class));
+
+ @Test
+ void generate() {
+ this.contextRunner.run(context -> {
+ MoonshotChatModel client = context.getBean(MoonshotChatModel.class);
+ String response = client.call("Hello");
+ assertThat(response).isNotEmpty();
+ logger.info("Response: " + response);
+ });
+ }
+
+ @Test
+ void generateStreaming() {
+ this.contextRunner.run(context -> {
+ MoonshotChatModel client = context.getBean(MoonshotChatModel.class);
+ 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: " + response);
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotPropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotPropertiesTests.java
new file mode 100644
index 000000000..c97df8b12
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotPropertiesTests.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.moonshot.autoconfigure;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.moonshot.MoonshotChatModel;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Geng Rong
+ */
+public class MoonshotPropertiesTests {
+
+ @Test
+ public void chatProperties() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.moonshot.base-url=TEST_BASE_URL",
+ "spring.ai.moonshot.api-key=abc123",
+ "spring.ai.moonshot.chat.options.model=MODEL_XYZ",
+ "spring.ai.moonshot.chat.options.temperature=0.55")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class))
+ .run(context -> {
+ var chatProperties = context.getBean(MoonshotChatProperties.class);
+ var connectionProperties = context.getBean(MoonshotCommonProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(chatProperties.getApiKey()).isNull();
+ assertThat(chatProperties.getBaseUrl()).isNull();
+
+ assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55);
+ });
+ }
+
+ @Test
+ public void chatOverrideConnectionProperties() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.moonshot.base-url=TEST_BASE_URL",
+ "spring.ai.moonshot.api-key=abc123",
+ "spring.ai.moonshot.chat.base-url=TEST_BASE_URL2",
+ "spring.ai.moonshot.chat.api-key=456",
+ "spring.ai.moonshot.chat.options.model=MODEL_XYZ",
+ "spring.ai.moonshot.chat.options.temperature=0.55")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class))
+ .run(context -> {
+ var chatProperties = context.getBean(MoonshotChatProperties.class);
+ var connectionProperties = context.getBean(MoonshotCommonProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(chatProperties.getApiKey()).isEqualTo("456");
+ assertThat(chatProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2");
+
+ assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55);
+ });
+ }
+
+ @Test
+ public void chatOptionsTest() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.moonshot.api-key=API_KEY",
+ "spring.ai.moonshot.base-url=TEST_BASE_URL",
+
+ "spring.ai.moonshot.chat.options.model=MODEL_XYZ",
+ "spring.ai.moonshot.chat.options.frequencyPenalty=-1.5",
+ "spring.ai.moonshot.chat.options.logitBias.myTokenId=-5",
+ "spring.ai.moonshot.chat.options.maxTokens=123",
+ "spring.ai.moonshot.chat.options.n=10",
+ "spring.ai.moonshot.chat.options.presencePenalty=0",
+ "spring.ai.moonshot.chat.options.responseFormat.type=json",
+ "spring.ai.moonshot.chat.options.seed=66",
+ "spring.ai.moonshot.chat.options.stop=boza,koza",
+ "spring.ai.moonshot.chat.options.temperature=0.55",
+ "spring.ai.moonshot.chat.options.topP=0.56",
+ "spring.ai.moonshot.chat.options.user=userXYZ"
+ )
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class))
+ .run(context -> {
+ var chatProperties = context.getBean(MoonshotChatProperties.class);
+ var connectionProperties = context.getBean(MoonshotCommonProperties.class);
+
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+ assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY");
+
+ assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(chatProperties.getOptions().getFrequencyPenalty()).isEqualTo(-1.5);
+ assertThat(chatProperties.getOptions().getMaxTokens()).isEqualTo(123);
+ assertThat(chatProperties.getOptions().getN()).isEqualTo(10);
+ assertThat(chatProperties.getOptions().getPresencePenalty()).isEqualTo(0);
+ assertThat(chatProperties.getOptions().getStop()).contains("boza", "koza");
+ assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55);
+ assertThat(chatProperties.getOptions().getTopP()).isEqualTo(0.56);
+
+ assertThat(chatProperties.getOptions().getUser()).isEqualTo("userXYZ");
+ });
+ }
+
+ @Test
+ void chatActivation() {
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.moonshot.api-key=API_KEY", "spring.ai.moonshot.base-url=TEST_BASE_URL",
+ "spring.ai.moonshot.chat.enabled=false")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(MoonshotChatProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(MoonshotChatModel.class)).isEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.moonshot.api-key=API_KEY", "spring.ai.moonshot.base-url=TEST_BASE_URL")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(MoonshotChatProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(MoonshotChatModel.class)).isNotEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.moonshot.api-key=API_KEY", "spring.ai.moonshot.base-url=TEST_BASE_URL",
+ "spring.ai.moonshot.chat.enabled=true")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(MoonshotChatProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(MoonshotChatModel.class)).isNotEmpty();
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/FunctionCallbackInPromptIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/FunctionCallbackInPromptIT.java
new file mode 100644
index 000000000..0dc2271f3
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/FunctionCallbackInPromptIT.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.moonshot.autoconfigure.tool;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.model.moonshot.autoconfigure.MoonshotAutoConfiguration;
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.model.Generation;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.model.function.FunctionCallback;
+import org.springframework.ai.moonshot.MoonshotChatModel;
+import org.springframework.ai.moonshot.MoonshotChatOptions;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Geng Rong
+ * @author Alexandros Pappas
+ */
+@EnabledIfEnvironmentVariable(named = "MOONSHOT_API_KEY", matches = ".*")
+public class FunctionCallbackInPromptIT {
+
+ private final Logger logger = LoggerFactory.getLogger(FunctionCallbackInPromptIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.moonshot.apiKey=" + System.getenv("MOONSHOT_API_KEY"))
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class));
+
+ @Test
+ void functionCallTest() {
+ this.contextRunner.run(context -> {
+
+ MoonshotChatModel chatModel = context.getBean(MoonshotChatModel.class);
+
+ UserMessage userMessage = new UserMessage(
+ "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius");
+
+ var promptOptions = MoonshotChatOptions.builder()
+ .functionCallbacks(List.of(FunctionCallback.builder()
+ .function("CurrentWeatherService", new MockWeatherService())
+ .description("Get the weather in location")
+ .inputType(MockWeatherService.Request.class)
+ .build()))
+ .build();
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), promptOptions));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+ });
+ }
+
+ @Test
+ void streamingFunctionCallTest() {
+
+ this.contextRunner.run(context -> {
+
+ MoonshotChatModel chatModel = context.getBean(MoonshotChatModel.class);
+
+ UserMessage userMessage = new UserMessage(
+ "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius");
+
+ var promptOptions = MoonshotChatOptions.builder()
+ .functionCallbacks(List.of(FunctionCallback.builder()
+ .function("CurrentWeatherService", new MockWeatherService())
+ .description("Get the weather in location")
+ .inputType(MockWeatherService.Request.class)
+ .build()))
+ .build();
+
+ Flux response = chatModel.stream(new Prompt(List.of(userMessage), promptOptions));
+
+ String content = response.collectList()
+ .block()
+ .stream()
+ .map(ChatResponse::getResults)
+ .flatMap(List::stream)
+ .map(Generation::getOutput)
+ .map(AssistantMessage::getText)
+ .collect(Collectors.joining());
+ logger.info("Response: {}", content);
+
+ assertThat(content).containsAnyOf("30.0", "30");
+ assertThat(content).containsAnyOf("10.0", "10");
+ assertThat(content).containsAnyOf("15.0", "15");
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/tool/FunctionCallbackWithPlainFunctionBeanIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java
similarity index 96%
rename from spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/tool/FunctionCallbackWithPlainFunctionBeanIT.java
rename to spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java
index b199bf035..72b3681c8 100644
--- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/tool/FunctionCallbackWithPlainFunctionBeanIT.java
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package org.springframework.ai.autoconfigure.moonshot.tool;
+package org.springframework.ai.model.moonshot.autoconfigure.tool;
import java.util.List;
import java.util.function.Function;
@@ -26,8 +26,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
-import org.springframework.ai.autoconfigure.moonshot.MoonshotAutoConfiguration;
-import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.model.moonshot.autoconfigure.MoonshotAutoConfiguration;
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/MockWeatherService.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/MockWeatherService.java
new file mode 100644
index 000000000..1fbc6858b
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/MockWeatherService.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.moonshot.autoconfigure.tool;
+
+import java.util.function.Function;
+
+import com.fasterxml.jackson.annotation.JsonClassDescription;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyDescription;
+
+/**
+ * Mock 3rd party weather service.
+ *
+ * @author Geng Rong
+ */
+public class MockWeatherService implements Function {
+
+ @Override
+ public Response apply(Request request) {
+
+ double temperature = 0;
+ if (request.location().contains("Paris")) {
+ temperature = 15;
+ }
+ else if (request.location().contains("Tokyo")) {
+ temperature = 10;
+ }
+ else if (request.location().contains("San Francisco")) {
+ temperature = 30;
+ }
+
+ return new Response(temperature, 15, 20, 2, 53, 45, Unit.C);
+ }
+
+ /**
+ * Temperature units.
+ */
+ public enum Unit {
+
+ /**
+ * Celsius.
+ */
+ C("metric"),
+ /**
+ * Fahrenheit.
+ */
+ F("imperial");
+
+ /**
+ * Human readable unit name.
+ */
+ public final String unitName;
+
+ Unit(String text) {
+ this.unitName = text;
+ }
+
+ }
+
+ /**
+ * Weather Function request.
+ */
+ @JsonInclude(Include.NON_NULL)
+ @JsonClassDescription("Weather API request")
+ public record Request(@JsonProperty(required = true,
+ value = "location") @JsonPropertyDescription("The city and state e.g. San Francisco, CA") String location,
+ @JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) {
+
+ }
+
+ /**
+ * Weather Function response.
+ */
+ public record Response(double temp, double feels_like, double temp_min, double temp_max, int pressure, int humidity,
+ Unit unit) {
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/MoonshotFunctionCallbackIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/MoonshotFunctionCallbackIT.java
new file mode 100644
index 000000000..86a85a9e1
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/tool/MoonshotFunctionCallbackIT.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.moonshot.autoconfigure.tool;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.model.moonshot.autoconfigure.MoonshotAutoConfiguration;
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.model.Generation;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.model.function.FunctionCallback;
+import org.springframework.ai.moonshot.MoonshotChatModel;
+import org.springframework.ai.moonshot.MoonshotChatOptions;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Geng Rong
+ * @author Alexandros Pappas
+ */
+@EnabledIfEnvironmentVariable(named = "MOONSHOT_API_KEY", matches = ".*")
+public class MoonshotFunctionCallbackIT {
+
+ private final Logger logger = LoggerFactory.getLogger(MoonshotFunctionCallbackIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.moonshot.apiKey=" + System.getenv("MOONSHOT_API_KEY"))
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class))
+ .withUserConfiguration(Config.class);
+
+ @Test
+ void functionCallTest() {
+ this.contextRunner.run(context -> {
+
+ MoonshotChatModel chatModel = context.getBean(MoonshotChatModel.class);
+
+ UserMessage userMessage = new UserMessage(
+ "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius");
+
+ ChatResponse response = chatModel
+ .call(new Prompt(List.of(userMessage), MoonshotChatOptions.builder().function("WeatherInfo").build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+
+ });
+ }
+
+ @Test
+ void streamFunctionCallTest() {
+ this.contextRunner.run(context -> {
+
+ MoonshotChatModel chatModel = context.getBean(MoonshotChatModel.class);
+
+ UserMessage userMessage = new UserMessage(
+ "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius");
+
+ Flux response = chatModel.stream(
+ new Prompt(List.of(userMessage), MoonshotChatOptions.builder().function("WeatherInfo").build()));
+
+ String content = response.collectList()
+ .block()
+ .stream()
+ .map(ChatResponse::getResults)
+ .flatMap(List::stream)
+ .map(Generation::getOutput)
+ .map(AssistantMessage::getText)
+ .filter(Objects::nonNull)
+ .collect(Collectors.joining());
+ logger.info("Response: {}", content);
+
+ assertThat(content).containsAnyOf("30.0", "30");
+ assertThat(content).containsAnyOf("10.0", "10");
+ assertThat(content).containsAnyOf("15.0", "15");
+
+ });
+ }
+
+ @Configuration
+ static class Config {
+
+ @Bean
+ public FunctionCallback weatherFunctionInfo() {
+
+ return FunctionCallback.builder()
+ .function("WeatherInfo", new MockWeatherService())
+ .description("Get the weather in location")
+ .inputType(MockWeatherService.Request.class)
+ .build();
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAIAutoConfigurationTest.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAIAutoConfigurationTest.java
new file mode 100644
index 000000000..b8935152e
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAIAutoConfigurationTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.oci.genai.autoconfigure;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+
+import com.oracle.bmc.http.client.pki.Pem;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import org.springframework.ai.oci.cohere.OCICohereChatModel;
+import org.springframework.ai.oci.cohere.OCICohereChatOptions;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class OCIGenAIAutoConfigurationTest {
+
+ @Test
+ void setProperties(@TempDir Path tempDir) throws Exception {
+ Path tmp = tempDir.resolve("my-key.pem");
+ createPrivateKey(tmp);
+ ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.oci.genai.authenticationType=simple",
+ "spring.ai.oci.genai.userId=my-user",
+ "spring.ai.oci.genai.tenantId=my-tenant",
+ "spring.ai.oci.genai.fingerprint=xyz",
+ "spring.ai.oci.genai.privateKey=" + tmp.toAbsolutePath(),
+ "spring.ai.oci.genai.region=us-ashburn-1",
+ "spring.ai.oci.genai.cohere.chat.options.compartment=my-compartment",
+ "spring.ai.oci.genai.cohere.chat.options.servingMode=dedicated",
+ "spring.ai.oci.genai.cohere.chat.options.model=my-model",
+ "spring.ai.oci.genai.cohere.chat.options.maxTokens=1000",
+ "spring.ai.oci.genai.cohere.chat.options.temperature=0.5",
+ "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"
+ // @formatter:on
+ ).withConfiguration(AutoConfigurations.of(OCIGenAiAutoConfiguration.class));
+
+ contextRunner.run(context -> {
+ OCICohereChatModel chatModel = context.getBean(OCICohereChatModel.class);
+ assertThat(chatModel).isNotNull();
+ OCICohereChatOptions options = (OCICohereChatOptions) chatModel.getDefaultOptions();
+ assertThat(options.getCompartment()).isEqualTo("my-compartment");
+ assertThat(options.getModel()).isEqualTo("my-model");
+ assertThat(options.getServingMode()).isEqualTo("dedicated");
+ assertThat(options.getMaxTokens()).isEqualTo(1000);
+ assertThat(options.getTemperature()).isEqualTo(0.5);
+ assertThat(options.getTopP()).isEqualTo(0.8);
+ assertThat(options.getFrequencyPenalty()).isEqualTo(0.1);
+ assertThat(options.getPresencePenalty()).isEqualTo(0.2);
+
+ OCIConnectionProperties props = context.getBean(OCIConnectionProperties.class);
+ assertThat(props.getAuthenticationType()).isEqualTo(OCIConnectionProperties.AuthenticationType.SIMPLE);
+ assertThat(props.getUserId()).isEqualTo("my-user");
+ assertThat(props.getTenantId()).isEqualTo("my-tenant");
+ assertThat(props.getFingerprint()).isEqualTo("xyz");
+ assertThat(props.getPrivateKey()).isEqualTo(tmp.toAbsolutePath().toString());
+ assertThat(props.getRegion()).isEqualTo("us-ashburn-1");
+
+ });
+ }
+
+ private void createPrivateKey(Path tmp) throws Exception {
+ KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
+ gen.initialize(2048);
+ KeyPair keyPair = gen.generateKeyPair();
+ byte[] encoded = Pem.encoder().encode(keyPair.getPrivate());
+ Files.write(tmp, encoded);
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfigurationIT.java
new file mode 100644
index 000000000..610ef9c97
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfigurationIT.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.oci.genai.autoconfigure;
+
+import java.nio.file.Paths;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+
+import org.springframework.ai.embedding.EmbeddingRequest;
+import org.springframework.ai.embedding.EmbeddingResponse;
+import org.springframework.ai.oci.OCIEmbeddingModel;
+import org.springframework.ai.oci.cohere.OCICohereChatModel;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnabledIfEnvironmentVariable(named = OCIGenAiAutoConfigurationIT.COMPARTMENT_ID_KEY, matches = ".+")
+public class OCIGenAiAutoConfigurationIT {
+
+ public static final String COMPARTMENT_ID_KEY = "OCI_COMPARTMENT_ID";
+
+ public static final String OCI_CHAT_MODEL_ID_KEY = "OCI_CHAT_MODEL_ID";
+
+ private final String CONFIG_FILE = Paths.get(System.getProperty("user.home"), ".oci", "config").toString();
+
+ private final String COMPARTMENT_ID = System.getenv(COMPARTMENT_ID_KEY);
+
+ private final String CHAT_MODEL_ID = System.getenv(OCI_CHAT_MODEL_ID_KEY);
+
+ private final ApplicationContextRunner embeddingContextRunner = new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.oci.genai.authenticationType=file",
+ "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"
+ // @formatter:on
+ ).withConfiguration(AutoConfigurations.of(OCIGenAiAutoConfiguration.class));
+
+ private final ApplicationContextRunner cohereChatContextRunner = new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.oci.genai.authenticationType=file",
+ "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
+ // @formatter:on
+ ).withConfiguration(AutoConfigurations.of(OCIGenAiAutoConfiguration.class));
+
+ @Test
+ void embeddings() {
+ this.embeddingContextRunner.run(context -> {
+ OCIEmbeddingModel embeddingModel = context.getBean(OCIEmbeddingModel.class);
+ assertThat(embeddingModel).isNotNull();
+ EmbeddingResponse response = embeddingModel
+ .call(new EmbeddingRequest(List.of("There are 50 states in the USA", "Canada has 10 provinces"), null));
+ assertThat(response).isNotNull();
+ assertThat(response.getResults()).hasSize(2);
+ });
+ }
+
+ @Test
+ @EnabledIfEnvironmentVariable(named = OCIGenAiAutoConfigurationIT.OCI_CHAT_MODEL_ID_KEY, matches = ".+")
+ void cohereChat() {
+ this.cohereChatContextRunner.run(context -> {
+ OCICohereChatModel chatModel = context.getBean(OCICohereChatModel.class);
+ assertThat(chatModel).isNotNull();
+ String response = chatModel.call("How many states are in the United States of America?");
+ assertThat(response).isNotBlank();
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/BaseOllamaIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/BaseOllamaIT.java
new file mode 100644
index 000000000..b57f2ccab
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/BaseOllamaIT.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.ollama.autoconfigure;
+
+import java.time.Duration;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.ollama.OllamaContainer;
+
+import org.springframework.ai.ollama.api.OllamaApi;
+import org.springframework.ai.ollama.management.ModelManagementOptions;
+import org.springframework.ai.ollama.management.OllamaModelManager;
+import org.springframework.ai.ollama.management.PullModelStrategy;
+import org.springframework.util.Assert;
+
+@Testcontainers
+@EnabledIfEnvironmentVariable(named = "OLLAMA_AUTOCONF_TESTS_ENABLED", matches = "true")
+public abstract class BaseOllamaIT {
+
+ static {
+ System.out.println("OLLAMA_AUTOCONF_TESTS_ENABLED=" + System.getenv("OLLAMA_AUTOCONF_TESTS_ENABLED"));
+ System.out.println("System property=" + System.getProperty("OLLAMA_AUTOCONF_TESTS_ENABLED"));
+ }
+ private static final String OLLAMA_LOCAL_URL = "http://localhost:11434";
+
+ private static final Duration DEFAULT_TIMEOUT = Duration.ofMinutes(10);
+
+ private static final int DEFAULT_MAX_RETRIES = 2;
+
+ // Environment variable to control whether to create a new container or use existing
+ // Ollama instance
+ private static final boolean SKIP_CONTAINER_CREATION = Boolean
+ .parseBoolean(System.getenv().getOrDefault("OLLAMA_WITH_REUSE", "false"));
+
+ private static OllamaContainer ollamaContainer;
+
+ private static final ThreadLocal ollamaApi = new ThreadLocal<>();
+
+ /**
+ * Initialize the Ollama API with the specified model. When OLLAMA_WITH_REUSE=true
+ * (default), uses TestContainers withReuse feature. When OLLAMA_WITH_REUSE=false,
+ * connects to local Ollama instance.
+ * @param model the Ollama model to initialize (must not be null or empty)
+ * @return configured OllamaApi instance
+ * @throws IllegalArgumentException if model is null or empty
+ */
+ protected static OllamaApi initializeOllama(final String model) {
+ Assert.hasText(model, "Model name must be provided");
+
+ if (!SKIP_CONTAINER_CREATION) {
+ ollamaContainer = new OllamaContainer(OllamaImage.DEFAULT_IMAGE).withReuse(true);
+ ollamaContainer.start();
+ }
+
+ final OllamaApi api = buildOllamaApiWithModel(model);
+ ollamaApi.set(api);
+ return api;
+ }
+
+ /**
+ * Get the initialized OllamaApi instance.
+ * @return the OllamaApi instance
+ * @throws IllegalStateException if called before initialization
+ */
+ protected static OllamaApi getOllamaApi() {
+ OllamaApi api = ollamaApi.get();
+ Assert.state(api != null, "OllamaApi not initialized. Call initializeOllama first.");
+ return api;
+ }
+
+ @AfterAll
+ public static void tearDown() {
+ if (ollamaContainer != null) {
+ ollamaContainer.stop();
+ }
+ }
+
+ public static OllamaApi buildOllamaApiWithModel(final String model) {
+ final String baseUrl = SKIP_CONTAINER_CREATION ? OLLAMA_LOCAL_URL : ollamaContainer.getEndpoint();
+ final OllamaApi api = new OllamaApi(baseUrl);
+ ensureModelIsPresent(api, model);
+ return api;
+ }
+
+ public String getBaseUrl() {
+ String baseUrl = SKIP_CONTAINER_CREATION ? OLLAMA_LOCAL_URL : ollamaContainer.getEndpoint();
+ return baseUrl;
+ }
+
+ private static void ensureModelIsPresent(final OllamaApi ollamaApi, final String model) {
+ final var modelManagementOptions = ModelManagementOptions.builder()
+ .maxRetries(DEFAULT_MAX_RETRIES)
+ .timeout(DEFAULT_TIMEOUT)
+ .build();
+ final var ollamaModelManager = new OllamaModelManager(ollamaApi, modelManagementOptions);
+ ollamaModelManager.pullModel(model, PullModelStrategy.WHEN_MISSING);
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationIT.java
new file mode 100644
index 000000000..1623d3ed3
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationIT.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.ollama.autoconfigure;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.model.Generation;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.ollama.OllamaChatModel;
+import org.springframework.ai.ollama.api.OllamaApi;
+import org.springframework.ai.ollama.api.OllamaModel;
+import org.springframework.ai.ollama.management.OllamaModelManager;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Christian Tzolov
+ * @author Eddú Meléndez
+ * @author Thomas Vitale
+ * @since 0.8.0
+ */
+public class OllamaChatAutoConfigurationIT extends BaseOllamaIT {
+
+ private static final String MODEL_NAME = OllamaModel.LLAMA3_2.getName();
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.ollama.baseUrl=" + getBaseUrl(),
+ "spring.ai.ollama.chat.options.model=" + MODEL_NAME,
+ "spring.ai.ollama.chat.options.temperature=0.5",
+ "spring.ai.ollama.chat.options.topK=10")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(OllamaAutoConfiguration.class));
+
+ private final UserMessage userMessage = new UserMessage("What's the capital of Denmark?");
+
+ @BeforeAll
+ public static void beforeAll() throws IOException, InterruptedException {
+ initializeOllama(MODEL_NAME);
+ }
+
+ @Test
+ public void chatCompletion() {
+ this.contextRunner.run(context -> {
+ OllamaChatModel chatModel = context.getBean(OllamaChatModel.class);
+ ChatResponse response = chatModel.call(new Prompt(this.userMessage));
+ assertThat(response.getResult().getOutput().getText()).contains("Copenhagen");
+ });
+ }
+
+ @Test
+ public void chatCompletionStreaming() {
+ this.contextRunner.run(context -> {
+
+ OllamaChatModel chatModel = context.getBean(OllamaChatModel.class);
+
+ Flux response = chatModel.stream(new Prompt(this.userMessage));
+
+ List responses = response.collectList().block();
+ assertThat(responses.size()).isGreaterThan(1);
+
+ String stitchedResponseContent = responses.stream()
+ .map(ChatResponse::getResults)
+ .flatMap(List::stream)
+ .map(Generation::getOutput)
+ .map(AssistantMessage::getText)
+ .collect(Collectors.joining());
+
+ assertThat(stitchedResponseContent).contains("Copenhagen");
+ });
+ }
+
+ @Test
+ public void chatCompletionWithPull() {
+ this.contextRunner.withPropertyValues("spring.ai.ollama.init.pull-model-strategy=when_missing")
+ .withPropertyValues("spring.ai.ollama.chat.options.model=tinyllama")
+ .run(context -> {
+ var model = "tinyllama";
+ OllamaApi ollamaApi = context.getBean(OllamaApi.class);
+ var modelManager = new OllamaModelManager(ollamaApi);
+ assertThat(modelManager.isModelAvailable(model)).isTrue();
+
+ OllamaChatModel chatModel = context.getBean(OllamaChatModel.class);
+ ChatResponse response = chatModel.call(new Prompt(this.userMessage));
+ assertThat(response.getResult().getOutput().getText()).contains("Copenhagen");
+ modelManager.deleteModel(model);
+ });
+ }
+
+ @Test
+ void chatActivation() {
+ this.contextRunner.withPropertyValues("spring.ai.ollama.chat.enabled=false").run(context -> {
+ assertThat(context.getBeansOfType(OllamaChatProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OllamaChatModel.class)).isEmpty();
+ });
+
+ this.contextRunner.run(context -> {
+ assertThat(context.getBeansOfType(OllamaChatProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OllamaChatModel.class)).isNotEmpty();
+ });
+
+ this.contextRunner.withPropertyValues("spring.ai.ollama.chat.enabled=true").run(context -> {
+ assertThat(context.getBeansOfType(OllamaChatProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OllamaChatModel.class)).isNotEmpty();
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationTests.java
new file mode 100644
index 000000000..077073ef9
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaChatAutoConfigurationTests.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.ollama.autoconfigure;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Christian Tzolov
+ * @since 0.8.0
+ */
+public class OllamaChatAutoConfigurationTests {
+
+ @Test
+ public void propertiesTest() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.ollama.base-url=TEST_BASE_URL",
+ "spring.ai.ollama.chat.options.model=MODEL_XYZ",
+ "spring.ai.ollama.chat.options.temperature=0.55",
+ "spring.ai.ollama.chat.options.topP=0.56",
+ "spring.ai.ollama.chat.options.topK=123")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class, OllamaAutoConfiguration.class))
+ .run(context -> {
+ var chatProperties = context.getBean(OllamaChatProperties.class);
+ var connectionProperties = context.getBean(OllamaConnectionProperties.class);
+
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(chatProperties.getModel()).isEqualTo("MODEL_XYZ");
+
+ assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55);
+ assertThat(chatProperties.getOptions().getTopP()).isEqualTo(0.56);
+
+ assertThat(chatProperties.getOptions().getTopK()).isEqualTo(123);
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingAutoConfigurationIT.java
new file mode 100644
index 000000000..1a38c32ae
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingAutoConfigurationIT.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.ollama.autoconfigure;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.embedding.EmbeddingResponse;
+import org.springframework.ai.ollama.OllamaEmbeddingModel;
+import org.springframework.ai.ollama.api.OllamaApi;
+import org.springframework.ai.ollama.api.OllamaModel;
+import org.springframework.ai.ollama.management.OllamaModelManager;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Christian Tzolov
+ * @author Thomas Vitale
+ * @since 1.0.0
+ */
+public class OllamaEmbeddingAutoConfigurationIT extends BaseOllamaIT {
+
+ private static final String MODEL_NAME = OllamaModel.NOMIC_EMBED_TEXT.getName();
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.ollama.embedding.options.model=" + MODEL_NAME,
+ "spring.ai.ollama.base-url=" + getBaseUrl())
+ .withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class, OllamaAutoConfiguration.class));
+
+ @BeforeAll
+ public static void beforeAll() throws IOException, InterruptedException {
+ initializeOllama(MODEL_NAME);
+ }
+
+ @Test
+ public void singleTextEmbedding() {
+ this.contextRunner.run(context -> {
+ OllamaEmbeddingModel embeddingModel = context.getBean(OllamaEmbeddingModel.class);
+ assertThat(embeddingModel).isNotNull();
+ EmbeddingResponse embeddingResponse = embeddingModel.embedForResponse(List.of("Hello World"));
+ assertThat(embeddingResponse.getResults()).hasSize(1);
+ assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty();
+ assertThat(embeddingModel.dimensions()).isEqualTo(768);
+ });
+ }
+
+ @Test
+ public void embeddingWithPull() {
+ this.contextRunner.withPropertyValues("spring.ai.ollama.init.pull-model-strategy=when_missing")
+ .withPropertyValues("spring.ai.ollama.embedding.options.model=all-minilm")
+ .run(context -> {
+ var model = "all-minilm";
+ OllamaApi ollamaApi = context.getBean(OllamaApi.class);
+ var modelManager = new OllamaModelManager(ollamaApi);
+ assertThat(modelManager.isModelAvailable(model)).isTrue();
+
+ OllamaEmbeddingModel embeddingModel = context.getBean(OllamaEmbeddingModel.class);
+ EmbeddingResponse embeddingResponse = embeddingModel.embedForResponse(List.of("Hello World"));
+ assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty();
+ modelManager.deleteModel(model);
+ });
+ }
+
+ @Test
+ void embeddingActivation() {
+ this.contextRunner.withPropertyValues("spring.ai.ollama.embedding.enabled=false").run(context -> {
+ assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isEmpty();
+ });
+
+ this.contextRunner.run(context -> {
+ assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isNotEmpty();
+ });
+
+ this.contextRunner.withPropertyValues("spring.ai.ollama.embedding.enabled=true").run(context -> {
+ assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isNotEmpty();
+ });
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingAutoConfigurationTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingAutoConfigurationTests.java
new file mode 100644
index 000000000..b29d88f8b
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaEmbeddingAutoConfigurationTests.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.ollama.autoconfigure;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Christian Tzolov
+ * @since 0.8.0
+ */
+public class OllamaEmbeddingAutoConfigurationTests {
+
+ @Test
+ public void propertiesTest() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.ollama.base-url=TEST_BASE_URL",
+ "spring.ai.ollama.embedding.options.model=MODEL_XYZ",
+ "spring.ai.ollama.embedding.options.temperature=0.13",
+ "spring.ai.ollama.embedding.options.topK=13"
+ // @formatter:on
+ )
+ .withConfiguration(AutoConfigurations.of(RestClientAutoConfiguration.class, OllamaAutoConfiguration.class))
+ .run(context -> {
+ var embeddingProperties = context.getBean(OllamaEmbeddingProperties.class);
+ var connectionProperties = context.getBean(OllamaConnectionProperties.class);
+
+ assertThat(embeddingProperties.getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+ assertThat(embeddingProperties.getOptions().toMap()).containsKeys("temperature");
+ assertThat(embeddingProperties.getOptions().toMap().get("temperature")).isEqualTo(0.13);
+ assertThat(embeddingProperties.getOptions().getTopK()).isEqualTo(13);
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaImage.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaImage.java
new file mode 100644
index 000000000..5bb7547d0
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaImage.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.ollama.autoconfigure;
+
+public final class OllamaImage {
+
+ public static final String DEFAULT_IMAGE = "ollama/ollama:0.5.7";
+
+ private OllamaImage() {
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackInPromptIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackInPromptIT.java
new file mode 100644
index 000000000..875a90742
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackInPromptIT.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.ollama.autoconfigure.tool;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.model.ollama.autoconfigure.BaseOllamaIT;
+import org.springframework.ai.model.ollama.autoconfigure.OllamaAutoConfiguration;
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.model.Generation;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.ollama.OllamaChatModel;
+import org.springframework.ai.ollama.api.OllamaOptions;
+import org.springframework.ai.tool.function.FunctionToolCallback;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class FunctionCallbackInPromptIT extends BaseOllamaIT {
+
+ private static final Logger logger = LoggerFactory.getLogger(FunctionCallbackInPromptIT.class);
+
+ private static final String MODEL_NAME = "qwen2.5:3b";
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.ollama.baseUrl=" + getBaseUrl(),
+ "spring.ai.ollama.chat.options.model=" + MODEL_NAME,
+ "spring.ai.ollama.chat.options.temperature=0.5",
+ "spring.ai.ollama.chat.options.topK=10")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(OllamaAutoConfiguration.class));
+
+ @BeforeAll
+ public static void beforeAll() {
+ initializeOllama(MODEL_NAME);
+ }
+
+ @Test
+ void functionCallTest() {
+ this.contextRunner.run(context -> {
+
+ OllamaChatModel chatModel = context.getBean(OllamaChatModel.class);
+
+ UserMessage userMessage = new UserMessage(
+ "What are the weather conditions in San Francisco, Tokyo, and Paris? Find the temperature in Celsius for each of the three locations.");
+
+ var promptOptions = OllamaOptions.builder()
+ .functionCallbacks(List.of(FunctionToolCallback
+ .builder("CurrentWeatherService", new MockWeatherService())
+ .description(
+ "Find the weather conditions, forecasts, and temperatures for a location, like a city or state.")
+ .inputType(MockWeatherService.Request.class)
+ .build()))
+ .build();
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), promptOptions));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+ });
+ }
+
+ @Test
+ void streamingFunctionCallTest() {
+ this.contextRunner.run(context -> {
+
+ OllamaChatModel chatModel = context.getBean(OllamaChatModel.class);
+
+ UserMessage userMessage = new UserMessage(
+ "What are the weather conditions in San Francisco, Tokyo, and Paris? Find the temperature in Celsius for each of the three locations.");
+
+ var promptOptions = OllamaOptions.builder()
+ .functionCallbacks(List.of(FunctionToolCallback
+ .builder("CurrentWeatherService", new MockWeatherService())
+ .description(
+ "Find the weather conditions, forecasts, and temperatures for a location, like a city or state.")
+ .inputType(MockWeatherService.Request.class)
+ .build()))
+ .build();
+
+ Flux response = chatModel.stream(new Prompt(List.of(userMessage), promptOptions));
+
+ String content = response.collectList()
+ .block()
+ .stream()
+ .map(ChatResponse::getResults)
+ .flatMap(List::stream)
+ .map(Generation::getOutput)
+ .map(AssistantMessage::getText)
+ .collect(Collectors.joining());
+ logger.info("Response: {}", content);
+
+ assertThat(content).containsAnyOf("30.0", "30");
+ assertThat(content).containsAnyOf("10.0", "10");
+ assertThat(content).containsAnyOf("15.0", "15");
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/MockWeatherService.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/MockWeatherService.java
new file mode 100644
index 000000000..6995eeed4
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/MockWeatherService.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.ollama.autoconfigure.tool;
+
+import java.util.function.Function;
+
+import com.fasterxml.jackson.annotation.JsonClassDescription;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyDescription;
+
+/**
+ * Mock 3rd party weather service.
+ *
+ * @author Christian Tzolov
+ */
+public class MockWeatherService implements Function {
+
+ @Override
+ public Response apply(Request request) {
+
+ double temperature = 10;
+ if (request.location().contains("Paris")) {
+ temperature = 15;
+ }
+ else if (request.location().contains("Tokyo")) {
+ temperature = 10;
+ }
+ else if (request.location().contains("San Francisco")) {
+ temperature = 30;
+ }
+
+ return new Response(temperature, 15, 20, 2, 53, 45, Unit.C);
+ }
+
+ /**
+ * Temperature units.
+ */
+ public enum Unit {
+
+ /**
+ * Celsius.
+ */
+ C("metric"),
+ /**
+ * Fahrenheit.
+ */
+ F("imperial");
+
+ /**
+ * Human readable unit name.
+ */
+ public final String unitName;
+
+ Unit(String text) {
+ this.unitName = text;
+ }
+
+ }
+
+ /**
+ * Weather Function request.
+ */
+ @JsonInclude(Include.NON_NULL)
+ @JsonClassDescription("Weather API request")
+ public record Request(@JsonProperty(required = true,
+ value = "location") @JsonPropertyDescription("The city and state e.g. San Francisco, CA") String location,
+ @JsonProperty(required = true, value = "lat") @JsonPropertyDescription("The city latitude") double lat,
+ @JsonProperty(required = true, value = "lon") @JsonPropertyDescription("The city longitude") double lon,
+ @JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) {
+
+ }
+
+ /**
+ * Weather Function response.
+ */
+ public record Response(double temp, double feels_like, double temp_min, double temp_max, int pressure, int humidity,
+ Unit unit) {
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/OllamaFunctionCallbackIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/OllamaFunctionCallbackIT.java
new file mode 100644
index 000000000..48d32380d
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/OllamaFunctionCallbackIT.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2023-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.ollama.autoconfigure.tool;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.model.ollama.autoconfigure.BaseOllamaIT;
+import org.springframework.ai.model.ollama.autoconfigure.OllamaAutoConfiguration;
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.model.Generation;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.model.tool.ToolCallingChatOptions;
+import org.springframework.ai.ollama.OllamaChatModel;
+import org.springframework.ai.ollama.api.OllamaOptions;
+import org.springframework.ai.tool.ToolCallback;
+import org.springframework.ai.tool.function.FunctionToolCallback;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class OllamaFunctionCallbackIT extends BaseOllamaIT {
+
+ private static final Logger logger = LoggerFactory.getLogger(OllamaFunctionCallbackIT.class);
+
+ private static final String MODEL_NAME = "qwen2.5:3b";
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.ollama.baseUrl=" + getBaseUrl(),
+ "spring.ai.ollama.chat.options.model=" + MODEL_NAME,
+ "spring.ai.ollama.chat.options.temperature=0.5",
+ "spring.ai.ollama.chat.options.topK=10")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(OllamaAutoConfiguration.class))
+ .withUserConfiguration(Config.class);
+
+ @BeforeAll
+ public static void beforeAll() {
+ initializeOllama(MODEL_NAME);
+ }
+
+ @Test
+ void functionCallTest() {
+ this.contextRunner.run(context -> {
+
+ OllamaChatModel chatModel = context.getBean(OllamaChatModel.class);
+
+ UserMessage userMessage = new UserMessage(
+ "What are the weather conditions in San Francisco, Tokyo, and Paris? Find the temperature in Celsius for each of the three locations.");
+
+ ChatResponse response = chatModel
+ .call(new Prompt(List.of(userMessage), OllamaOptions.builder().function("WeatherInfo").build()));
+
+ logger.info("Response: " + response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+ });
+ }
+
+ @Test
+ void streamFunctionCallTest() {
+ this.contextRunner.run(context -> {
+
+ OllamaChatModel chatModel = context.getBean(OllamaChatModel.class);
+
+ UserMessage userMessage = new UserMessage(
+ "What are the weather conditions in San Francisco, Tokyo, and Paris? Find the temperature in Celsius for each of the three locations.");
+
+ Flux response = chatModel
+ .stream(new Prompt(List.of(userMessage), OllamaOptions.builder().function("WeatherInfo").build()));
+
+ String content = response.collectList()
+ .block()
+ .stream()
+ .map(ChatResponse::getResults)
+ .flatMap(List::stream)
+ .map(Generation::getOutput)
+ .map(AssistantMessage::getText)
+ .collect(Collectors.joining());
+ logger.info("Response: " + content);
+
+ assertThat(content).contains("30", "10", "15");
+ });
+ }
+
+ @Test
+ void functionCallWithPortableFunctionCallingOptions() {
+ this.contextRunner.run(context -> {
+
+ OllamaChatModel chatModel = context.getBean(OllamaChatModel.class);
+
+ // Test weatherFunction
+ UserMessage userMessage = new UserMessage(
+ "What are the weather conditions in San Francisco, Tokyo, and Paris? Find the temperature in Celsius for each of the three locations.");
+
+ ToolCallingChatOptions functionOptions = ToolCallingChatOptions.builder().toolNames("WeatherInfo").build();
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), functionOptions));
+
+ logger.info("Response: " + response.getResult().getOutput().getText());
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+ });
+ }
+
+ @Configuration
+ static class Config {
+
+ @Bean
+ public ToolCallback weatherFunctionInfo() {
+
+ return FunctionToolCallback.builder("WeatherInfo", new MockWeatherService())
+ .description(
+ "Find the weather conditions, forecasts, and temperatures for a location, like a city or state.")
+ .inputType(MockWeatherService.Request.class)
+ .build();
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/OllamaFunctionToolBeanIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/OllamaFunctionToolBeanIT.java
new file mode 100644
index 000000000..947192863
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/ollama/autoconfigure/tool/OllamaFunctionToolBeanIT.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2023-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.ollama.autoconfigure.tool;
+
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.model.ollama.autoconfigure.BaseOllamaIT;
+import org.springframework.ai.model.ollama.autoconfigure.OllamaAutoConfiguration;
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.model.Generation;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.model.tool.ToolCallingChatOptions;
+import org.springframework.ai.ollama.OllamaChatModel;
+import org.springframework.ai.ollama.api.OllamaOptions;
+import org.springframework.ai.tool.ToolCallbacks;
+import org.springframework.ai.tool.annotation.Tool;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Description;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Integration tests for function-based tool calling in Ollama.
+ *
+ * @author Thomas Vitale
+ */
+public class OllamaFunctionToolBeanIT extends BaseOllamaIT {
+
+ private static final Logger logger = LoggerFactory.getLogger(OllamaFunctionToolBeanIT.class);
+
+ private static final String MODEL_NAME = "qwen2.5:3b";
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.ollama.baseUrl=" + getBaseUrl(),
+ "spring.ai.ollama.chat.options.model=" + MODEL_NAME,
+ "spring.ai.ollama.chat.options.temperature=0.5",
+ "spring.ai.ollama.chat.options.topK=10")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(OllamaAutoConfiguration.class))
+ .withUserConfiguration(Config.class);
+
+ @BeforeAll
+ public static void beforeAll() {
+ initializeOllama(MODEL_NAME);
+ }
+
+ @Test
+ void toolCallTest() {
+ this.contextRunner.run(context -> {
+
+ OllamaChatModel chatModel = context.getBean(OllamaChatModel.class);
+
+ MyTools myTools = context.getBean(MyTools.class);
+
+ UserMessage userMessage = new UserMessage(
+ "What are the weather conditions in San Francisco, Tokyo, and Paris? Find the temperature in Celsius for each of the three locations.");
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
+ OllamaOptions.builder().toolCallbacks(ToolCallbacks.from(myTools)).build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+ });
+
+ }
+
+ @Test
+ void functionCallTest() {
+ this.contextRunner.run(context -> {
+
+ OllamaChatModel chatModel = context.getBean(OllamaChatModel.class);
+
+ UserMessage userMessage = new UserMessage(
+ "What are the weather conditions in San Francisco, Tokyo, and Paris? Find the temperature in Celsius for each of the three locations.");
+
+ ChatResponse response = chatModel
+ .call(new Prompt(List.of(userMessage), OllamaOptions.builder().toolNames("weatherInfo").build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+ });
+ }
+
+ @Test
+ void streamFunctionCallTest() {
+ this.contextRunner.run(context -> {
+
+ OllamaChatModel chatModel = context.getBean(OllamaChatModel.class);
+
+ UserMessage userMessage = new UserMessage(
+ "What are the weather conditions in San Francisco, Tokyo, and Paris? Find the temperature in Celsius for each of the three locations.");
+
+ Flux response = chatModel
+ .stream(new Prompt(List.of(userMessage), OllamaOptions.builder().function("weatherInfo").build()));
+
+ String content = response.collectList()
+ .block()
+ .stream()
+ .map(ChatResponse::getResults)
+ .flatMap(List::stream)
+ .map(Generation::getOutput)
+ .map(AssistantMessage::getText)
+ .collect(Collectors.joining());
+ logger.info("Response: {}", content);
+
+ assertThat(content).contains("30", "10", "15");
+ });
+ }
+
+ @Test
+ void functionCallWithPortableFunctionCallingOptions() {
+ this.contextRunner.run(context -> {
+
+ OllamaChatModel chatModel = context.getBean(OllamaChatModel.class);
+
+ // Test weatherFunction
+ UserMessage userMessage = new UserMessage(
+ "What are the weather conditions in San Francisco, Tokyo, and Paris? Find the temperature in Celsius for each of the three locations.");
+
+ ToolCallingChatOptions functionOptions = ToolCallingChatOptions.builder().toolNames("weatherInfo").build();
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), functionOptions));
+
+ logger.info("Response: {}", response.getResult().getOutput().getText());
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+ });
+ }
+
+ static class MyTools {
+
+ @Tool(description = "Find the weather conditions, and temperatures for a location, like a city or state.")
+ public String weatherByLocation(String locationName) {
+ int temperature = 0;
+ if (locationName.equals("San Francisco")) {
+ temperature = 30;
+ }
+ else if (locationName.equals("Tokyo")) {
+ temperature = 10;
+ }
+ else if (locationName.equals("Paris")) {
+ temperature = 15;
+ }
+ return "The temperature in " + locationName + " is " + temperature + " degrees Celsius.";
+ }
+
+ }
+
+ @Configuration
+ static class Config {
+
+ @Bean
+ @Description("Find the weather conditions, forecasts, and temperatures for a location, like a city or state.")
+ public Function weatherInfo() {
+ return new MockWeatherService();
+ }
+
+ @Bean
+ public MyTools myTools() {
+ return new MyTools();
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAutoConfigurationIT.java
new file mode 100644
index 000000000..e384541c0
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAutoConfigurationIT.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.openai.autoconfigure;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.metadata.Usage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.embedding.EmbeddingResponse;
+import org.springframework.ai.image.ImagePrompt;
+import org.springframework.ai.image.ImageResponse;
+import org.springframework.ai.openai.OpenAiAudioSpeechModel;
+import org.springframework.ai.openai.OpenAiAudioTranscriptionModel;
+import org.springframework.ai.openai.OpenAiChatModel;
+import org.springframework.ai.openai.OpenAiEmbeddingModel;
+import org.springframework.ai.openai.OpenAiImageModel;
+import org.springframework.ai.openai.api.OpenAiApi;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*")
+public class OpenAiAutoConfigurationIT {
+
+ private static final Log logger = LogFactory.getLog(OpenAiAutoConfigurationIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY"))
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class));
+
+ @Test
+ void chatCall() {
+ this.contextRunner.run(context -> {
+ OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class);
+ String response = chatModel.call("Hello");
+ assertThat(response).isNotEmpty();
+ logger.info("Response: " + response);
+ });
+ }
+
+ @Test
+ void chatCallAudioResponse() {
+ this.contextRunner
+ .withPropertyValues(
+ "spring.ai.openai.chat.options.model=" + OpenAiApi.ChatModel.GPT_4_O_AUDIO_PREVIEW.getValue(),
+ "spring.ai.openai.chat.options.output-modalities=text,audio",
+ "spring.ai.openai.chat.options.output-audio.voice=ALLOY",
+ "spring.ai.openai.chat.options.output-audio.format=WAV")
+ .run(context -> {
+ OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class);
+
+ ChatResponse response = chatModel
+ .call(new Prompt(new UserMessage("Tell me joke about Spring Framework")));
+ assertThat(response).isNotNull();
+ logger.info("Response: " + response);
+ // AudioPlayer.play(response.getResult().getOutput().getMedia().get(0).getDataAsByteArray());
+ });
+ }
+
+ @Test
+ void transcribe() {
+ this.contextRunner.run(context -> {
+ OpenAiAudioTranscriptionModel transcriptionModel = context.getBean(OpenAiAudioTranscriptionModel.class);
+ Resource audioFile = new ClassPathResource("/speech/jfk.flac");
+ String response = transcriptionModel.call(audioFile);
+ assertThat(response).isNotEmpty();
+ logger.info("Response: " + response);
+ });
+ }
+
+ @Test
+ void speech() {
+ this.contextRunner.run(context -> {
+ OpenAiAudioSpeechModel speechModel = context.getBean(OpenAiAudioSpeechModel.class);
+ byte[] response = speechModel.call("H");
+ assertThat(response).isNotNull();
+ assertThat(verifyMp3FrameHeader(response))
+ .withFailMessage("Expected MP3 frame header to be present in the response, but it was not found.")
+ .isTrue();
+ assertThat(response.length).isNotEqualTo(0);
+
+ logger.debug("Response: " + Arrays.toString(response));
+ });
+ }
+
+ public boolean verifyMp3FrameHeader(byte[] audioResponse) {
+ // Check if the response is null or too short to contain a frame header
+ if (audioResponse == null || audioResponse.length < 2) {
+ return false;
+ }
+ // Check for the MP3 frame header
+ // 0xFFE0 is the sync word for an MP3 frame (11 bits set to 1 followed by 3 bits
+ // set to 0)
+ return (audioResponse[0] & 0xFF) == 0xFF && (audioResponse[1] & 0xE0) == 0xE0;
+ }
+
+ @Test
+ void generateStreaming() {
+ this.contextRunner.run(context -> {
+ OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class);
+ Flux responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello")));
+ String response = responseFlux.collectList()
+ .block()
+ .stream()
+ .map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText())
+ .collect(Collectors.joining());
+
+ assertThat(response).isNotEmpty();
+ logger.info("Response: " + response);
+ });
+ }
+
+ @Test
+ void streamingWithTokenUsage() {
+ this.contextRunner.withPropertyValues("spring.ai.openai.chat.options.stream-usage=true").run(context -> {
+ OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class);
+
+ Flux responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello")));
+
+ Usage[] streamingTokenUsage = new Usage[1];
+ String response = responseFlux.collectList().block().stream().map(chatResponse -> {
+ streamingTokenUsage[0] = chatResponse.getMetadata().getUsage();
+ return (chatResponse.getResult() != null) ? chatResponse.getResult().getOutput().getText() : "";
+ }).collect(Collectors.joining());
+
+ assertThat(streamingTokenUsage[0].getPromptTokens()).isGreaterThan(0);
+ assertThat(streamingTokenUsage[0].getCompletionTokens()).isGreaterThan(0);
+ assertThat(streamingTokenUsage[0].getTotalTokens()).isGreaterThan(0);
+
+ assertThat(response).isNotEmpty();
+ logger.info("Response: " + response);
+ });
+ }
+
+ @Test
+ void embedding() {
+ this.contextRunner.run(context -> {
+ OpenAiEmbeddingModel embeddingModel = context.getBean(OpenAiEmbeddingModel.class);
+
+ EmbeddingResponse embeddingResponse = embeddingModel
+ .embedForResponse(List.of("Hello World", "World is big and salvation is near"));
+ assertThat(embeddingResponse.getResults()).hasSize(2);
+ assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty();
+ assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0);
+ assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty();
+ assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1);
+
+ assertThat(embeddingModel.dimensions()).isEqualTo(1536);
+ });
+ }
+
+ @Test
+ void generateImage() {
+ this.contextRunner.withPropertyValues("spring.ai.openai.image.options.size=1024x1024").run(context -> {
+ OpenAiImageModel imageModel = context.getBean(OpenAiImageModel.class);
+ ImageResponse imageResponse = imageModel.call(new ImagePrompt("forest"));
+ assertThat(imageResponse.getResults()).hasSize(1);
+ assertThat(imageResponse.getResult().getOutput().getUrl()).isNotEmpty();
+ logger.info("Generated image: " + imageResponse.getResult().getOutput().getUrl());
+ });
+ }
+
+ @Test
+ void generateImageWithModel() {
+ // The 256x256 size is supported by dall-e-2, but not by dall-e-3.
+ this.contextRunner
+ .withPropertyValues("spring.ai.openai.image.options.model=dall-e-2",
+ "spring.ai.openai.image.options.size=256x256")
+ .run(context -> {
+ OpenAiImageModel imageModel = context.getBean(OpenAiImageModel.class);
+ ImageResponse imageResponse = imageModel.call(new ImagePrompt("forest"));
+ assertThat(imageResponse.getResults()).hasSize(1);
+ assertThat(imageResponse.getResult().getOutput().getUrl()).isNotEmpty();
+ logger.info("Generated image: " + imageResponse.getResult().getOutput().getUrl());
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiPropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiPropertiesTests.java
new file mode 100644
index 000000000..bbff01e58
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiPropertiesTests.java
@@ -0,0 +1,687 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.openai.autoconfigure;
+
+import org.junit.jupiter.api.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.skyscreamer.jsonassert.JSONCompareMode;
+
+import org.springframework.ai.model.ModelOptionsUtils;
+import org.springframework.ai.openai.OpenAiAudioSpeechModel;
+import org.springframework.ai.openai.OpenAiAudioTranscriptionModel;
+import org.springframework.ai.openai.OpenAiChatModel;
+import org.springframework.ai.openai.OpenAiEmbeddingModel;
+import org.springframework.ai.openai.OpenAiImageModel;
+import org.springframework.ai.openai.api.OpenAiApi;
+import org.springframework.ai.openai.api.OpenAiApi.ChatCompletionRequest.ToolChoiceBuilder;
+import org.springframework.ai.openai.api.OpenAiAudioApi;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit Tests for {@link OpenAiConnectionProperties}, {@link OpenAiChatProperties} and
+ * {@link OpenAiEmbeddingProperties}.
+ *
+ * @author Christian Tzolov
+ * @author Thomas Vitale
+ * @since 0.8.0
+ */
+public class OpenAiPropertiesTests {
+
+ @Test
+ public void chatProperties() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.api-key=abc123",
+ "spring.ai.openai.chat.options.model=MODEL_XYZ",
+ "spring.ai.openai.chat.options.temperature=0.55")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ var chatProperties = context.getBean(OpenAiChatProperties.class);
+ var connectionProperties = context.getBean(OpenAiConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(chatProperties.getApiKey()).isNull();
+ assertThat(chatProperties.getBaseUrl()).isNull();
+
+ assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55);
+ });
+ }
+
+ @Test
+ public void transcriptionProperties() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.api-key=abc123",
+ "spring.ai.openai.audio.transcription.options.model=MODEL_XYZ",
+ "spring.ai.openai.audio.transcription.options.temperature=0.55")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ var transcriptionProperties = context.getBean(OpenAiAudioTranscriptionProperties.class);
+ var connectionProperties = context.getBean(OpenAiConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(transcriptionProperties.getApiKey()).isNull();
+ assertThat(transcriptionProperties.getBaseUrl()).isNull();
+
+ assertThat(transcriptionProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(transcriptionProperties.getOptions().getTemperature()).isEqualTo(0.55f);
+ });
+ }
+
+ @Test
+ public void chatOverrideConnectionProperties() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.api-key=abc123",
+ "spring.ai.openai.chat.base-url=TEST_BASE_URL2",
+ "spring.ai.openai.chat.api-key=456",
+ "spring.ai.openai.chat.options.model=MODEL_XYZ",
+ "spring.ai.openai.chat.options.temperature=0.55")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ var chatProperties = context.getBean(OpenAiChatProperties.class);
+ var connectionProperties = context.getBean(OpenAiConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(chatProperties.getApiKey()).isEqualTo("456");
+ assertThat(chatProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2");
+
+ assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55);
+ });
+ }
+
+ @Test
+ public void transcriptionOverrideConnectionProperties() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.api-key=abc123",
+ "spring.ai.openai.audio.transcription.base-url=TEST_BASE_URL2",
+ "spring.ai.openai.audio.transcription.api-key=456",
+ "spring.ai.openai.audio.transcription.options.model=MODEL_XYZ",
+ "spring.ai.openai.audio.transcription.options.temperature=0.55")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ var transcriptionProperties = context.getBean(OpenAiAudioTranscriptionProperties.class);
+ var connectionProperties = context.getBean(OpenAiConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(transcriptionProperties.getApiKey()).isEqualTo("456");
+ assertThat(transcriptionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2");
+
+ assertThat(transcriptionProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(transcriptionProperties.getOptions().getTemperature()).isEqualTo(0.55f);
+ });
+ }
+
+ @Test
+ public void speechProperties() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.api-key=abc123",
+ "spring.ai.openai.audio.speech.options.model=TTS_1",
+ "spring.ai.openai.audio.speech.options.voice=alloy",
+ "spring.ai.openai.audio.speech.options.response-format=mp3",
+ "spring.ai.openai.audio.speech.options.speed=0.75")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ var speechProperties = context.getBean(OpenAiAudioSpeechProperties.class);
+ var connectionProperties = context.getBean(OpenAiConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(speechProperties.getApiKey()).isNull();
+ assertThat(speechProperties.getBaseUrl()).isNull();
+
+ assertThat(speechProperties.getOptions().getModel()).isEqualTo("TTS_1");
+ assertThat(speechProperties.getOptions().getVoice())
+ .isEqualTo(OpenAiAudioApi.SpeechRequest.Voice.ALLOY);
+ assertThat(speechProperties.getOptions().getResponseFormat())
+ .isEqualTo(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.MP3);
+ assertThat(speechProperties.getOptions().getSpeed()).isEqualTo(0.75f);
+ });
+ }
+
+ @Test
+ public void speechPropertiesTest() {
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.api-key=abc123",
+ "spring.ai.openai.audio.speech.options.model=TTS_1",
+ "spring.ai.openai.audio.speech.options.voice=alloy",
+ "spring.ai.openai.audio.speech.options.response-format=mp3",
+ "spring.ai.openai.audio.speech.options.speed=0.75")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ var speechProperties = context.getBean(OpenAiAudioSpeechProperties.class);
+ var connectionProperties = context.getBean(OpenAiConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(speechProperties.getOptions().getModel()).isEqualTo("TTS_1");
+ assertThat(speechProperties.getOptions().getVoice())
+ .isEqualTo(OpenAiAudioApi.SpeechRequest.Voice.ALLOY);
+ assertThat(speechProperties.getOptions().getResponseFormat())
+ .isEqualTo(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.MP3);
+ assertThat(speechProperties.getOptions().getSpeed()).isEqualTo(0.75f);
+ });
+ }
+
+ @Test
+ public void speechOverrideConnectionPropertiesTest() {
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.api-key=abc123",
+ "spring.ai.openai.audio.speech.base-url=TEST_BASE_URL2",
+ "spring.ai.openai.audio.speech.api-key=456",
+ "spring.ai.openai.audio.speech.options.model=TTS_2",
+ "spring.ai.openai.audio.speech.options.voice=echo",
+ "spring.ai.openai.audio.speech.options.response-format=opus",
+ "spring.ai.openai.audio.speech.options.speed=0.5")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ var speechProperties = context.getBean(OpenAiAudioSpeechProperties.class);
+ var connectionProperties = context.getBean(OpenAiConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(speechProperties.getApiKey()).isEqualTo("456");
+ assertThat(speechProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2");
+
+ assertThat(speechProperties.getOptions().getModel()).isEqualTo("TTS_2");
+ assertThat(speechProperties.getOptions().getVoice()).isEqualTo(OpenAiAudioApi.SpeechRequest.Voice.ECHO);
+ assertThat(speechProperties.getOptions().getResponseFormat())
+ .isEqualTo(OpenAiAudioApi.SpeechRequest.AudioResponseFormat.OPUS);
+ assertThat(speechProperties.getOptions().getSpeed()).isEqualTo(0.5f);
+ });
+ }
+
+ @Test
+ public void embeddingProperties() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.api-key=abc123",
+ "spring.ai.openai.embedding.options.model=MODEL_XYZ")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ var embeddingProperties = context.getBean(OpenAiEmbeddingProperties.class);
+ var connectionProperties = context.getBean(OpenAiConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(embeddingProperties.getApiKey()).isNull();
+ assertThat(embeddingProperties.getBaseUrl()).isNull();
+
+ assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ });
+ }
+
+ @Test
+ public void embeddingOverrideConnectionProperties() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.api-key=abc123",
+ "spring.ai.openai.embedding.base-url=TEST_BASE_URL2",
+ "spring.ai.openai.embedding.api-key=456",
+ "spring.ai.openai.embedding.options.model=MODEL_XYZ")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ var embeddingProperties = context.getBean(OpenAiEmbeddingProperties.class);
+ var connectionProperties = context.getBean(OpenAiConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(embeddingProperties.getApiKey()).isEqualTo("456");
+ assertThat(embeddingProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2");
+
+ assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ });
+ }
+
+ @Test
+ public void imageProperties() {
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.api-key=abc123",
+ "spring.ai.openai.image.options.model=MODEL_XYZ",
+ "spring.ai.openai.image.options.n=3")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ var imageProperties = context.getBean(OpenAiImageProperties.class);
+ var connectionProperties = context.getBean(OpenAiConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(imageProperties.getApiKey()).isNull();
+ assertThat(imageProperties.getBaseUrl()).isNull();
+
+ assertThat(imageProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(imageProperties.getOptions().getN()).isEqualTo(3);
+ });
+ }
+
+ @Test
+ public void imageOverrideConnectionProperties() {
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.api-key=abc123",
+ "spring.ai.openai.image.base-url=TEST_BASE_URL2",
+ "spring.ai.openai.image.api-key=456",
+ "spring.ai.openai.image.options.model=MODEL_XYZ",
+ "spring.ai.openai.image.options.n=3")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ var imageProperties = context.getBean(OpenAiImageProperties.class);
+ var connectionProperties = context.getBean(OpenAiConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(imageProperties.getApiKey()).isEqualTo("456");
+ assertThat(imageProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2");
+
+ assertThat(imageProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(imageProperties.getOptions().getN()).isEqualTo(3);
+ });
+ }
+
+ @Test
+ public void chatOptionsTest() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.openai.api-key=API_KEY",
+ "spring.ai.openai.base-url=TEST_BASE_URL",
+
+ "spring.ai.openai.chat.options.model=MODEL_XYZ",
+ "spring.ai.openai.chat.options.frequencyPenalty=-1.5",
+ "spring.ai.openai.chat.options.logitBias.myTokenId=-5",
+ "spring.ai.openai.chat.options.maxTokens=123",
+ "spring.ai.openai.chat.options.n=10",
+ "spring.ai.openai.chat.options.presencePenalty=0",
+ "spring.ai.openai.chat.options.seed=66",
+ "spring.ai.openai.chat.options.stop=boza,koza",
+ "spring.ai.openai.chat.options.temperature=0.55",
+ "spring.ai.openai.chat.options.topP=0.56",
+
+ // "spring.ai.openai.chat.options.toolChoice.functionName=toolChoiceFunctionName",
+ "spring.ai.openai.chat.options.toolChoice=" + ModelOptionsUtils.toJsonString(ToolChoiceBuilder.FUNCTION("toolChoiceFunctionName")),
+
+ "spring.ai.openai.chat.options.tools[0].function.name=myFunction1",
+ "spring.ai.openai.chat.options.tools[0].function.description=function description",
+ "spring.ai.openai.chat.options.tools[0].function.jsonSchema=" + """
+ {
+ "type": "object",
+ "properties": {
+ "location": {
+ "type": "string",
+ "description": "The city and state e.g. San Francisco, CA"
+ },
+ "lat": {
+ "type": "number",
+ "description": "The city latitude"
+ },
+ "lon": {
+ "type": "number",
+ "description": "The city longitude"
+ },
+ "unit": {
+ "type": "string",
+ "enum": ["c", "f"]
+ }
+ },
+ "required": ["location", "lat", "lon", "unit"]
+ }
+ """,
+ "spring.ai.openai.chat.options.user=userXYZ"
+ )
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ var chatProperties = context.getBean(OpenAiChatProperties.class);
+ var connectionProperties = context.getBean(OpenAiConnectionProperties.class);
+ var embeddingProperties = context.getBean(OpenAiEmbeddingProperties.class);
+
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+ assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY");
+
+ assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("text-embedding-ada-002");
+
+ assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(chatProperties.getOptions().getFrequencyPenalty()).isEqualTo(-1.5);
+ assertThat(chatProperties.getOptions().getLogitBias().get("myTokenId")).isEqualTo(-5);
+ assertThat(chatProperties.getOptions().getMaxTokens()).isEqualTo(123);
+ assertThat(chatProperties.getOptions().getN()).isEqualTo(10);
+ assertThat(chatProperties.getOptions().getPresencePenalty()).isEqualTo(0);
+ assertThat(chatProperties.getOptions().getSeed()).isEqualTo(66);
+ assertThat(chatProperties.getOptions().getStop()).contains("boza", "koza");
+ assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55);
+ assertThat(chatProperties.getOptions().getTopP()).isEqualTo(0.56);
+
+ JSONAssert.assertEquals("{\"type\":\"function\",\"function\":{\"name\":\"toolChoiceFunctionName\"}}",
+ "" + chatProperties.getOptions().getToolChoice(), JSONCompareMode.LENIENT);
+
+ assertThat(chatProperties.getOptions().getUser()).isEqualTo("userXYZ");
+
+ assertThat(chatProperties.getOptions().getTools()).hasSize(1);
+ var tool = chatProperties.getOptions().getTools().get(0);
+ assertThat(tool.getType()).isEqualTo(OpenAiApi.FunctionTool.Type.FUNCTION);
+ var function = tool.getFunction();
+ assertThat(function.getName()).isEqualTo("myFunction1");
+ assertThat(function.getDescription()).isEqualTo("function description");
+ assertThat(function.getParameters()).isNotEmpty();
+ });
+ }
+
+ @Test
+ public void transcriptionOptionsTest() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.openai.api-key=API_KEY",
+ "spring.ai.openai.base-url=TEST_BASE_URL",
+
+ "spring.ai.openai.audio.transcription.options.model=MODEL_XYZ",
+ "spring.ai.openai.audio.transcription.options.language=en",
+ "spring.ai.openai.audio.transcription.options.prompt=Er, yes, I think so",
+ "spring.ai.openai.audio.transcription.options.responseFormat=JSON",
+ "spring.ai.openai.audio.transcription.options.temperature=0.55"
+ )
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ var transcriptionProperties = context.getBean(OpenAiAudioTranscriptionProperties.class);
+ var connectionProperties = context.getBean(OpenAiConnectionProperties.class);
+ var embeddingProperties = context.getBean(OpenAiEmbeddingProperties.class);
+
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+ assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY");
+
+ assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("text-embedding-ada-002");
+
+ assertThat(transcriptionProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(transcriptionProperties.getOptions().getLanguage()).isEqualTo("en");
+ assertThat(transcriptionProperties.getOptions().getPrompt()).isEqualTo("Er, yes, I think so");
+ assertThat(transcriptionProperties.getOptions().getResponseFormat())
+ .isEqualTo(OpenAiAudioApi.TranscriptResponseFormat.JSON);
+ assertThat(transcriptionProperties.getOptions().getTemperature()).isEqualTo(0.55f);
+ });
+ }
+
+ @Test
+ public void embeddingOptionsTest() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.openai.api-key=API_KEY",
+ "spring.ai.openai.base-url=TEST_BASE_URL",
+
+ "spring.ai.openai.embedding.options.model=MODEL_XYZ",
+ "spring.ai.openai.embedding.options.encodingFormat=MyEncodingFormat",
+ "spring.ai.openai.embedding.options.user=userXYZ"
+ )
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ var connectionProperties = context.getBean(OpenAiConnectionProperties.class);
+ var embeddingProperties = context.getBean(OpenAiEmbeddingProperties.class);
+
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+ assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY");
+
+ assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(embeddingProperties.getOptions().getEncodingFormat()).isEqualTo("MyEncodingFormat");
+ assertThat(embeddingProperties.getOptions().getUser()).isEqualTo("userXYZ");
+ });
+ }
+
+ @Test
+ public void imageOptionsTest() {
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.openai.api-key=API_KEY",
+ "spring.ai.openai.base-url=TEST_BASE_URL",
+
+ "spring.ai.openai.image.options.n=3",
+ "spring.ai.openai.image.options.model=MODEL_XYZ",
+ "spring.ai.openai.image.options.quality=hd",
+ "spring.ai.openai.image.options.response_format=url",
+ "spring.ai.openai.image.options.size=1024x1024",
+ "spring.ai.openai.image.options.width=1024",
+ "spring.ai.openai.image.options.height=1024",
+ "spring.ai.openai.image.options.style=vivid",
+ "spring.ai.openai.image.options.user=userXYZ"
+ )
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ var imageProperties = context.getBean(OpenAiImageProperties.class);
+ var connectionProperties = context.getBean(OpenAiConnectionProperties.class);
+
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+ assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY");
+
+ assertThat(imageProperties.getOptions().getN()).isEqualTo(3);
+ assertThat(imageProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(imageProperties.getOptions().getQuality()).isEqualTo("hd");
+ assertThat(imageProperties.getOptions().getResponseFormat()).isEqualTo("url");
+ assertThat(imageProperties.getOptions().getSize()).isEqualTo("1024x1024");
+ assertThat(imageProperties.getOptions().getWidth()).isEqualTo(1024);
+ assertThat(imageProperties.getOptions().getHeight()).isEqualTo(1024);
+ assertThat(imageProperties.getOptions().getStyle()).isEqualTo("vivid");
+ assertThat(imageProperties.getOptions().getUser()).isEqualTo("userXYZ");
+ });
+ }
+
+ @Test
+ void embeddingActivation() {
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.embedding.enabled=false")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isNotEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.embedding.enabled=true")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isNotEmpty();
+ });
+ }
+
+ @Test
+ void chatActivation() {
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.chat.enabled=false")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiChatProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiChatModel.class)).isEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiChatProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiChatModel.class)).isNotEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.chat.enabled=true")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiChatProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiChatModel.class)).isNotEmpty();
+ });
+
+ }
+
+ @Test
+ void imageActivation() {
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.image.enabled=false")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiImageProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiImageModel.class)).isEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiImageProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiImageModel.class)).isNotEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.image.enabled=true")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiImageProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiImageModel.class)).isNotEmpty();
+ });
+
+ }
+
+ @Test
+ void audioSpeechActivation() {
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.audio.speech.enabled=false")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiAudioSpeechProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiAudioSpeechProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isNotEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.audio.speech.enabled=true")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiAudioSpeechProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isNotEmpty();
+ });
+
+ }
+
+ @Test
+ void audioTranscriptionActivation() {
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.audio.transcription.enabled=false")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiAudioTranscriptionProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiAudioTranscriptionProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isNotEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.audio.transcription.enabled=true")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiAudioTranscriptionProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isNotEmpty();
+ });
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiResponseFormatPropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiResponseFormatPropertiesTests.java
new file mode 100644
index 000000000..a863c88b7
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiResponseFormatPropertiesTests.java
@@ -0,0 +1,357 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.openai.autoconfigure;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.openai.OpenAiAudioSpeechModel;
+import org.springframework.ai.openai.OpenAiAudioTranscriptionModel;
+import org.springframework.ai.openai.OpenAiChatModel;
+import org.springframework.ai.openai.OpenAiEmbeddingModel;
+import org.springframework.ai.openai.OpenAiImageModel;
+import org.springframework.ai.openai.api.OpenAiAudioApi;
+import org.springframework.ai.openai.api.ResponseFormat;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit Tests for {@link OpenAiChatProperties} #options#responseFormat support.
+ *
+ * @author Christian Tzolov
+ */
+public class OpenAiResponseFormatPropertiesTests {
+
+ @Test
+ public void responseFormatJsonSchema() {
+
+ String responseFormatJsonSchema = """
+ {
+ "$schema" : "https://json-schema.org/draft/2020-12/schema",
+ "type" : "object",
+ "properties" : {
+ "someString" : {
+ "type" : "string"
+ }
+ },
+ "additionalProperties" : false
+ }
+ """;
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.openai.api-key=API_KEY",
+
+ "spring.ai.openai.chat.options.response-format.type=JSON_SCHEMA",
+ "spring.ai.openai.chat.options.response-format.name=MyName",
+ "spring.ai.openai.chat.options.response-format.schema=" + responseFormatJsonSchema,
+ "spring.ai.openai.chat.options.response-format.strict=true"
+ )
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ var chatProperties = context.getBean(OpenAiChatProperties.class);
+ var connectionProperties = context.getBean(OpenAiConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY");
+
+ assertThat(chatProperties.getOptions().getResponseFormat())
+ .isEqualTo(new ResponseFormat(ResponseFormat.Type.JSON_SCHEMA, responseFormatJsonSchema));
+ });
+ }
+
+ @Test
+ public void responseFormatJsonObject() {
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY",
+ "spring.ai.openai.chat.options.response-format.type=JSON_OBJECT")
+
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ var chatProperties = context.getBean(OpenAiChatProperties.class);
+
+ assertThat(chatProperties.getOptions().getResponseFormat())
+ .isEqualTo(ResponseFormat.builder().type(ResponseFormat.Type.JSON_OBJECT).build());
+ });
+ }
+
+ @Test
+ public void emptyResponseFormat() {
+
+ new ApplicationContextRunner().withPropertyValues("spring.ai.openai.api-key=API_KEY")
+
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ var chatProperties = context.getBean(OpenAiChatProperties.class);
+
+ assertThat(chatProperties.getOptions().getResponseFormat()).isNull();
+ });
+ }
+
+ @Test
+ public void transcriptionOptionsTest() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.openai.api-key=API_KEY",
+ "spring.ai.openai.base-url=TEST_BASE_URL",
+
+ "spring.ai.openai.audio.transcription.options.model=MODEL_XYZ",
+ "spring.ai.openai.audio.transcription.options.language=en",
+ "spring.ai.openai.audio.transcription.options.prompt=Er, yes, I think so",
+ "spring.ai.openai.audio.transcription.options.responseFormat=JSON",
+ "spring.ai.openai.audio.transcription.options.temperature=0.55"
+ )
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ var transcriptionProperties = context.getBean(OpenAiAudioTranscriptionProperties.class);
+ var connectionProperties = context.getBean(OpenAiConnectionProperties.class);
+ var embeddingProperties = context.getBean(OpenAiEmbeddingProperties.class);
+
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+ assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY");
+
+ assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("text-embedding-ada-002");
+
+ assertThat(transcriptionProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(transcriptionProperties.getOptions().getLanguage()).isEqualTo("en");
+ assertThat(transcriptionProperties.getOptions().getPrompt()).isEqualTo("Er, yes, I think so");
+ assertThat(transcriptionProperties.getOptions().getResponseFormat())
+ .isEqualTo(OpenAiAudioApi.TranscriptResponseFormat.JSON);
+ assertThat(transcriptionProperties.getOptions().getTemperature()).isEqualTo(0.55f);
+ });
+ }
+
+ @Test
+ public void embeddingOptionsTest() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.openai.api-key=API_KEY",
+ "spring.ai.openai.base-url=TEST_BASE_URL",
+
+ "spring.ai.openai.embedding.options.model=MODEL_XYZ",
+ "spring.ai.openai.embedding.options.encodingFormat=MyEncodingFormat",
+ "spring.ai.openai.embedding.options.user=userXYZ"
+ )
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ var connectionProperties = context.getBean(OpenAiConnectionProperties.class);
+ var embeddingProperties = context.getBean(OpenAiEmbeddingProperties.class);
+
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+ assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY");
+
+ assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(embeddingProperties.getOptions().getEncodingFormat()).isEqualTo("MyEncodingFormat");
+ assertThat(embeddingProperties.getOptions().getUser()).isEqualTo("userXYZ");
+ });
+ }
+
+ @Test
+ public void imageOptionsTest() {
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.openai.api-key=API_KEY",
+ "spring.ai.openai.base-url=TEST_BASE_URL",
+
+ "spring.ai.openai.image.options.n=3",
+ "spring.ai.openai.image.options.model=MODEL_XYZ",
+ "spring.ai.openai.image.options.quality=hd",
+ "spring.ai.openai.image.options.response_format=url",
+ "spring.ai.openai.image.options.size=1024x1024",
+ "spring.ai.openai.image.options.width=1024",
+ "spring.ai.openai.image.options.height=1024",
+ "spring.ai.openai.image.options.style=vivid",
+ "spring.ai.openai.image.options.user=userXYZ"
+ )
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ var imageProperties = context.getBean(OpenAiImageProperties.class);
+ var connectionProperties = context.getBean(OpenAiConnectionProperties.class);
+
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+ assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY");
+
+ assertThat(imageProperties.getOptions().getN()).isEqualTo(3);
+ assertThat(imageProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(imageProperties.getOptions().getQuality()).isEqualTo("hd");
+ assertThat(imageProperties.getOptions().getResponseFormat()).isEqualTo("url");
+ assertThat(imageProperties.getOptions().getSize()).isEqualTo("1024x1024");
+ assertThat(imageProperties.getOptions().getWidth()).isEqualTo(1024);
+ assertThat(imageProperties.getOptions().getHeight()).isEqualTo(1024);
+ assertThat(imageProperties.getOptions().getStyle()).isEqualTo("vivid");
+ assertThat(imageProperties.getOptions().getUser()).isEqualTo("userXYZ");
+ });
+ }
+
+ @Test
+ void embeddingActivation() {
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.embedding.enabled=false")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isNotEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.embedding.enabled=true")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isNotEmpty();
+ });
+ }
+
+ @Test
+ void chatActivation() {
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.chat.enabled=false")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiChatProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiChatModel.class)).isEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiChatProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiChatModel.class)).isNotEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.chat.enabled=true")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiChatProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiChatModel.class)).isNotEmpty();
+ });
+
+ }
+
+ @Test
+ void imageActivation() {
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.image.enabled=false")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiImageProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiImageModel.class)).isEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiImageProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiImageModel.class)).isNotEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.image.enabled=true")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiImageProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiImageModel.class)).isNotEmpty();
+ });
+
+ }
+
+ @Test
+ void audioSpeechActivation() {
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.audio.speech.enabled=false")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiAudioSpeechProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiAudioSpeechProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isNotEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.audio.speech.enabled=true")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiAudioSpeechProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isNotEmpty();
+ });
+
+ }
+
+ @Test
+ void audioTranscriptionActivation() {
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.audio.transcription.enabled=false")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiAudioTranscriptionProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiAudioTranscriptionProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isNotEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL",
+ "spring.ai.openai.audio.transcription.enabled=true")
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(OpenAiAudioTranscriptionProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isNotEmpty();
+ });
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackInPrompt2IT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackInPrompt2IT.java
new file mode 100644
index 000000000..8e238be52
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackInPrompt2IT.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.openai.autoconfigure.tool;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.ai.model.openai.autoconfigure.OpenAiAutoConfiguration;
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.openai.OpenAiChatModel;
+import org.springframework.ai.openai.api.OpenAiApi.ChatModel;
+import org.springframework.ai.tool.function.FunctionToolCallback;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*")
+public class FunctionCallbackInPrompt2IT {
+
+ private final Logger logger = LoggerFactory.getLogger(FunctionCallbackInPromptIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY"))
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class));
+
+ @Test
+ void functionCallTest() {
+ this.contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName())
+ .run(context -> {
+
+ OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class);
+
+ ChatClient chatClient = ChatClient.builder(chatModel).build();
+
+ // @formatter:off
+ chatClient.prompt()
+ .user("Tell me a joke?")
+ .call().content();
+
+ String content = ChatClient.builder(chatModel).build().prompt()
+ .user("What's the weather like in San Francisco, Tokyo, and Paris?")
+ .functions(FunctionToolCallback
+ .builder("CurrentWeatherService", new MockWeatherService())
+ .description("Get the weather in location")
+ .inputType(MockWeatherService.Request.class)
+ .build())
+ .call().content();
+ // @formatter:on
+
+ logger.info("Response: {}", content);
+
+ assertThat(content).contains("30", "10", "15");
+ });
+ }
+
+ @Test
+ void lambdaFunctionCallTest() {
+ Map state = new ConcurrentHashMap<>();
+
+ record LightInfo(String roomName, boolean isOn) {
+ }
+
+ this.contextRunner.run(context -> {
+
+ OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class);
+
+ // @formatter:off
+ String content = ChatClient.builder(chatModel).build().prompt()
+ .user("Turn the light on in the kitchen and in the living room!")
+ .functions(FunctionToolCallback
+ .builder("turnLight", (LightInfo lightInfo) -> {
+ logger.info("Turning light to [" + lightInfo.isOn + "] in " + lightInfo.roomName());
+ state.put(lightInfo.roomName(), lightInfo.isOn());
+ })
+ .description("Turn light on or off in a room")
+ .inputType(LightInfo.class)
+ .build())
+ .call().content();
+ // @formatter:on
+ logger.info("Response: {}", content);
+ assertThat(state).containsEntry("kitchen", Boolean.TRUE);
+ assertThat(state).containsEntry("living room", Boolean.TRUE);
+ });
+ }
+
+ @Test
+ void functionCallTest2() {
+ this.contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName())
+ .run(context -> {
+
+ OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class);
+
+ // @formatter:off
+ String content = ChatClient.builder(chatModel).build().prompt()
+ .user("What's the weather like in Amsterdam?")
+ .functions(FunctionToolCallback
+ .builder("CurrentWeatherService", input -> "18 degrees Celsius")
+ .description("Get the weather in location")
+ .inputType(MockWeatherService.Request.class)
+ .build())
+ .call().content();
+ // @formatter:on
+ logger.info("Response: {}", content);
+
+ assertThat(content).contains("18");
+ });
+ }
+
+ @Test
+ void streamingFunctionCallTest() {
+
+ this.contextRunner.withPropertyValues("spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName())
+ .run(context -> {
+
+ OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class);
+
+ // @formatter:off
+ String content = ChatClient.builder(chatModel).build().prompt()
+ .user("What's the weather like in San Francisco, Tokyo, and Paris?")
+ .functions(FunctionToolCallback
+ .builder("CurrentWeatherService", new MockWeatherService())
+ .description("Get the weather in location")
+ .inputType(MockWeatherService.Request.class)
+ .build())
+ .stream().content()
+ .collectList().block().stream().collect(Collectors.joining());
+ // @formatter:on
+
+ logger.info("Response: {}", content);
+
+ assertThat(content).contains("30", "10", "15");
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackInPromptIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackInPromptIT.java
new file mode 100644
index 000000000..45596ff5e
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackInPromptIT.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.openai.autoconfigure.tool;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.model.openai.autoconfigure.OpenAiAutoConfiguration;
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.model.Generation;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.openai.OpenAiChatModel;
+import org.springframework.ai.openai.OpenAiChatOptions;
+import org.springframework.ai.openai.api.OpenAiApi.ChatModel;
+import org.springframework.ai.tool.function.FunctionToolCallback;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*")
+public class FunctionCallbackInPromptIT {
+
+ private final Logger logger = LoggerFactory.getLogger(FunctionCallbackInPromptIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY"))
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class));
+
+ @Test
+ void functionCallTest() {
+ this.contextRunner
+ .withPropertyValues("spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName(),
+ "spring.ai.openai.chat.options.temperature=0.1")
+ .run(context -> {
+
+ OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class);
+
+ UserMessage userMessage = new UserMessage(
+ "What's the weather like in San Francisco, Tokyo, and Paris?");
+
+ var promptOptions = OpenAiChatOptions.builder()
+ .functionCallbacks(
+ List.of(FunctionToolCallback.builder("CurrentWeatherService", new MockWeatherService())
+ .description("Get the weather in location")
+ .inputType(MockWeatherService.Request.class)
+ .build()))
+ .build();
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), promptOptions));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+ });
+ }
+
+ @Test
+ void streamingFunctionCallTest() {
+
+ this.contextRunner
+ .withPropertyValues("spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName(),
+ "spring.ai.openai.chat.options.temperature=0.5")
+ .run(context -> {
+
+ OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class);
+
+ UserMessage userMessage = new UserMessage(
+ "What's the weather like in San Francisco, Tokyo, and Paris?");
+
+ var promptOptions = OpenAiChatOptions.builder()
+ .functionCallbacks(
+ List.of(FunctionToolCallback.builder("CurrentWeatherService", new MockWeatherService())
+ .description("Get the weather in location")
+ .inputType(MockWeatherService.Request.class)
+ .build()))
+ .build();
+
+ Flux response = chatModel.stream(new Prompt(List.of(userMessage), promptOptions));
+
+ String content = response.collectList()
+ .block()
+ .stream()
+ .map(ChatResponse::getResults)
+ .flatMap(List::stream)
+ .map(Generation::getOutput)
+ .map(AssistantMessage::getText)
+ .collect(Collectors.joining());
+ logger.info("Response: {}", content);
+
+ assertThat(content).contains("30", "10", "15");
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java
new file mode 100644
index 000000000..b1ddf745d
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java
@@ -0,0 +1,431 @@
+/*
+ * Copyright 2023-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.openai.autoconfigure.tool;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.model.openai.autoconfigure.OpenAiAutoConfiguration;
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.model.Generation;
+import org.springframework.ai.chat.model.ToolContext;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.model.tool.ToolCallingChatOptions;
+import org.springframework.ai.openai.OpenAiChatModel;
+import org.springframework.ai.openai.OpenAiChatOptions;
+import org.springframework.ai.openai.api.OpenAiApi.ChatModel;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Description;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*")
+class FunctionCallbackWithPlainFunctionBeanIT {
+
+ private static final Logger logger = LoggerFactory.getLogger(FunctionCallbackWithPlainFunctionBeanIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY"),
+ "spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName())
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .withUserConfiguration(Config.class);
+
+ private static Map feedback = new ConcurrentHashMap<>();
+
+ @BeforeEach
+ void setUp() {
+ feedback.clear();
+ }
+
+ @Test
+ void functionCallingVoidInput() {
+ this.contextRunner.run(context -> {
+
+ OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class);
+
+ // Test weatherFunction
+ UserMessage userMessage = new UserMessage("Turn the light on in the living room");
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
+ OpenAiChatOptions.builder().function("turnLivingRoomLightOn").build()));
+
+ logger.info("Response: {}", response);
+ assertThat(feedback).hasSize(1);
+ assertThat(feedback.get("turnLivingRoomLightOn")).isEqualTo(Boolean.valueOf(true));
+ });
+ }
+
+ @Test
+ void functionCallingSupplier() {
+ this.contextRunner.run(context -> {
+
+ OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class);
+
+ // Test weatherFunction
+ UserMessage userMessage = new UserMessage("Turn the light on in the living room");
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
+ OpenAiChatOptions.builder().function("turnLivingRoomLightOnSupplier").build()));
+
+ logger.info("Response: {}", response);
+ assertThat(feedback).hasSize(1);
+ assertThat(feedback.get("turnLivingRoomLightOnSupplier")).isEqualTo(Boolean.valueOf(true));
+ });
+ }
+
+ @Test
+ void functionCallingVoidOutput() {
+ this.contextRunner.run(context -> {
+
+ OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class);
+
+ // Test weatherFunction
+ UserMessage userMessage = new UserMessage("Turn the light on in the kitchen and in the living room");
+
+ ChatResponse response = chatModel
+ .call(new Prompt(List.of(userMessage), OpenAiChatOptions.builder().function("turnLight").build()));
+
+ logger.info("Response: {}", response);
+ assertThat(feedback).hasSize(2);
+ assertThat(feedback.get("kitchen")).isEqualTo(Boolean.valueOf(true));
+ assertThat(feedback.get("living room")).isEqualTo(Boolean.valueOf(true));
+ });
+ }
+
+ @Test
+ void functionCallingConsumer() {
+ this.contextRunner.run(context -> {
+
+ OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class);
+
+ // Test weatherFunction
+ UserMessage userMessage = new UserMessage("Turn the light on in the kitchen and in the living room");
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
+ OpenAiChatOptions.builder().function("turnLightConsumer").build()));
+
+ logger.info("Response: {}", response);
+ assertThat(feedback).hasSize(2);
+ assertThat(feedback.get("kitchen")).isEqualTo(Boolean.valueOf(true));
+ assertThat(feedback.get("living room")).isEqualTo(Boolean.valueOf(true));
+
+ });
+ }
+
+ @Test
+ void trainScheduler() {
+ this.contextRunner.run(context -> {
+
+ OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class);
+
+ // Test weatherFunction
+ UserMessage userMessage = new UserMessage(
+ "Please schedule a train from San Francisco to Los Angeles on 2023-12-25");
+
+ ToolCallingChatOptions functionOptions = ToolCallingChatOptions.builder()
+ .toolNames("trainReservation")
+ .build();
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), functionOptions));
+
+ logger.info("Response: {}", response.getResult().getOutput().getText());
+ });
+ }
+
+ @Test
+ void functionCallWithDirectBiFunction() {
+ this.contextRunner.run(context -> {
+
+ OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class);
+
+ ChatClient chatClient = ChatClient.builder(chatModel).build();
+
+ String content = chatClient.prompt("What's the weather like in San Francisco, Tokyo, and Paris?")
+ .functions("weatherFunctionWithContext")
+ .toolContext(Map.of("sessionId", "123"))
+ .call()
+ .content();
+ logger.info(content);
+
+ // Test weatherFunction
+ UserMessage userMessage = new UserMessage(
+ "What's the weather like in San Francisco, Tokyo, and Paris? You can call the following functions 'weatherFunction'");
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
+ OpenAiChatOptions.builder()
+ .function("weatherFunctionWithContext")
+ .toolContext(Map.of("sessionId", "123"))
+ .build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+
+ });
+ }
+
+ @Test
+ void functionCallWithBiFunctionClass() {
+ this.contextRunner.run(context -> {
+
+ OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class);
+
+ ChatClient chatClient = ChatClient.builder(chatModel).build();
+
+ String content = chatClient.prompt("What's the weather like in San Francisco, Tokyo, and Paris?")
+ .functions("weatherFunctionWithClassBiFunction")
+ .toolContext(Map.of("sessionId", "123"))
+ .call()
+ .content();
+ logger.info(content);
+
+ // Test weatherFunction
+ UserMessage userMessage = new UserMessage(
+ "What's the weather like in San Francisco, Tokyo, and Paris? You can call the following functions 'weatherFunction'");
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
+ OpenAiChatOptions.builder()
+ .function("weatherFunctionWithClassBiFunction")
+ .toolContext(Map.of("sessionId", "123"))
+ .build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+
+ });
+ }
+
+ @Test
+ void functionCallTest() {
+ this.contextRunner.run(context -> {
+
+ OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class);
+
+ // Test weatherFunction
+ UserMessage userMessage = new UserMessage(
+ "What's the weather like in San Francisco, Tokyo, and Paris? You can call the following functions 'weatherFunction'");
+
+ ChatResponse response = chatModel.call(
+ new Prompt(List.of(userMessage), OpenAiChatOptions.builder().function("weatherFunction").build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+
+ // Test weatherFunctionTwo
+ response = chatModel.call(new Prompt(List.of(userMessage),
+ OpenAiChatOptions.builder().function("weatherFunctionTwo").build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+
+ });
+ }
+
+ @Test
+ void functionCallWithPortableFunctionCallingOptions() {
+ this.contextRunner.run(context -> {
+
+ OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class);
+
+ // Test weatherFunction
+ UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?");
+
+ ToolCallingChatOptions functionOptions = ToolCallingChatOptions.builder()
+ .toolNames("weatherFunction")
+ .build();
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), functionOptions));
+
+ logger.info("Response: {}", response.getResult().getOutput().getText());
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+ });
+ }
+
+ @Test
+ void streamFunctionCallTest() {
+ this.contextRunner.run(context -> {
+
+ OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class);
+
+ // Test weatherFunction
+ UserMessage userMessage = new UserMessage(
+ "What's the weather like in San Francisco, Tokyo, and Paris? You can call the following functions 'weatherFunction'");
+
+ Flux response = chatModel.stream(
+ new Prompt(List.of(userMessage), OpenAiChatOptions.builder().function("weatherFunction").build()));
+
+ String content = response.collectList()
+ .block()
+ .stream()
+ .map(ChatResponse::getResults)
+ .flatMap(List::stream)
+ .map(Generation::getOutput)
+ .map(AssistantMessage::getText)
+ .collect(Collectors.joining());
+ logger.info("Response: {}", content);
+
+ assertThat(content).contains("30", "10", "15");
+
+ // Test weatherFunctionTwo
+ response = chatModel.stream(new Prompt(List.of(userMessage),
+ OpenAiChatOptions.builder().function("weatherFunctionTwo").build()));
+
+ content = response.collectList()
+ .block()
+ .stream()
+ .map(ChatResponse::getResults)
+ .flatMap(List::stream)
+ .map(Generation::getOutput)
+ .map(AssistantMessage::getText)
+ .collect(Collectors.joining());
+ logger.info("Response: {}", content);
+
+ assertThat(content).isNotEmpty().withFailMessage("Content returned from OpenAI model is empty");
+ assertThat(content).contains("30", "10", "15");
+
+ });
+ }
+
+ @Configuration
+ static class Config {
+
+ @Bean
+ @Description("Get the weather in location")
+ public MyBiFunction weatherFunctionWithClassBiFunction() {
+ return new MyBiFunction();
+ }
+
+ @Bean
+ @Description("Get the weather in location")
+ public BiFunction weatherFunctionWithContext() {
+ return (request, context) -> new MockWeatherService().apply(request);
+ }
+
+ @Bean
+ @Description("Get the weather in location")
+ public Function weatherFunction() {
+ return new MockWeatherService();
+ }
+
+ // Relies on the Request's JsonClassDescription annotation to provide the
+ // function description.
+ @Bean
+ public Function weatherFunctionTwo() {
+ MockWeatherService weatherService = new MockWeatherService();
+ return (weatherService::apply);
+ }
+
+ @Bean
+ @Description("Turn light on or off in a room")
+ public Function turnLight() {
+ return (LightInfo lightInfo) -> {
+ logger.info("Turning light to [" + lightInfo.isOn + "] in " + lightInfo.roomName());
+ feedback.put(lightInfo.roomName(), lightInfo.isOn());
+ return null;
+ };
+ }
+
+ @Bean
+ @Description("Turn light on or off in a room")
+ public Consumer turnLightConsumer() {
+ return (LightInfo lightInfo) -> {
+ logger.info("Turning light to [" + lightInfo.isOn + "] in " + lightInfo.roomName());
+ feedback.put(lightInfo.roomName(), lightInfo.isOn());
+ };
+ }
+
+ @Bean
+ @Description("Turns light on in the living room")
+ public Function turnLivingRoomLightOn() {
+ return (Void v) -> {
+ logger.info("Turning light on in the living room");
+ feedback.put("turnLivingRoomLightOn", Boolean.TRUE);
+ return "Done";
+ };
+ }
+
+ @Bean
+ @Description("Turns light on in the living room")
+ public Supplier turnLivingRoomLightOnSupplier() {
+ return () -> {
+ logger.info("Turning light on in the living room");
+ feedback.put("turnLivingRoomLightOnSupplier", Boolean.TRUE);
+ return "Done";
+ };
+ }
+
+ @Bean
+ @Description("Schedule a train reservation")
+ public Function, TrainSearchResponse> trainReservation() {
+ return (TrainSearchRequest request) -> {
+ logger.info("Turning light to [" + request.data().from() + "] in " + request.data().to());
+ return new TrainSearchResponse<>(
+ new TrainSearchScheduleResponse(request.data().from(), request.data().to(), "", "123"));
+ };
+ }
+
+ }
+
+ public static class MyBiFunction
+ implements BiFunction {
+
+ @Override
+ public MockWeatherService.Response apply(MockWeatherService.Request request, ToolContext context) {
+ return new MockWeatherService().apply(request);
+ }
+
+ }
+
+ record LightInfo(String roomName, boolean isOn) {
+ }
+
+ record TrainSearchSchedule(String from, String to, String date) {
+ }
+
+ record TrainSearchScheduleResponse(String from, String to, String date, String trainNumber) {
+ }
+
+ record TrainSearchRequest(T data) {
+ }
+
+ record TrainSearchResponse(T data) {
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/MockWeatherService.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/MockWeatherService.java
new file mode 100644
index 000000000..f3f24a06f
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/MockWeatherService.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.openai.autoconfigure.tool;
+
+import java.util.function.Function;
+
+import com.fasterxml.jackson.annotation.JsonClassDescription;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyDescription;
+
+/**
+ * Mock 3rd party weather service.
+ *
+ * @author Christian Tzolov
+ */
+public class MockWeatherService implements Function {
+
+ @Override
+ public Response apply(Request request) {
+
+ double temperature = 10;
+ if (request.location().contains("Paris")) {
+ temperature = 15;
+ }
+ else if (request.location().contains("Tokyo")) {
+ temperature = 10;
+ }
+ else if (request.location().contains("San Francisco")) {
+ temperature = 30;
+ }
+
+ return new Response(temperature, 15, 20, 2, 53, 45, Unit.C);
+ }
+
+ /**
+ * Temperature units.
+ */
+ public enum Unit {
+
+ /**
+ * Celsius.
+ */
+ C("metric"),
+ /**
+ * Fahrenheit.
+ */
+ F("imperial");
+
+ /**
+ * Human readable unit name.
+ */
+ public final String unitName;
+
+ Unit(String text) {
+ this.unitName = text;
+ }
+
+ }
+
+ /**
+ * Weather Function request.
+ */
+ @JsonInclude(Include.NON_NULL)
+ @JsonClassDescription("Weather API request")
+ public record Request(@JsonProperty(required = true,
+ value = "location") @JsonPropertyDescription("The city and state e.g. San Francisco, CA") String location,
+ @JsonProperty(required = true, value = "lat") @JsonPropertyDescription("The city latitude") double lat,
+ @JsonProperty(required = true, value = "lon") @JsonPropertyDescription("The city longitude") double lon,
+ @JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) {
+
+ }
+
+ /**
+ * Weather Function response.
+ */
+ public record Response(double temp, double feels_like, double temp_min, double temp_max, int pressure, int humidity,
+ Unit unit) {
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/OpenAiFunctionCallback2IT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/OpenAiFunctionCallback2IT.java
new file mode 100644
index 000000000..682a8dacb
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/OpenAiFunctionCallback2IT.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.openai.autoconfigure.tool;
+
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.ai.model.openai.autoconfigure.OpenAiAutoConfiguration;
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.openai.OpenAiChatModel;
+import org.springframework.ai.openai.api.OpenAiApi.ChatModel;
+import org.springframework.ai.tool.ToolCallback;
+import org.springframework.ai.tool.function.FunctionToolCallback;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*")
+public class OpenAiFunctionCallback2IT {
+
+ private final Logger logger = LoggerFactory.getLogger(OpenAiFunctionCallback2IT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY"),
+ "spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName())
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .withUserConfiguration(Config.class);
+
+ @Test
+ void functionCallTest() {
+ this.contextRunner.withPropertyValues("spring.ai.openai.chat.options.temperature=0.1").run(context -> {
+
+ OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class);
+
+ // @formatter:off
+ ChatClient chatClient = ChatClient.builder(chatModel)
+ .defaultFunctions("WeatherInfo")
+ .defaultUser(u -> u.text("What's the weather like in {cities}?"))
+ .build();
+
+ String content = chatClient.prompt()
+ .user(u -> u.param("cities", "San Francisco, Tokyo, Paris"))
+ .call().content();
+ // @formatter:on
+
+ logger.info("Response: {}", content);
+
+ assertThat(content).contains("30", "10", "15");
+ });
+ }
+
+ @Test
+ void streamFunctionCallTest() {
+ this.contextRunner.withPropertyValues("spring.ai.openai.chat.options.temperature=0.1").run(context -> {
+
+ OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class);
+
+ // @formatter:off
+ String content = ChatClient.builder(chatModel).build().prompt()
+ .functions("WeatherInfo")
+ .user("What's the weather like in San Francisco, Tokyo, and Paris?")
+ .stream().content()
+ .collectList().block().stream().collect(Collectors.joining());
+ // @formatter:on
+
+ logger.info("Response: {}", content);
+
+ assertThat(content).contains("30", "10", "15");
+ });
+ }
+
+ @Configuration
+ static class Config {
+
+ @Bean
+ public ToolCallback weatherFunctionInfo() {
+
+ return FunctionToolCallback.builder("WeatherInfo", new MockWeatherService())
+ .description("Get the weather in location")
+ .inputType(MockWeatherService.Request.class)
+ .build();
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/OpenAiFunctionCallbackIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/OpenAiFunctionCallbackIT.java
new file mode 100644
index 000000000..6ab701eb2
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/OpenAiFunctionCallbackIT.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.openai.autoconfigure.tool;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.model.openai.autoconfigure.OpenAiAutoConfiguration;
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.model.Generation;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.openai.OpenAiChatModel;
+import org.springframework.ai.openai.OpenAiChatOptions;
+import org.springframework.ai.openai.api.OpenAiApi.ChatModel;
+import org.springframework.ai.tool.ToolCallback;
+import org.springframework.ai.tool.function.FunctionToolCallback;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*")
+public class OpenAiFunctionCallbackIT {
+
+ private final Logger logger = LoggerFactory.getLogger(OpenAiFunctionCallbackIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY"),
+ "spring.ai.openai.chat.options.model=" + ChatModel.GPT_4_O_MINI.getName())
+ .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class))
+ .withUserConfiguration(Config.class);
+
+ @Test
+ void functionCallTest() {
+ this.contextRunner.withPropertyValues("spring.ai.openai.chat.options.temperature=0.1").run(context -> {
+
+ OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class);
+
+ UserMessage userMessage = new UserMessage("What's the weather like in San Francisco, Tokyo, and Paris?");
+
+ ChatResponse response = chatModel
+ .call(new Prompt(List.of(userMessage), OpenAiChatOptions.builder().function("WeatherInfo").build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+
+ });
+ }
+
+ @Test
+ void streamFunctionCallTest() {
+ this.contextRunner.withPropertyValues("spring.ai.openai.chat.options.temperature=0.1").run(context -> {
+
+ OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class);
+
+ UserMessage userMessage = new UserMessage(
+ "What's the weather like in San Francisco, Tokyo, and Paris? You can call the following functions 'WeatherInfo'");
+
+ Flux response = chatModel
+ .stream(new Prompt(List.of(userMessage), OpenAiChatOptions.builder().function("WeatherInfo").build()));
+
+ String content = response.collectList()
+ .block()
+ .stream()
+ .map(ChatResponse::getResults)
+ .flatMap(List::stream)
+ .map(Generation::getOutput)
+ .map(AssistantMessage::getText)
+ .collect(Collectors.joining());
+ logger.info("Response: {}", content);
+
+ assertThat(content).containsAnyOf("30.0", "30");
+ assertThat(content).containsAnyOf("10.0", "10");
+ assertThat(content).containsAnyOf("15.0", "15");
+
+ });
+ }
+
+ @Configuration
+ static class Config {
+
+ @Bean
+ public ToolCallback weatherFunctionInfo() {
+
+ return FunctionToolCallback.builder("WeatherInfo", new MockWeatherService())
+ .description("Get the weather in location")
+ .inputType(MockWeatherService.Request.class)
+ .build();
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlAutoConfigurationIT.java
new file mode 100644
index 000000000..678c9f75f
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlAutoConfigurationIT.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.postgresml.autoconfigure;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.PostgreSQLContainer;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+import org.springframework.ai.embedding.EmbeddingResponse;
+import org.springframework.ai.postgresml.PostgresMlEmbeddingModel;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
+import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Utkarsh Srivastava
+ */
+@JdbcTest(properties = "logging.level.sql=TRACE")
+@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
+@Testcontainers
+@Disabled("Disabled from automatic execution, as it requires an excessive amount of memory (over 9GB)!")
+public class PostgresMlAutoConfigurationIT {
+
+ @Container
+ @ServiceConnection
+ static PostgreSQLContainer> postgres = new PostgreSQLContainer<>(
+ DockerImageName.parse("ghcr.io/postgresml/postgresml:2.8.1").asCompatibleSubstituteFor("postgres"))
+ .withCommand("sleep", "infinity")
+ .withUsername("postgresml")
+ .withPassword("postgresml")
+ .withDatabaseName("postgresml")
+ .waitingFor(Wait.forLogMessage(".*Starting dashboard.*\\s", 1));
+
+ @Autowired
+ JdbcTemplate jdbcTemplate;
+
+ @Test
+ void embedding() {
+ ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withBean(JdbcTemplate.class, () -> this.jdbcTemplate)
+ .withConfiguration(AutoConfigurations.of(PostgresMlAutoConfiguration.class));
+ contextRunner.run(context -> {
+ PostgresMlEmbeddingModel embeddingModel = context.getBean(PostgresMlEmbeddingModel.class);
+
+ EmbeddingResponse embeddingResponse = embeddingModel
+ .embedForResponse(List.of("Hello World", "World is big and salvation is near"));
+ assertThat(embeddingResponse.getResults()).hasSize(2);
+ assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty();
+ assertThat(embeddingResponse.getResults().get(0).getIndex()).isZero();
+ assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty();
+ assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1);
+
+ assertThat(embeddingModel.dimensions()).isEqualTo(768);
+ });
+ }
+
+ @Test
+ void embeddingActivation() {
+ new ApplicationContextRunner().withBean(JdbcTemplate.class, () -> this.jdbcTemplate)
+ .withConfiguration(AutoConfigurations.of(PostgresMlAutoConfiguration.class))
+ .withPropertyValues("spring.ai.postgresml.embedding.enabled=false")
+ .run(context -> {
+ assertThat(context.getBeansOfType(PostgresMlEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(PostgresMlEmbeddingModel.class)).isEmpty();
+ });
+
+ new ApplicationContextRunner().withBean(JdbcTemplate.class, () -> this.jdbcTemplate)
+ .withConfiguration(AutoConfigurations.of(PostgresMlAutoConfiguration.class))
+ .withPropertyValues("spring.ai.postgresml.embedding.enabled=true")
+ .run(context -> {
+ assertThat(context.getBeansOfType(PostgresMlEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(PostgresMlEmbeddingModel.class)).isNotEmpty();
+ });
+
+ new ApplicationContextRunner().withBean(JdbcTemplate.class, () -> this.jdbcTemplate)
+ .withConfiguration(AutoConfigurations.of(PostgresMlAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(PostgresMlEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(PostgresMlEmbeddingModel.class)).isNotEmpty();
+ });
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlEmbeddingPropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlEmbeddingPropertiesTests.java
new file mode 100644
index 000000000..9ac0dd861
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlEmbeddingPropertiesTests.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.postgresml.autoconfigure;
+
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.document.MetadataMode;
+import org.springframework.ai.postgresml.PostgresMlEmbeddingModel;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit Tests for {@link PostgresMlEmbeddingProperties}.
+ *
+ * @author Utkarsh Srivastava
+ * @author Christian Tzolov
+ */
+@SpringBootTest(properties = { "spring.ai.postgresml.embedding.options.metadata-mode=all",
+ "spring.ai.postgresml.embedding.options.kwargs.key1=value1",
+ "spring.ai.postgresml.embedding.options.kwargs.key2=value2",
+ "spring.ai.postgresml.embedding.options.transformer=abc123" })
+class PostgresMlEmbeddingPropertiesTests {
+
+ @Autowired
+ private PostgresMlEmbeddingProperties postgresMlProperties;
+
+ @Test
+ void postgresMlPropertiesAreCorrect() {
+ assertThat(this.postgresMlProperties).isNotNull();
+ assertThat(this.postgresMlProperties.getOptions().getTransformer()).isEqualTo("abc123");
+ assertThat(this.postgresMlProperties.getOptions().getVectorType())
+ .isEqualTo(PostgresMlEmbeddingModel.VectorType.PG_ARRAY);
+ assertThat(this.postgresMlProperties.getOptions().getKwargs())
+ .isEqualTo(Map.of("key1", "value1", "key2", "value2"));
+ assertThat(this.postgresMlProperties.getOptions().getMetadataMode()).isEqualTo(MetadataMode.ALL);
+ }
+
+ @SpringBootConfiguration
+ @EnableConfigurationProperties(PostgresMlEmbeddingProperties.class)
+ static class TestConfiguration {
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanAutoConfigurationIT.java
new file mode 100644
index 000000000..bc2334e20
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanAutoConfigurationIT.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.qianfan.autoconfigure;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariables;
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.embedding.EmbeddingResponse;
+import org.springframework.ai.image.ImagePrompt;
+import org.springframework.ai.image.ImageResponse;
+import org.springframework.ai.qianfan.QianFanChatModel;
+import org.springframework.ai.qianfan.QianFanEmbeddingModel;
+import org.springframework.ai.qianfan.QianFanImageModel;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Geng Rong
+ */
+@EnabledIfEnvironmentVariables({ @EnabledIfEnvironmentVariable(named = "QIANFAN_API_KEY", matches = ".+"),
+ @EnabledIfEnvironmentVariable(named = "QIANFAN_SECRET_KEY", matches = ".+") })
+public class QianFanAutoConfigurationIT {
+
+ private static final Log logger = LogFactory.getLog(QianFanAutoConfigurationIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.qianfan.apiKey=" + System.getenv("QIANFAN_API_KEY"),
+ "spring.ai.qianfan.secretKey=" + System.getenv("QIANFAN_SECRET_KEY"))
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, QianFanAutoConfiguration.class));
+
+ @Test
+ void generate() {
+ this.contextRunner.run(context -> {
+ QianFanChatModel client = context.getBean(QianFanChatModel.class);
+ String response = client.call("Hello");
+ assertThat(response).isNotEmpty();
+ logger.info("Response: " + response);
+ });
+ }
+
+ @Test
+ void generateStreaming() {
+ this.contextRunner.run(context -> {
+ QianFanChatModel client = context.getBean(QianFanChatModel.class);
+ 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: " + response);
+ });
+ }
+
+ @Test
+ void embedding() {
+ this.contextRunner.run(context -> {
+ QianFanEmbeddingModel embeddingClient = context.getBean(QianFanEmbeddingModel.class);
+
+ EmbeddingResponse embeddingResponse = embeddingClient
+ .embedForResponse(List.of("Hello World", "World is big and salvation is near"));
+ assertThat(embeddingResponse.getResults()).hasSize(2);
+ assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty();
+ assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0);
+ assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty();
+ assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1);
+
+ assertThat(embeddingClient.dimensions()).isEqualTo(1024);
+ });
+ }
+
+ @Test
+ void generateImage() {
+ this.contextRunner.withPropertyValues("spring.ai.qianfan.image.options.size=1024x1024").run(context -> {
+ QianFanImageModel imageModel = context.getBean(QianFanImageModel.class);
+ ImageResponse imageResponse = imageModel.call(new ImagePrompt("forest"));
+ assertThat(imageResponse.getResults()).hasSize(1);
+ assertThat(imageResponse.getResult().getOutput().getUrl()).isNull();
+ assertThat(imageResponse.getResult().getOutput().getB64Json()).isNotEmpty();
+ logger.info("Generated image: " + imageResponse.getResult().getOutput().getB64Json());
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanPropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanPropertiesTests.java
new file mode 100644
index 000000000..8d976f971
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanPropertiesTests.java
@@ -0,0 +1,438 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.qianfan.autoconfigure;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.qianfan.QianFanChatModel;
+import org.springframework.ai.qianfan.QianFanEmbeddingModel;
+import org.springframework.ai.qianfan.QianFanImageModel;
+import org.springframework.ai.qianfan.api.QianFanApi;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit Tests for {@link QianFanConnectionProperties}, {@link QianFanChatProperties} and
+ * {@link QianFanEmbeddingProperties}.
+ *
+ * @author Geng Rong
+ */
+public class QianFanPropertiesTests {
+
+ @Test
+ public void chatProperties() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.qianfan.base-url=TEST_BASE_URL",
+ "spring.ai.qianfan.api-key=abc123",
+ "spring.ai.qianfan.secret-key=def123",
+ "spring.ai.qianfan.chat.options.model=MODEL_XYZ",
+ "spring.ai.qianfan.chat.options.temperature=0.55")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, QianFanAutoConfiguration.class))
+ .run(context -> {
+ var chatProperties = context.getBean(QianFanChatProperties.class);
+ var connectionProperties = context.getBean(QianFanConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getSecretKey()).isEqualTo("def123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(chatProperties.getApiKey()).isNull();
+ assertThat(chatProperties.getBaseUrl()).isNull();
+
+ assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55);
+ });
+ }
+
+ @Test
+ public void chatOverrideConnectionProperties() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.qianfan.base-url=TEST_BASE_URL",
+ "spring.ai.qianfan.api-key=abc123",
+ "spring.ai.qianfan.secret-key=def123",
+ "spring.ai.qianfan.chat.base-url=TEST_BASE_URL2",
+ "spring.ai.qianfan.chat.api-key=456",
+ "spring.ai.qianfan.chat.secret-key=def456",
+ "spring.ai.qianfan.chat.options.model=MODEL_XYZ",
+ "spring.ai.qianfan.chat.options.temperature=0.55")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, QianFanAutoConfiguration.class))
+ .run(context -> {
+ var chatProperties = context.getBean(QianFanChatProperties.class);
+ var connectionProperties = context.getBean(QianFanConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getSecretKey()).isEqualTo("def123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(chatProperties.getApiKey()).isEqualTo("456");
+ assertThat(chatProperties.getSecretKey()).isEqualTo("def456");
+ assertThat(chatProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2");
+
+ assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55);
+ });
+ }
+
+ @Test
+ public void embeddingProperties() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.qianfan.base-url=TEST_BASE_URL",
+ "spring.ai.qianfan.api-key=abc123",
+ "spring.ai.qianfan.secret-key=def123",
+ "spring.ai.qianfan.embedding.options.model=MODEL_XYZ")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, QianFanAutoConfiguration.class))
+ .run(context -> {
+ var embeddingProperties = context.getBean(QianFanEmbeddingProperties.class);
+ var connectionProperties = context.getBean(QianFanConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getSecretKey()).isEqualTo("def123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(embeddingProperties.getApiKey()).isNull();
+ assertThat(embeddingProperties.getBaseUrl()).isNull();
+
+ assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ });
+ }
+
+ @Test
+ public void embeddingOverrideConnectionProperties() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.qianfan.base-url=TEST_BASE_URL",
+ "spring.ai.qianfan.api-key=abc123",
+ "spring.ai.qianfan.secret-key=def123",
+ "spring.ai.qianfan.embedding.base-url=TEST_BASE_URL2",
+ "spring.ai.qianfan.embedding.api-key=456",
+ "spring.ai.qianfan.embedding.secret-key=def456",
+ "spring.ai.qianfan.embedding.options.model=MODEL_XYZ")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, QianFanAutoConfiguration.class))
+ .run(context -> {
+ var embeddingProperties = context.getBean(QianFanEmbeddingProperties.class);
+ var connectionProperties = context.getBean(QianFanConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getSecretKey()).isEqualTo("def123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(embeddingProperties.getApiKey()).isEqualTo("456");
+ assertThat(embeddingProperties.getSecretKey()).isEqualTo("def456");
+ assertThat(embeddingProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2");
+
+ assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ });
+ }
+
+ @Test
+ public void chatOptionsTest() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.qianfan.api-key=API_KEY",
+ "spring.ai.qianfan.secret-key=SECRET_KEY",
+ "spring.ai.qianfan.base-url=TEST_BASE_URL",
+
+ "spring.ai.qianfan.chat.options.model=MODEL_XYZ",
+ "spring.ai.qianfan.chat.options.frequencyPenalty=-1.5",
+ "spring.ai.qianfan.chat.options.logitBias.myTokenId=-5",
+ "spring.ai.qianfan.chat.options.maxTokens=123",
+ "spring.ai.qianfan.chat.options.presencePenalty=0",
+ "spring.ai.qianfan.chat.options.responseFormat.type=json",
+ "spring.ai.qianfan.chat.options.stop=boza,koza",
+ "spring.ai.qianfan.chat.options.temperature=0.55",
+ "spring.ai.qianfan.chat.options.topP=0.56"
+ )
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, QianFanAutoConfiguration.class))
+ .run(context -> {
+ var chatProperties = context.getBean(QianFanChatProperties.class);
+ var connectionProperties = context.getBean(QianFanConnectionProperties.class);
+ var embeddingProperties = context.getBean(QianFanEmbeddingProperties.class);
+
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+ assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY");
+ assertThat(connectionProperties.getSecretKey()).isEqualTo("SECRET_KEY");
+
+ assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("bge_large_zh");
+
+ assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(chatProperties.getOptions().getFrequencyPenalty()).isEqualTo(-1.5);
+ assertThat(chatProperties.getOptions().getMaxTokens()).isEqualTo(123);
+ assertThat(chatProperties.getOptions().getPresencePenalty()).isEqualTo(0);
+ assertThat(chatProperties.getOptions().getResponseFormat())
+ .isEqualTo(new QianFanApi.ChatCompletionRequest.ResponseFormat("json"));
+ assertThat(chatProperties.getOptions().getStop()).contains("boza", "koza");
+ assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55);
+ assertThat(chatProperties.getOptions().getTopP()).isEqualTo(0.56);
+ });
+ }
+
+ @Test
+ public void embeddingOptionsTest() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.qianfan.api-key=API_KEY",
+ "spring.ai.qianfan.secret-key=SECRET_KEY",
+ "spring.ai.qianfan.base-url=TEST_BASE_URL",
+
+ "spring.ai.qianfan.embedding.options.model=MODEL_XYZ",
+ "spring.ai.qianfan.embedding.options.encodingFormat=MyEncodingFormat"
+ )
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, QianFanAutoConfiguration.class))
+ .run(context -> {
+ var connectionProperties = context.getBean(QianFanConnectionProperties.class);
+ var embeddingProperties = context.getBean(QianFanEmbeddingProperties.class);
+
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+ assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY");
+ assertThat(connectionProperties.getSecretKey()).isEqualTo("SECRET_KEY");
+
+ assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ });
+ }
+
+ @Test
+ void embeddingActivation() {
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY",
+ "spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.qianfan.embedding.enabled=false")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, QianFanAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(QianFanEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(QianFanEmbeddingModel.class)).isEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY",
+ "spring.ai.qianfan.base-url=TEST_BASE_URL")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, QianFanAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(QianFanEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(QianFanEmbeddingModel.class)).isNotEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY",
+ "spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.qianfan.embedding.enabled=true")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, QianFanAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(QianFanEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(QianFanEmbeddingModel.class)).isNotEmpty();
+ });
+ }
+
+ @Test
+ void chatActivation() {
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY",
+ "spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.qianfan.chat.enabled=false")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, QianFanAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(QianFanChatProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(QianFanChatModel.class)).isEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY",
+ "spring.ai.qianfan.base-url=TEST_BASE_URL")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, QianFanAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(QianFanChatProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(QianFanChatModel.class)).isNotEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY",
+ "spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.qianfan.chat.enabled=true")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, QianFanAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(QianFanChatProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(QianFanChatModel.class)).isNotEmpty();
+ });
+
+ }
+
+ @Test
+ public void imageProperties() {
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.qianfan.base-url=TEST_BASE_URL",
+ "spring.ai.qianfan.api-key=abc123",
+ "spring.ai.qianfan.secret-key=def123",
+ "spring.ai.qianfan.image.options.model=MODEL_XYZ",
+ "spring.ai.qianfan.image.options.n=3")
+ // @formatter:on
+ .withConfiguration(
+ AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class,
+ WebClientAutoConfiguration.class, QianFanAutoConfiguration.class))
+ .run(context -> {
+ var imageProperties = context.getBean(QianFanImageProperties.class);
+ var connectionProperties = context.getBean(QianFanConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getSecretKey()).isEqualTo("def123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(imageProperties.getApiKey()).isNull();
+ assertThat(imageProperties.getBaseUrl()).isNull();
+
+ assertThat(imageProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(imageProperties.getOptions().getN()).isEqualTo(3);
+ });
+ }
+
+ @Test
+ public void imageOverrideConnectionProperties() {
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.qianfan.base-url=TEST_BASE_URL",
+ "spring.ai.qianfan.api-key=abc123",
+ "spring.ai.qianfan.secret-key=def123",
+ "spring.ai.qianfan.image.base-url=TEST_BASE_URL2",
+ "spring.ai.qianfan.image.api-key=456",
+ "spring.ai.qianfan.image.secret-key=def456",
+ "spring.ai.qianfan.image.options.model=MODEL_XYZ",
+ "spring.ai.qianfan.image.options.n=3")
+ // @formatter:on
+ .withConfiguration(
+ AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class,
+ WebClientAutoConfiguration.class, QianFanAutoConfiguration.class))
+ .run(context -> {
+ var imageProperties = context.getBean(QianFanImageProperties.class);
+ var connectionProperties = context.getBean(QianFanConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getSecretKey()).isEqualTo("def123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(imageProperties.getApiKey()).isEqualTo("456");
+ assertThat(imageProperties.getSecretKey()).isEqualTo("def456");
+ assertThat(imageProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2");
+
+ assertThat(imageProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(imageProperties.getOptions().getN()).isEqualTo(3);
+ });
+ }
+
+ @Test
+ public void imageOptionsTest() {
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.qianfan.api-key=API_KEY",
+ "spring.ai.qianfan.secret-key=SECRET_KEY",
+ "spring.ai.qianfan.base-url=TEST_BASE_URL",
+
+ "spring.ai.qianfan.image.options.n=3",
+ "spring.ai.qianfan.image.options.model=MODEL_XYZ",
+ "spring.ai.qianfan.image.options.size=1024x1024",
+ "spring.ai.qianfan.image.options.width=1024",
+ "spring.ai.qianfan.image.options.height=1024",
+ "spring.ai.qianfan.image.options.style=vivid",
+ "spring.ai.qianfan.image.options.user=userXYZ"
+ )
+ // @formatter:on
+ .withConfiguration(
+ AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class,
+ WebClientAutoConfiguration.class, QianFanAutoConfiguration.class))
+ .run(context -> {
+ var imageProperties = context.getBean(QianFanImageProperties.class);
+ var connectionProperties = context.getBean(QianFanConnectionProperties.class);
+
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+ assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY");
+ assertThat(connectionProperties.getSecretKey()).isEqualTo("SECRET_KEY");
+
+ assertThat(imageProperties.getOptions().getN()).isEqualTo(3);
+ assertThat(imageProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(imageProperties.getOptions().getSize()).isEqualTo("1024x1024");
+ assertThat(imageProperties.getOptions().getWidth()).isEqualTo(1024);
+ assertThat(imageProperties.getOptions().getHeight()).isEqualTo(1024);
+ assertThat(imageProperties.getOptions().getStyle()).isEqualTo("vivid");
+ assertThat(imageProperties.getOptions().getUser()).isEqualTo("userXYZ");
+ });
+ }
+
+ @Test
+ void imageActivation() {
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY",
+ "spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.qianfan.image.enabled=false")
+ .withConfiguration(
+ AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class,
+ WebClientAutoConfiguration.class, QianFanAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(QianFanImageProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(QianFanImageModel.class)).isEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY",
+ "spring.ai.qianfan.base-url=TEST_BASE_URL")
+ .withConfiguration(
+ AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class,
+ WebClientAutoConfiguration.class, QianFanAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(QianFanImageProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(QianFanImageModel.class)).isNotEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.qianfan.api-key=API_KEY", "spring.ai.qianfan.secret-key=SECRET_KEY",
+ "spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.qianfan.image.enabled=true")
+ .withConfiguration(
+ AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class,
+ WebClientAutoConfiguration.class, QianFanAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(QianFanImageProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(QianFanImageModel.class)).isNotEmpty();
+ });
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiAutoConfigurationIT.java
new file mode 100644
index 000000000..ad447cb4f
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiAutoConfigurationIT.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.stabilityai.autoconfigure;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+
+import org.springframework.ai.image.Image;
+import org.springframework.ai.image.ImageGeneration;
+import org.springframework.ai.image.ImageModel;
+import org.springframework.ai.image.ImagePrompt;
+import org.springframework.ai.image.ImageResponse;
+import org.springframework.ai.stabilityai.StyleEnum;
+import org.springframework.ai.stabilityai.api.StabilityAiImageOptions;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnabledIfEnvironmentVariable(named = "STABILITYAI_API_KEY", matches = ".*")
+public class StabilityAiAutoConfigurationIT {
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.stabilityai.image.api-key=" + System.getenv("STABILITYAI_API_KEY"))
+ .withConfiguration(AutoConfigurations.of(StabilityAiImageAutoConfiguration.class));
+
+ @Test
+ void generate() {
+ this.contextRunner.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/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImagePropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImagePropertiesTests.java
new file mode 100644
index 000000000..74e1624f9
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImagePropertiesTests.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.stabilityai.autoconfigure;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.stabilityai.StabilityAiImageModel;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Christian Tzolov
+ * @since 0.8.0
+ */
+public class StabilityAiImagePropertiesTests {
+
+ @Test
+ public void chatPropertiesTest() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.stabilityai.image.api-key=API_KEY",
+ "spring.ai.stabilityai.image.base-url=ENDPOINT",
+ "spring.ai.stabilityai.image.options.n=10",
+ "spring.ai.stabilityai.image.options.model=MODEL_XYZ",
+ "spring.ai.stabilityai.image.options.width=512",
+ "spring.ai.stabilityai.image.options.height=256",
+ "spring.ai.stabilityai.image.options.response-format=application/json",
+ "spring.ai.stabilityai.image.options.n=4",
+ "spring.ai.stabilityai.image.options.cfg-scale=7",
+ "spring.ai.stabilityai.image.options.clip-guidance-preset=SIMPLE",
+ "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"
+ )
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(StabilityAiImageAutoConfiguration.class))
+ .run(context -> {
+ var chatProperties = context.getBean(StabilityAiImageProperties.class);
+
+ assertThat(chatProperties.getBaseUrl()).isEqualTo("ENDPOINT");
+ assertThat(chatProperties.getApiKey()).isEqualTo("API_KEY");
+ assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+
+ assertThat(chatProperties.getOptions().getWidth()).isEqualTo(512);
+ assertThat(chatProperties.getOptions().getHeight()).isEqualTo(256);
+ assertThat(chatProperties.getOptions().getResponseFormat()).isEqualTo("application/json");
+ assertThat(chatProperties.getOptions().getN()).isEqualTo(4);
+ assertThat(chatProperties.getOptions().getCfgScale()).isEqualTo(7);
+ assertThat(chatProperties.getOptions().getClipGuidancePreset()).isEqualTo("SIMPLE");
+ assertThat(chatProperties.getOptions().getSampler()).isEqualTo("K_EULER");
+ assertThat(chatProperties.getOptions().getSeed()).isEqualTo(0);
+ assertThat(chatProperties.getOptions().getSteps()).isEqualTo(30);
+ assertThat(chatProperties.getOptions().getStylePreset()).isEqualTo("neon-punk");
+ });
+ }
+
+ @Test
+ void stabilityImageActivation() {
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.stabilityai.image.api-key=API_KEY",
+ "spring.ai.stabilityai.image.base-url=ENDPOINT", "spring.ai.stabilityai.image.enabled=false")
+ .withConfiguration(AutoConfigurations.of(StabilityAiImageAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(StabilityAiImageProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(StabilityAiImageModel.class)).isEmpty();
+
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.stabilityai.image.api-key=API_KEY",
+ "spring.ai.stabilityai.image.base-url=ENDPOINT")
+ .withConfiguration(AutoConfigurations.of(StabilityAiImageAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(StabilityAiImageProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(StabilityAiImageModel.class)).isNotEmpty();
+
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.stabilityai.image.api-key=API_KEY",
+ "spring.ai.stabilityai.image.base-url=ENDPOINT", "spring.ai.stabilityai.image.enabled=true")
+ .withConfiguration(AutoConfigurations.of(StabilityAiImageAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(StabilityAiImageProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(StabilityAiImageModel.class)).isNotEmpty();
+
+ });
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfigurationTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfigurationTests.java
new file mode 100644
index 000000000..f67bad99b
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfigurationTests.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright 2023-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.tool.autoconfigure;
+
+import java.util.function.Function;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.model.function.FunctionCallback;
+import org.springframework.ai.model.tool.DefaultToolCallingManager;
+import org.springframework.ai.model.tool.ToolCallingManager;
+import org.springframework.ai.tool.StaticToolCallbackProvider;
+import org.springframework.ai.tool.ToolCallback;
+import org.springframework.ai.tool.ToolCallbackProvider;
+import org.springframework.ai.tool.annotation.Tool;
+import org.springframework.ai.tool.definition.ToolDefinition;
+import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
+import org.springframework.ai.tool.execution.ToolExecutionExceptionProcessor;
+import org.springframework.ai.tool.function.FunctionToolCallback;
+import org.springframework.ai.tool.method.MethodToolCallback;
+import org.springframework.ai.tool.method.MethodToolCallbackProvider;
+import org.springframework.ai.tool.resolution.DelegatingToolCallbackResolver;
+import org.springframework.ai.tool.resolution.ToolCallbackResolver;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Description;
+import org.springframework.util.ReflectionUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit tests for {@link ToolCallingAutoConfiguration}.
+ *
+ * @author Thomas Vitale
+ * @author Christian Tzolov
+ */
+class ToolCallingAutoConfigurationTests {
+
+ @Test
+ void beansAreCreated() {
+ new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(ToolCallingAutoConfiguration.class))
+ .run(context -> {
+ var toolCallbackResolver = context.getBean(ToolCallbackResolver.class);
+ assertThat(toolCallbackResolver).isInstanceOf(DelegatingToolCallbackResolver.class);
+
+ var toolExecutionExceptionProcessor = context.getBean(ToolExecutionExceptionProcessor.class);
+ assertThat(toolExecutionExceptionProcessor).isInstanceOf(DefaultToolExecutionExceptionProcessor.class);
+
+ var toolCallingManager = context.getBean(ToolCallingManager.class);
+ assertThat(toolCallingManager).isInstanceOf(DefaultToolCallingManager.class);
+ });
+ }
+
+ @Test
+ void resolveMultipleFuncitonAndToolCallbacks() {
+ new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(ToolCallingAutoConfiguration.class))
+ .withUserConfiguration(Config.class)
+ .run(context -> {
+ var toolCallbackResolver = context.getBean(ToolCallbackResolver.class);
+ assertThat(toolCallbackResolver).isInstanceOf(DelegatingToolCallbackResolver.class);
+
+ assertThat(toolCallbackResolver.resolve("getForecast")).isNotNull();
+ assertThat(toolCallbackResolver.resolve("getForecast").getName()).isEqualTo("getForecast");
+
+ assertThat(toolCallbackResolver.resolve("getAlert")).isNotNull();
+ assertThat(toolCallbackResolver.resolve("getAlert").getName()).isEqualTo("getAlert");
+
+ assertThat(toolCallbackResolver.resolve("weatherFunction1")).isNotNull();
+ assertThat(toolCallbackResolver.resolve("weatherFunction1").getName()).isEqualTo("weatherFunction1");
+
+ assertThat(toolCallbackResolver.resolve("getCurrentWeather3")).isNotNull();
+ assertThat(toolCallbackResolver.resolve("getCurrentWeather3").getName())
+ .isEqualTo("getCurrentWeather3");
+
+ assertThat(toolCallbackResolver.resolve("getCurrentWeather4")).isNotNull();
+ assertThat(toolCallbackResolver.resolve("getCurrentWeather4").getName())
+ .isEqualTo("getCurrentWeather4");
+
+ assertThat(toolCallbackResolver.resolve("getCurrentWeather5")).isNotNull();
+ assertThat(toolCallbackResolver.resolve("getCurrentWeather5").getName())
+ .isEqualTo("getCurrentWeather5");
+ });
+ }
+
+ static class WeatherService {
+
+ @Tool(description = "Get the weather in location. Return temperature in 36°F or 36°C format.")
+ public String getForecast(String location) {
+ return "30";
+ }
+
+ @Tool(description = "Get the weather in location. Return temperature in 36°F or 36°C format.")
+ public String getForecast2(String location) {
+ return "30";
+ }
+
+ public String getAlert(String usState) {
+ return "Alert";
+ }
+
+ }
+
+ @Configuration
+ static class Config {
+
+ // Note: Currently we do not have ToolCallbackResolver implementation that can
+ // resolve the ToolCallback from the Tool annotation.
+ // Therefore we need to provide the ToolCallback instances explicitly using the
+ // ToolCallbacks.from(...) utility method.
+ @Bean
+ public ToolCallbackProvider toolCallbacks() {
+ return MethodToolCallbackProvider.builder().toolObjects(new WeatherService()).build();
+ }
+
+ public record Request(String location) {
+ }
+
+ public record Response(String temperature) {
+ }
+
+ @Bean
+ @Description("Get the weather in location. Return temperature in 36°F or 36°C format.")
+ public Function weatherFunction1() {
+ return request -> new Response("30");
+ }
+
+ @Bean
+ public FunctionCallback functionCallbacks3() {
+ return FunctionCallback.builder()
+ .function("getCurrentWeather3", (Request request) -> "15.0°C")
+ .description("Gets the weather in location")
+ .inputType(Request.class)
+ .build();
+ }
+
+ @Bean
+ public FunctionCallback functionCallbacks4() {
+ return FunctionCallback.builder()
+ .function("getCurrentWeather4", (Request request) -> "15.0°C")
+ .description("Gets the weather in location")
+ .inputType(Request.class)
+ .build();
+
+ }
+
+ @Bean
+ public ToolCallback toolCallbacks5() {
+ return FunctionToolCallback.builder("getCurrentWeather5", (Request request) -> "15.0°C")
+ .description("Gets the weather in location")
+ .inputType(Request.class)
+ .build();
+
+ }
+
+ @Bean
+ public ToolCallbackProvider blabla() {
+ return new StaticToolCallbackProvider(
+ FunctionToolCallback.builder("getCurrentWeather5", (Request request) -> "15.0°C")
+ .description("Gets the weather in location")
+ .inputType(Request.class)
+ .build());
+
+ }
+
+ @Bean
+ public ToolCallback toolCallbacks6() {
+ var toolMethod = ReflectionUtils.findMethod(WeatherService.class, "getAlert", String.class);
+ return MethodToolCallback.builder()
+ .toolDefinition(ToolDefinition.builder(toolMethod).build())
+ .toolMethod(toolMethod)
+ .toolObject(new WeatherService())
+ .build();
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelAutoConfigurationIT.java
new file mode 100644
index 000000000..50b60c35d
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelAutoConfigurationIT.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.transformers.autoconfigure;
+
+import java.io.File;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import org.springframework.ai.embedding.EmbeddingModel;
+import org.springframework.ai.transformers.TransformersEmbeddingModel;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Christian Tzolov
+ */
+public class TransformersEmbeddingModelAutoConfigurationIT {
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(TransformersEmbeddingModelAutoConfiguration.class));
+
+ @TempDir
+ File tempDir;
+
+ @Test
+ public void embedding() {
+ this.contextRunner.run(context -> {
+ var properties = context.getBean(TransformersEmbeddingModelProperties.class);
+ assertThat(properties.getCache().isEnabled()).isTrue();
+ assertThat(properties.getCache().getDirectory()).isEqualTo(
+ new File(System.getProperty("java.io.tmpdir"), "spring-ai-onnx-generative").getAbsolutePath());
+
+ EmbeddingModel embeddingModel = context.getBean(EmbeddingModel.class);
+ assertThat(embeddingModel).isInstanceOf(TransformersEmbeddingModel.class);
+
+ List embeddings = embeddingModel.embed(List.of("Spring Framework", "Spring AI"));
+
+ assertThat(embeddings.size()).isEqualTo(2); // batch size
+ assertThat(embeddings.get(0).length).isEqualTo(embeddingModel.dimensions()); // dimensions
+ // size
+ });
+ }
+
+ @Test
+ public void remoteOnnxModel() {
+ // https://huggingface.co/intfloat/e5-small-v2
+ this.contextRunner.withPropertyValues(
+ "spring.ai.embedding.transformer.cache.directory=" + this.tempDir.getAbsolutePath(),
+ "spring.ai.embedding.transformer.onnx.modelUri=https://huggingface.co/intfloat/e5-small-v2/resolve/main/model.onnx",
+ "spring.ai.embedding.transformer.tokenizer.uri=https://huggingface.co/intfloat/e5-small-v2/raw/main/tokenizer.json")
+ .run(context -> {
+ var properties = context.getBean(TransformersEmbeddingModelProperties.class);
+ assertThat(properties.getOnnx().getModelUri())
+ .isEqualTo("https://huggingface.co/intfloat/e5-small-v2/resolve/main/model.onnx");
+ assertThat(properties.getTokenizer().getUri())
+ .isEqualTo("https://huggingface.co/intfloat/e5-small-v2/raw/main/tokenizer.json");
+
+ assertThat(properties.getCache().isEnabled()).isTrue();
+ assertThat(properties.getCache().getDirectory()).isEqualTo(this.tempDir.getAbsolutePath());
+ assertThat(this.tempDir.listFiles()).hasSize(2);
+
+ EmbeddingModel embeddingModel = context.getBean(EmbeddingModel.class);
+ assertThat(embeddingModel).isInstanceOf(TransformersEmbeddingModel.class);
+
+ assertThat(embeddingModel.dimensions()).isEqualTo(384);
+
+ List embeddings = embeddingModel.embed(List.of("Spring Framework", "Spring AI"));
+
+ assertThat(embeddings.size()).isEqualTo(2); // batch size
+ assertThat(embeddings.get(0).length).isEqualTo(embeddingModel.dimensions()); // dimensions
+ // size
+ });
+ }
+
+ @Test
+ void embeddingActivation() {
+ this.contextRunner.withPropertyValues("spring.ai.embedding.transformer.enabled=false").run(context -> {
+ assertThat(context.getBeansOfType(TransformersEmbeddingModelProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(TransformersEmbeddingModel.class)).isEmpty();
+ });
+
+ this.contextRunner.withPropertyValues("spring.ai.embedding.transformer.enabled=true").run(context -> {
+ assertThat(context.getBeansOfType(TransformersEmbeddingModelProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(TransformersEmbeddingModel.class)).isNotEmpty();
+ });
+
+ this.contextRunner.run(context -> {
+ assertThat(context.getBeansOfType(TransformersEmbeddingModelProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(TransformersEmbeddingModel.class)).isNotEmpty();
+ });
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiTextEmbeddingModelAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiTextEmbeddingModelAutoConfigurationIT.java
new file mode 100644
index 000000000..c1ed7eaf3
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiTextEmbeddingModelAutoConfigurationIT.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.vertexai.autoconfigure.embedding;
+
+import java.io.File;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.junit.jupiter.api.io.TempDir;
+
+import org.springframework.ai.document.Document;
+import org.springframework.ai.embedding.DocumentEmbeddingRequest;
+import org.springframework.ai.embedding.EmbeddingOptionsBuilder;
+import org.springframework.ai.embedding.EmbeddingResponse;
+import org.springframework.ai.embedding.EmbeddingResultMetadata;
+import org.springframework.ai.vertexai.embedding.multimodal.VertexAiMultimodalEmbeddingModel;
+import org.springframework.ai.vertexai.embedding.text.VertexAiTextEmbeddingModel;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Christian Tzolov
+ */
+@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_PROJECT_ID", matches = ".*")
+@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_LOCATION", matches = ".*")
+public class VertexAiTextEmbeddingModelAutoConfigurationIT {
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.vertex.ai.embedding.project-id=" + System.getenv("VERTEX_AI_GEMINI_PROJECT_ID"),
+ "spring.ai.vertex.ai.embedding.location=" + System.getenv("VERTEX_AI_GEMINI_LOCATION"))
+ .withConfiguration(AutoConfigurations.of(VertexAiEmbeddingAutoConfiguration.class));
+
+ @TempDir
+ File tempDir;
+
+ @Test
+ public void textEmbedding() {
+ this.contextRunner.run(context -> {
+ var conntectionProperties = context.getBean(VertexAiEmbeddingConnectionProperties.class);
+ var textEmbeddingProperties = context.getBean(VertexAiTextEmbeddingProperties.class);
+
+ assertThat(conntectionProperties).isNotNull();
+ assertThat(textEmbeddingProperties.isEnabled()).isTrue();
+
+ VertexAiTextEmbeddingModel embeddingModel = context.getBean(VertexAiTextEmbeddingModel.class);
+ assertThat(embeddingModel).isInstanceOf(VertexAiTextEmbeddingModel.class);
+
+ List embeddings = embeddingModel.embed(List.of("Spring Framework", "Spring AI"));
+
+ assertThat(embeddings.size()).isEqualTo(2); // batch size
+ assertThat(embeddings.get(0).length).isEqualTo(embeddingModel.dimensions());
+ });
+ }
+
+ @Test
+ void textEmbeddingActivation() {
+ this.contextRunner.withPropertyValues("spring.ai.vertex.ai.embedding.text.enabled=false").run(context -> {
+ assertThat(context.getBeansOfType(VertexAiTextEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(VertexAiTextEmbeddingModel.class)).isEmpty();
+ });
+
+ this.contextRunner.withPropertyValues("spring.ai.vertex.ai.embedding.text.enabled=true").run(context -> {
+ assertThat(context.getBeansOfType(VertexAiTextEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(VertexAiTextEmbeddingModel.class)).isNotEmpty();
+ });
+
+ this.contextRunner.run(context -> {
+ assertThat(context.getBeansOfType(VertexAiTextEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(VertexAiTextEmbeddingModel.class)).isNotEmpty();
+ });
+
+ }
+
+ @Test
+ public void multimodalEmbedding() {
+ this.contextRunner.run(context -> {
+ var conntectionProperties = context.getBean(VertexAiEmbeddingConnectionProperties.class);
+ var multimodalEmbeddingProperties = context.getBean(VertexAiMultimodalEmbeddingProperties.class);
+
+ assertThat(conntectionProperties).isNotNull();
+ assertThat(multimodalEmbeddingProperties.isEnabled()).isTrue();
+
+ VertexAiMultimodalEmbeddingModel multiModelEmbeddingModel = context
+ .getBean(VertexAiMultimodalEmbeddingModel.class);
+
+ assertThat(multiModelEmbeddingModel).isNotNull();
+
+ var document = new Document("Hello World");
+
+ DocumentEmbeddingRequest embeddingRequest = new DocumentEmbeddingRequest(List.of(document),
+ EmbeddingOptionsBuilder.builder().build());
+
+ EmbeddingResponse embeddingResponse = multiModelEmbeddingModel.call(embeddingRequest);
+ assertThat(embeddingResponse.getResults()).hasSize(1);
+ assertThat(embeddingResponse.getResults().get(0)).isNotNull();
+ assertThat(embeddingResponse.getResults().get(0).getMetadata().getModalityType())
+ .isEqualTo(EmbeddingResultMetadata.ModalityType.TEXT);
+ assertThat(embeddingResponse.getResults().get(0).getOutput()).hasSize(1408);
+
+ assertThat(embeddingResponse.getMetadata().getModel()).isEqualTo("multimodalembedding@001");
+ assertThat(embeddingResponse.getMetadata().getUsage().getPromptTokens()).isEqualTo(0);
+
+ assertThat(multiModelEmbeddingModel.dimensions()).isEqualTo(1408);
+
+ });
+ }
+
+ @Test
+ void multimodalEmbeddingActivation() {
+ this.contextRunner.withPropertyValues("spring.ai.vertex.ai.embedding.multimodal.enabled=false").run(context -> {
+ assertThat(context.getBeansOfType(VertexAiMultimodalEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(VertexAiMultimodalEmbeddingModel.class)).isEmpty();
+ });
+
+ this.contextRunner.withPropertyValues("spring.ai.vertex.ai.embedding.multimodal.enabled=true").run(context -> {
+ assertThat(context.getBeansOfType(VertexAiMultimodalEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(VertexAiMultimodalEmbeddingModel.class)).isNotEmpty();
+ });
+
+ this.contextRunner.run(context -> {
+ assertThat(context.getBeansOfType(VertexAiMultimodalEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(VertexAiMultimodalEmbeddingModel.class)).isNotEmpty();
+ });
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiAutoConfigurationIT.java
new file mode 100644
index 000000000..a6cdfb3dd
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiAutoConfigurationIT.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.vertexai.autoconfigure.gemini;
+
+import java.util.stream.Collectors;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_PROJECT_ID", matches = ".*")
+@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_LOCATION", matches = ".*")
+public class VertexAiGeminiAutoConfigurationIT {
+
+ private static final Log logger = LogFactory.getLog(VertexAiGeminiAutoConfigurationIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.vertex.ai.gemini.project-id=" + System.getenv("VERTEX_AI_GEMINI_PROJECT_ID"),
+ "spring.ai.vertex.ai.gemini.location=" + System.getenv("VERTEX_AI_GEMINI_LOCATION"))
+ .withConfiguration(AutoConfigurations.of(VertexAiGeminiAutoConfiguration.class));
+
+ @Test
+ void generate() {
+ this.contextRunner.run(context -> {
+ VertexAiGeminiChatModel chatModel = context.getBean(VertexAiGeminiChatModel.class);
+ String response = chatModel.call("Hello");
+ assertThat(response).isNotEmpty();
+ logger.info("Response: " + response);
+ });
+ }
+
+ @Test
+ void generateStreaming() {
+ this.contextRunner.run(context -> {
+ VertexAiGeminiChatModel chatModel = context.getBean(VertexAiGeminiChatModel.class);
+ Flux responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello")));
+ String response = responseFlux.collectList()
+ .block()
+ .stream()
+ .map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText())
+ .collect(Collectors.joining());
+
+ assertThat(response).isNotEmpty();
+ logger.info("Response: " + response);
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/gemini/tool/FunctionCallWithFunctionBeanIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithFunctionBeanIT.java
similarity index 96%
rename from spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/gemini/tool/FunctionCallWithFunctionBeanIT.java
rename to spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithFunctionBeanIT.java
index c02e2b22a..364c46635 100644
--- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/gemini/tool/FunctionCallWithFunctionBeanIT.java
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithFunctionBeanIT.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package org.springframework.ai.autoconfigure.vertexai.gemini.tool;
+package org.springframework.ai.model.vertexai.autoconfigure.gemini.tool;
import java.util.List;
import java.util.Set;
@@ -25,7 +25,7 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.springframework.ai.autoconfigure.vertexai.gemini.VertexAiGeminiAutoConfiguration;
+import org.springframework.ai.model.vertexai.autoconfigure.gemini.VertexAiGeminiAutoConfiguration;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithFunctionWrapperIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithFunctionWrapperIT.java
new file mode 100644
index 000000000..32cc29327
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithFunctionWrapperIT.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.vertexai.autoconfigure.gemini.tool;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.ai.model.vertexai.autoconfigure.gemini.VertexAiGeminiAutoConfiguration;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.tool.ToolCallback;
+import org.springframework.ai.tool.function.FunctionToolCallback;
+import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel;
+import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatOptions;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_PROJECT_ID", matches = ".*")
+@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_LOCATION", matches = ".*")
+public class FunctionCallWithFunctionWrapperIT {
+
+ private final Logger logger = LoggerFactory.getLogger(FunctionCallWithFunctionWrapperIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.vertex.ai.gemini.project-id=" + System.getenv("VERTEX_AI_GEMINI_PROJECT_ID"),
+ "spring.ai.vertex.ai.gemini.location=" + System.getenv("VERTEX_AI_GEMINI_LOCATION"))
+ .withConfiguration(AutoConfigurations.of(VertexAiGeminiAutoConfiguration.class))
+ .withUserConfiguration(Config.class);
+
+ @Test
+ void functionCallTest() {
+ this.contextRunner
+ .withPropertyValues("spring.ai.vertex.ai.gemini.chat.options.model="
+ + VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH.getValue())
+ .run(context -> {
+
+ VertexAiGeminiChatModel chatModel = context.getBean(VertexAiGeminiChatModel.class);
+
+ var userMessage = new UserMessage("""
+ What's the weather like in San Francisco, Paris and in Tokyo?
+ Return the temperature in Celsius.
+ """);
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
+ VertexAiGeminiChatOptions.builder().toolName("WeatherInfo").build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+ });
+ }
+
+ @Configuration
+ static class Config {
+
+ @Bean
+ public ToolCallback weatherFunctionInfo() {
+
+ return FunctionToolCallback.builder("WeatherInfo", new MockWeatherService())
+ .description("Get the current weather in a given location")
+ .inputType(MockWeatherService.Request.class)
+ .build();
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithPromptFunctionIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithPromptFunctionIT.java
new file mode 100644
index 000000000..3d9d04a86
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/FunctionCallWithPromptFunctionIT.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.vertexai.autoconfigure.gemini.tool;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.ai.model.vertexai.autoconfigure.gemini.VertexAiGeminiAutoConfiguration;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.tool.function.FunctionToolCallback;
+import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatModel;
+import org.springframework.ai.vertexai.gemini.VertexAiGeminiChatOptions;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_PROJECT_ID", matches = ".*")
+@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_LOCATION", matches = ".*")
+public class FunctionCallWithPromptFunctionIT {
+
+ private final Logger logger = LoggerFactory.getLogger(FunctionCallWithPromptFunctionIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.vertex.ai.gemini.project-id=" + System.getenv("VERTEX_AI_GEMINI_PROJECT_ID"),
+ "spring.ai.vertex.ai.gemini.location=" + System.getenv("VERTEX_AI_GEMINI_LOCATION"))
+ .withConfiguration(AutoConfigurations.of(VertexAiGeminiAutoConfiguration.class));
+
+ @Test
+ void functionCallTest() {
+ this.contextRunner
+ .withPropertyValues("spring.ai.vertex.ai.gemini.chat.options.model="
+ + VertexAiGeminiChatModel.ChatModel.GEMINI_2_0_FLASH_LIGHT.getValue())
+ .run(context -> {
+
+ VertexAiGeminiChatModel chatModel = context.getBean(VertexAiGeminiChatModel.class);
+
+ // var systemMessage = new SystemMessage("""
+ // Use Multi-turn function calling.
+ // Answer for all listed locations.
+ // If the information was not fetched call the function again. Repeat at
+ // most 3 times.
+ // """);
+ var userMessage = new UserMessage("""
+ What's the weather like in San Francisco, Paris and in Tokyo?
+ Return the temperature in Celsius.
+ """);
+
+ var promptOptions = VertexAiGeminiChatOptions.builder()
+ .toolCallbacks(
+ List.of(FunctionToolCallback.builder("CurrentWeatherService", new MockWeatherService())
+ .description("Get the weather in location")
+ .inputType(MockWeatherService.Request.class)
+ .build()))
+ .build();
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), promptOptions));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+
+ // Verify that no function call is made.
+ response = chatModel
+ .call(new Prompt(List.of(userMessage), VertexAiGeminiChatOptions.builder().build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).doesNotContain("30", "10", "15");
+
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/MockWeatherService.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/MockWeatherService.java
new file mode 100644
index 000000000..af856fdd0
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/tool/MockWeatherService.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.vertexai.autoconfigure.gemini.tool;
+
+import java.util.function.Function;
+
+import com.fasterxml.jackson.annotation.JsonClassDescription;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyDescription;
+
+/**
+ * Mock 3rd party weather service.
+ *
+ * @author Christian Tzolov
+ */
+@JsonClassDescription("Get the weather in location")
+public class MockWeatherService implements Function {
+
+ @Override
+ public Response apply(Request request) {
+
+ double temperature = 0;
+ if (request.location().contains("Paris")) {
+ temperature = 15;
+ }
+ else if (request.location().contains("Tokyo")) {
+ temperature = 10;
+ }
+ else if (request.location().contains("San Francisco")) {
+ temperature = 30;
+ }
+
+ return new Response(temperature, 15, 20, 2, 53, 45, Unit.C);
+ }
+
+ /**
+ * Temperature units.
+ */
+ public enum Unit {
+
+ /**
+ * Celsius.
+ */
+ C("metric"),
+ /**
+ * Fahrenheit.
+ */
+ F("imperial");
+
+ /**
+ * Human readable unit name.
+ */
+ public final String unitName;
+
+ Unit(String text) {
+ this.unitName = text;
+ }
+
+ }
+
+ /**
+ * Weather Function request.
+ */
+ @JsonInclude(Include.NON_NULL)
+ @JsonClassDescription("Weather API request")
+ public record Request(@JsonProperty(required = true,
+ value = "location") @JsonPropertyDescription("The city and state e.g. San Francisco, CA") String location,
+ @JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) {
+
+ }
+
+ /**
+ * Weather Function response.
+ */
+ public record Response(double temp, double feels_like, double temp_min, double temp_max, int pressure, int humidity,
+ Unit unit) {
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiAutoConfigurationTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiAutoConfigurationTests.java
new file mode 100644
index 000000000..86f6fca45
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiAutoConfigurationTests.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.watsonxai.autoconfigure;
+
+import org.junit.Test;
+
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class WatsonxAiAutoConfigurationTests {
+
+ @Test
+ public void propertiesTest() {
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.watsonx.ai.base-url=TEST_BASE_URL",
+ "spring.ai.watsonx.ai.stream-endpoint=ml/v1/text/generation_stream?version=2023-05-29",
+ "spring.ai.watsonx.ai.text-endpoint=ml/v1/text/generation?version=2023-05-29",
+ "spring.ai.watsonx.ai.embedding-endpoint=ml/v1/text/embeddings?version=2023-05-29",
+ "spring.ai.watsonx.ai.projectId=1",
+ "spring.ai.watsonx.ai.IAMToken=123456")
+ // @formatter:on
+ .withConfiguration(
+ AutoConfigurations.of(RestClientAutoConfiguration.class, WatsonxAiAutoConfiguration.class))
+ .run(context -> {
+ var connectionProperties = context.getBean(WatsonxAiConnectionProperties.class);
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+ assertThat(connectionProperties.getStreamEndpoint())
+ .isEqualTo("ml/v1/text/generation_stream?version=2023-05-29");
+ assertThat(connectionProperties.getTextEndpoint())
+ .isEqualTo("ml/v1/text/generation?version=2023-05-29");
+ assertThat(connectionProperties.getEmbeddingEndpoint())
+ .isEqualTo("ml/v1/text/embeddings?version=2023-05-29");
+ assertThat(connectionProperties.getProjectId()).isEqualTo("1");
+ assertThat(connectionProperties.getIAMToken()).isEqualTo("123456");
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfigurationIT.java
new file mode 100644
index 000000000..d36e0a691
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfigurationIT.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.zhipuai.autoconfigure;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.embedding.EmbeddingResponse;
+import org.springframework.ai.image.ImagePrompt;
+import org.springframework.ai.image.ImageResponse;
+import org.springframework.ai.zhipuai.ZhiPuAiChatModel;
+import org.springframework.ai.zhipuai.ZhiPuAiEmbeddingModel;
+import org.springframework.ai.zhipuai.ZhiPuAiImageModel;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Geng Rong
+ */
+@EnabledIfEnvironmentVariable(named = "ZHIPU_AI_API_KEY", matches = ".*")
+public class ZhiPuAiAutoConfigurationIT {
+
+ private static final Log logger = LogFactory.getLog(ZhiPuAiAutoConfigurationIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.zhipuai.apiKey=" + System.getenv("ZHIPU_AI_API_KEY"))
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class));
+
+ @Test
+ void generate() {
+ this.contextRunner.run(context -> {
+ ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.class);
+ String response = chatModel.call("Hello");
+ assertThat(response).isNotEmpty();
+ logger.info("Response: " + response);
+ });
+ }
+
+ @Test
+ void generateStreaming() {
+ this.contextRunner.run(context -> {
+ ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.class);
+ Flux responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello")));
+ String response = responseFlux.collectList()
+ .block()
+ .stream()
+ .map(chatResponse -> chatResponse.getResults().get(0).getOutput().getText())
+ .collect(Collectors.joining());
+
+ assertThat(response).isNotEmpty();
+ logger.info("Response: " + response);
+ });
+ }
+
+ @Test
+ void embedding() {
+ this.contextRunner.run(context -> {
+ ZhiPuAiEmbeddingModel embeddingModel = context.getBean(ZhiPuAiEmbeddingModel.class);
+
+ EmbeddingResponse embeddingResponse = embeddingModel
+ .embedForResponse(List.of("Hello World", "World is big and salvation is near"));
+ assertThat(embeddingResponse.getResults()).hasSize(2);
+ assertThat(embeddingResponse.getResults().get(0).getOutput()).isNotEmpty();
+ assertThat(embeddingResponse.getResults().get(0).getIndex()).isEqualTo(0);
+ assertThat(embeddingResponse.getResults().get(1).getOutput()).isNotEmpty();
+ assertThat(embeddingResponse.getResults().get(1).getIndex()).isEqualTo(1);
+
+ assertThat(embeddingModel.dimensions()).isEqualTo(1024);
+ });
+ }
+
+ @Test
+ void generateImage() {
+ this.contextRunner.withPropertyValues("spring.ai.zhipuai.image.options.size=1024x1024").run(context -> {
+ ZhiPuAiImageModel ImageModel = context.getBean(ZhiPuAiImageModel.class);
+ ImageResponse imageResponse = ImageModel.call(new ImagePrompt("forest"));
+ assertThat(imageResponse.getResults()).hasSize(1);
+ assertThat(imageResponse.getResult().getOutput().getUrl()).isNotEmpty();
+ logger.info("Generated image: " + imageResponse.getResult().getOutput().getUrl());
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiPropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiPropertiesTests.java
new file mode 100644
index 000000000..ec1b9387a
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiPropertiesTests.java
@@ -0,0 +1,432 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.zhipuai.autoconfigure;
+
+import org.junit.jupiter.api.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.skyscreamer.jsonassert.JSONCompareMode;
+
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.model.ModelOptionsUtils;
+import org.springframework.ai.zhipuai.ZhiPuAiChatModel;
+import org.springframework.ai.zhipuai.ZhiPuAiEmbeddingModel;
+import org.springframework.ai.zhipuai.ZhiPuAiImageModel;
+import org.springframework.ai.zhipuai.api.ZhiPuAiApi;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit Tests for {@link ZhiPuAiConnectionProperties}, {@link ZhiPuAiChatProperties} and
+ * {@link ZhiPuAiEmbeddingProperties}.
+ *
+ * @author Geng Rong
+ */
+public class ZhiPuAiPropertiesTests {
+
+ @Test
+ public void chatProperties() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.zhipuai.base-url=TEST_BASE_URL",
+ "spring.ai.zhipuai.api-key=abc123",
+ "spring.ai.zhipuai.chat.options.model=MODEL_XYZ",
+ "spring.ai.zhipuai.chat.options.temperature=0.55")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class))
+ .run(context -> {
+ var chatProperties = context.getBean(ZhiPuAiChatProperties.class);
+ var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(chatProperties.getApiKey()).isNull();
+ assertThat(chatProperties.getBaseUrl()).isNull();
+
+ assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55);
+ });
+ }
+
+ @Test
+ public void chatOverrideConnectionProperties() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.zhipuai.base-url=TEST_BASE_URL",
+ "spring.ai.zhipuai.api-key=abc123",
+ "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")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class))
+ .run(context -> {
+ var chatProperties = context.getBean(ZhiPuAiChatProperties.class);
+ var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(chatProperties.getApiKey()).isEqualTo("456");
+ assertThat(chatProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2");
+
+ assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55);
+ });
+ }
+
+ @Test
+ public void embeddingProperties() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.zhipuai.base-url=TEST_BASE_URL",
+ "spring.ai.zhipuai.api-key=abc123",
+ "spring.ai.zhipuai.embedding.options.model=MODEL_XYZ")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class))
+ .run(context -> {
+ var embeddingProperties = context.getBean(ZhiPuAiEmbeddingProperties.class);
+ var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(embeddingProperties.getApiKey()).isNull();
+ assertThat(embeddingProperties.getBaseUrl()).isNull();
+
+ assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ });
+ }
+
+ @Test
+ public void embeddingOverrideConnectionProperties() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.zhipuai.base-url=TEST_BASE_URL",
+ "spring.ai.zhipuai.api-key=abc123",
+ "spring.ai.zhipuai.embedding.base-url=TEST_BASE_URL2",
+ "spring.ai.zhipuai.embedding.api-key=456",
+ "spring.ai.zhipuai.embedding.options.model=MODEL_XYZ")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class))
+ .run(context -> {
+ var embeddingProperties = context.getBean(ZhiPuAiEmbeddingProperties.class);
+ var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(embeddingProperties.getApiKey()).isEqualTo("456");
+ assertThat(embeddingProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2");
+
+ assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ });
+ }
+
+ @Test
+ public void imageProperties() {
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.zhipuai.base-url=TEST_BASE_URL",
+ "spring.ai.zhipuai.api-key=abc123",
+ "spring.ai.zhipuai.image.options.model=MODEL_XYZ")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class))
+ .run(context -> {
+ var imageProperties = context.getBean(ZhiPuAiImageProperties.class);
+ var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(imageProperties.getApiKey()).isNull();
+ assertThat(imageProperties.getBaseUrl()).isNull();
+
+ assertThat(imageProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ });
+ }
+
+ @Test
+ public void imageOverrideConnectionProperties() {
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.zhipuai.base-url=TEST_BASE_URL",
+ "spring.ai.zhipuai.api-key=abc123",
+ "spring.ai.zhipuai.image.base-url=TEST_BASE_URL2",
+ "spring.ai.zhipuai.image.api-key=456",
+ "spring.ai.zhipuai.image.options.model=MODEL_XYZ")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class))
+ .run(context -> {
+ var imageProperties = context.getBean(ZhiPuAiImageProperties.class);
+ var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class);
+
+ assertThat(connectionProperties.getApiKey()).isEqualTo("abc123");
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+
+ assertThat(imageProperties.getApiKey()).isEqualTo("456");
+ assertThat(imageProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL2");
+
+ assertThat(imageProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ });
+ }
+
+ @Test
+ public void chatOptionsTest() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.zhipuai.api-key=API_KEY",
+ "spring.ai.zhipuai.base-url=TEST_BASE_URL",
+
+ "spring.ai.zhipuai.chat.options.model=MODEL_XYZ",
+ "spring.ai.zhipuai.chat.options.maxTokens=123",
+ "spring.ai.zhipuai.chat.options.stop=boza,koza",
+ "spring.ai.zhipuai.chat.options.temperature=0.55",
+ "spring.ai.zhipuai.chat.options.topP=0.56",
+ "spring.ai.zhipuai.chat.options.requestId=RequestId",
+ "spring.ai.zhipuai.chat.options.doSample=true",
+
+ // "spring.ai.zhipuai.chat.options.toolChoice.functionName=toolChoiceFunctionName",
+ "spring.ai.zhipuai.chat.options.toolChoice=" + ModelOptionsUtils.toJsonString(ZhiPuAiApi.ChatCompletionRequest.ToolChoiceBuilder.function("toolChoiceFunctionName")),
+
+ "spring.ai.zhipuai.chat.options.tools[0].function.name=myFunction1",
+ "spring.ai.zhipuai.chat.options.tools[0].function.description=function description",
+ "spring.ai.zhipuai.chat.options.tools[0].function.jsonSchema=" + """
+ {
+ "type": "object",
+ "properties": {
+ "location": {
+ "type": "string",
+ "description": "The city and state e.g. San Francisco, CA"
+ },
+ "lat": {
+ "type": "number",
+ "description": "The city latitude"
+ },
+ "lon": {
+ "type": "number",
+ "description": "The city longitude"
+ },
+ "unit": {
+ "type": "string",
+ "enum": ["c", "f"]
+ }
+ },
+ "required": ["location", "lat", "lon", "unit"]
+ }
+ """,
+ "spring.ai.zhipuai.chat.options.user=userXYZ"
+ )
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class))
+ .run(context -> {
+ var chatProperties = context.getBean(ZhiPuAiChatProperties.class);
+ var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class);
+ var embeddingProperties = context.getBean(ZhiPuAiEmbeddingProperties.class);
+
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+ assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY");
+
+ assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("Embedding-2");
+
+ assertThat(chatProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(chatProperties.getOptions().getMaxTokens()).isEqualTo(123);
+ assertThat(chatProperties.getOptions().getStop()).contains("boza", "koza");
+ assertThat(chatProperties.getOptions().getTemperature()).isEqualTo(0.55);
+ assertThat(chatProperties.getOptions().getTopP()).isEqualTo(0.56);
+ assertThat(chatProperties.getOptions().getRequestId()).isEqualTo("RequestId");
+ assertThat(chatProperties.getOptions().getDoSample()).isEqualTo(Boolean.TRUE);
+
+ JSONAssert.assertEquals("{\"type\":\"function\",\"function\":{\"name\":\"toolChoiceFunctionName\"}}",
+ chatProperties.getOptions().getToolChoice(), JSONCompareMode.LENIENT);
+
+ assertThat(chatProperties.getOptions().getUser()).isEqualTo("userXYZ");
+
+ assertThat(chatProperties.getOptions().getTools()).hasSize(1);
+ var tool = chatProperties.getOptions().getTools().get(0);
+ assertThat(tool.getType()).isEqualTo(ZhiPuAiApi.FunctionTool.Type.FUNCTION);
+ var function = tool.getFunction();
+ assertThat(function.getName()).isEqualTo("myFunction1");
+ assertThat(function.getDescription()).isEqualTo("function description");
+ assertThat(function.getParameters()).isNotEmpty();
+ });
+ }
+
+ @Test
+ public void embeddingOptionsTest() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.zhipuai.api-key=API_KEY",
+ "spring.ai.zhipuai.base-url=TEST_BASE_URL",
+
+ "spring.ai.zhipuai.embedding.options.model=MODEL_XYZ",
+ "spring.ai.zhipuai.embedding.options.encodingFormat=MyEncodingFormat",
+ "spring.ai.zhipuai.embedding.options.user=userXYZ"
+ )
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class))
+ .run(context -> {
+ var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class);
+ var embeddingProperties = context.getBean(ZhiPuAiEmbeddingProperties.class);
+
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+ assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY");
+
+ assertThat(embeddingProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ });
+ }
+
+ @Test
+ public void imageOptionsTest() {
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.zhipuai.api-key=API_KEY",
+ "spring.ai.zhipuai.base-url=TEST_BASE_URL",
+ "spring.ai.zhipuai.image.options.model=MODEL_XYZ",
+ "spring.ai.zhipuai.image.options.user=userXYZ"
+ )
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class))
+ .run(context -> {
+ var imageProperties = context.getBean(ZhiPuAiImageProperties.class);
+ var connectionProperties = context.getBean(ZhiPuAiConnectionProperties.class);
+
+ assertThat(connectionProperties.getBaseUrl()).isEqualTo("TEST_BASE_URL");
+ assertThat(connectionProperties.getApiKey()).isEqualTo("API_KEY");
+ assertThat(imageProperties.getOptions().getModel()).isEqualTo("MODEL_XYZ");
+ assertThat(imageProperties.getOptions().getUser()).isEqualTo("userXYZ");
+ });
+ }
+
+ @Test
+ void embeddingActivation() {
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL",
+ "spring.ai.zhipuai.embedding.enabled=false")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(ZhiPuAiEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(ZhiPuAiEmbeddingModel.class)).isEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(ZhiPuAiEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(ZhiPuAiEmbeddingModel.class)).isNotEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL",
+ "spring.ai.zhipuai.embedding.enabled=true")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(ZhiPuAiEmbeddingProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(ZhiPuAiEmbeddingModel.class)).isNotEmpty();
+ });
+ }
+
+ @Test
+ void chatActivation() {
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL",
+ "spring.ai.zhipuai.chat.enabled=false")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(ZhiPuAiChatProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(ZhiPuAiChatModel.class)).isEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(ZhiPuAiChatProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(ZhiPuAiChatModel.class)).isNotEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL",
+ "spring.ai.zhipuai.chat.enabled=true")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(ZhiPuAiChatProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(ZhiPuAiChatModel.class)).isNotEmpty();
+ });
+
+ }
+
+ @Test
+ void imageActivation() {
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL",
+ "spring.ai.zhipuai.image.enabled=false")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(ZhiPuAiImageProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(ZhiPuAiImageModel.class)).isEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(ZhiPuAiImageProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(ZhiPuAiImageModel.class)).isNotEmpty();
+ });
+
+ new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL",
+ "spring.ai.zhipuai.image.enabled=true")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class))
+ .run(context -> {
+ assertThat(context.getBeansOfType(ZhiPuAiImageProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(ZhiPuAiImageModel.class)).isNotEmpty();
+ });
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackInPromptIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackInPromptIT.java
new file mode 100644
index 000000000..12dc83d6d
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackInPromptIT.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.zhipuai.autoconfigure.tool;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiAutoConfiguration;
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.model.Generation;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.model.function.FunctionCallback;
+import org.springframework.ai.zhipuai.ZhiPuAiChatModel;
+import org.springframework.ai.zhipuai.ZhiPuAiChatOptions;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Geng Rong
+ */
+@EnabledIfEnvironmentVariable(named = "ZHIPU_AI_API_KEY", matches = ".*")
+public class FunctionCallbackInPromptIT {
+
+ private final Logger logger = LoggerFactory.getLogger(FunctionCallbackInPromptIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.zhipuai.apiKey=" + System.getenv("ZHIPU_AI_API_KEY"))
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class));
+
+ @Test
+ void functionCallTest() {
+ this.contextRunner.withPropertyValues("spring.ai.zhipuai.chat.options.model=glm-4").run(context -> {
+
+ ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.class);
+
+ UserMessage userMessage = new UserMessage(
+ "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius.");
+
+ var promptOptions = ZhiPuAiChatOptions.builder()
+ .functionCallbacks(List.of(FunctionCallback.builder()
+ .function("CurrentWeatherService", new MockWeatherService())
+ .description("Get the weather in location")
+ .inputType(MockWeatherService.Request.class)
+ // .responseConverter(response -> "" + response.temp() +
+ // response.unit())
+ .build()))
+ .build();
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), promptOptions));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+ });
+ }
+
+ @Test
+ void streamingFunctionCallTest() {
+
+ this.contextRunner.withPropertyValues("spring.ai.zhipuai.chat.options.model=glm-4").run(context -> {
+
+ ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.class);
+
+ UserMessage userMessage = new UserMessage(
+ "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius.");
+
+ var promptOptions = ZhiPuAiChatOptions.builder()
+ .functionCallbacks(List.of(FunctionCallback.builder()
+ .function("CurrentWeatherService", new MockWeatherService())
+ .description("Get the weather in location")
+ .inputType(MockWeatherService.Request.class)
+ .build()))
+ .build();
+
+ Flux response = chatModel.stream(new Prompt(List.of(userMessage), promptOptions));
+
+ String content = response.collectList()
+ .block()
+ .stream()
+ .map(ChatResponse::getResults)
+ .flatMap(List::stream)
+ .map(Generation::getOutput)
+ .map(AssistantMessage::getText)
+ .collect(Collectors.joining());
+ logger.info("Response: {}", content);
+
+ assertThat(content).containsAnyOf("30.0", "30");
+ assertThat(content).containsAnyOf("10.0", "10");
+ assertThat(content).containsAnyOf("15.0", "15");
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackWithPlainFunctionBeanIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java
similarity index 96%
rename from spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackWithPlainFunctionBeanIT.java
rename to spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java
index 1b073148f..df21b42ec 100644
--- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackWithPlainFunctionBeanIT.java
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package org.springframework.ai.autoconfigure.zhipuai.tool;
+package org.springframework.ai.model.zhipuai.autoconfigure.tool;
import java.util.List;
import java.util.function.Function;
@@ -26,8 +26,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
-import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
-import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiAutoConfiguration;
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiAutoConfiguration;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/MockWeatherService.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/MockWeatherService.java
new file mode 100644
index 000000000..9d3e97312
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/MockWeatherService.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.zhipuai.autoconfigure.tool;
+
+import java.util.function.Function;
+
+import com.fasterxml.jackson.annotation.JsonClassDescription;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyDescription;
+
+/**
+ * Mock 3rd party weather service.
+ *
+ * @author Geng Rong
+ */
+public class MockWeatherService implements Function {
+
+ @Override
+ public Response apply(Request request) {
+
+ double temperature = 0;
+ if (request.location().contains("Paris")) {
+ temperature = 15;
+ }
+ else if (request.location().contains("Tokyo")) {
+ temperature = 10;
+ }
+ else if (request.location().contains("San Francisco")) {
+ temperature = 30;
+ }
+
+ return new Response(temperature, 15, 20, 2, 53, 45, Unit.C);
+ }
+
+ /**
+ * Temperature units.
+ */
+ public enum Unit {
+
+ /**
+ * Celsius.
+ */
+ C("metric"),
+ /**
+ * Fahrenheit.
+ */
+ F("imperial");
+
+ /**
+ * Human readable unit name.
+ */
+ public final String unitName;
+
+ Unit(String text) {
+ this.unitName = text;
+ }
+
+ }
+
+ /**
+ * Weather Function request.
+ */
+ @JsonInclude(Include.NON_NULL)
+ @JsonClassDescription("Weather API request")
+ public record Request(@JsonProperty(required = true,
+ value = "location") @JsonPropertyDescription("The city and state e.g. San Francisco, CA") String location,
+ @JsonProperty(required = true, value = "lat") @JsonPropertyDescription("The city latitude") double lat,
+ @JsonProperty(required = true, value = "lon") @JsonPropertyDescription("The city longitude") double lon,
+ @JsonProperty(required = true, value = "unit") @JsonPropertyDescription("Temperature unit") Unit unit) {
+
+ }
+
+ /**
+ * Weather Function response.
+ */
+ public record Response(double temp, double feels_like, double temp_min, double temp_max, int pressure, int humidity,
+ Unit unit) {
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/ZhipuAiFunctionCallbackIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/ZhipuAiFunctionCallbackIT.java
new file mode 100644
index 000000000..b7e636592
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/tool/ZhipuAiFunctionCallbackIT.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.model.zhipuai.autoconfigure.tool;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiAutoConfiguration;
+import org.springframework.ai.chat.messages.AssistantMessage;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.model.Generation;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.model.function.FunctionCallback;
+import org.springframework.ai.zhipuai.ZhiPuAiChatModel;
+import org.springframework.ai.zhipuai.ZhiPuAiChatOptions;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Geng Rong
+ */
+@EnabledIfEnvironmentVariable(named = "ZHIPU_AI_API_KEY", matches = ".*")
+public class ZhipuAiFunctionCallbackIT {
+
+ private final Logger logger = LoggerFactory.getLogger(ZhipuAiFunctionCallbackIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.zhipuai.apiKey=" + System.getenv("ZHIPU_AI_API_KEY"))
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class))
+ .withUserConfiguration(Config.class);
+
+ @Test
+ void functionCallTest() {
+ this.contextRunner.withPropertyValues("spring.ai.zhipuai.chat.options.model=glm-4").run(context -> {
+
+ ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.class);
+
+ UserMessage userMessage = new UserMessage(
+ "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius.");
+
+ ChatResponse response = chatModel
+ .call(new Prompt(List.of(userMessage), ZhiPuAiChatOptions.builder().function("WeatherInfo").build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+
+ });
+ }
+
+ @Test
+ void streamFunctionCallTest() {
+ this.contextRunner.withPropertyValues("spring.ai.zhipuai.chat.options.model=glm-4").run(context -> {
+
+ ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.class);
+
+ UserMessage userMessage = new UserMessage(
+ "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius.");
+
+ Flux response = chatModel
+ .stream(new Prompt(List.of(userMessage), ZhiPuAiChatOptions.builder().function("WeatherInfo").build()));
+
+ String content = response.collectList()
+ .block()
+ .stream()
+ .map(ChatResponse::getResults)
+ .flatMap(List::stream)
+ .map(Generation::getOutput)
+ .map(AssistantMessage::getText)
+ .collect(Collectors.joining());
+ logger.info("Response: {}", content);
+
+ assertThat(content).containsAnyOf("30.0", "30");
+ assertThat(content).containsAnyOf("10.0", "10");
+ assertThat(content).containsAnyOf("15.0", "15");
+
+ });
+ }
+
+ @Configuration
+ static class Config {
+
+ @Bean
+ public FunctionCallback weatherFunctionInfo() {
+
+ return FunctionCallback.builder()
+ .function("WeatherInfo", new MockWeatherService())
+ .description("Get the weather in location")
+ .inputType(MockWeatherService.Request.class)
+ // .responseConverter(response -> "" + response.temp() + response.unit())
+ .build();
+ }
+
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryAutoConfigurationIT.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryAutoConfigurationIT.java
new file mode 100644
index 000000000..4b712f050
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryAutoConfigurationIT.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.retry.autoconfigure;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.retry.support.RetryTemplate;
+import org.springframework.web.client.ResponseErrorHandler;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Christian Tzolov
+ */
+public class SpringAiRetryAutoConfigurationIT {
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration(
+ AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class));
+
+ @Test
+ void testRetryAutoConfiguration() {
+ this.contextRunner.run(context -> {
+ assertThat(context).hasSingleBean(RetryTemplate.class);
+ assertThat(context).hasSingleBean(ResponseErrorHandler.class);
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryPropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryPropertiesTests.java
new file mode 100644
index 000000000..64cefc6c2
--- /dev/null
+++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/retry/autoconfigure/SpringAiRetryPropertiesTests.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.ai.retry.autoconfigure;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit Tests for {@link SpringAiRetryProperties}.
+ *
+ * @author Christian Tzolov
+ */
+public class SpringAiRetryPropertiesTests {
+
+ @Test
+ public void retryDefaultProperties() {
+
+ new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class))
+ .run(context -> {
+ var retryProperties = context.getBean(SpringAiRetryProperties.class);
+
+ assertThat(retryProperties.getMaxAttempts()).isEqualTo(10);
+ // do not retry on 4xx errors
+ assertThat(retryProperties.isOnClientErrors()).isFalse();
+ assertThat(retryProperties.getExcludeOnHttpCodes()).isEmpty();
+ assertThat(retryProperties.getOnHttpCodes()).isEmpty();
+ assertThat(retryProperties.getBackoff().getInitialInterval().toMillis()).isEqualTo(2000);
+ assertThat(retryProperties.getBackoff().getMultiplier()).isEqualTo(5);
+ assertThat(retryProperties.getBackoff().getMaxInterval().toMillis()).isEqualTo(3 * 60000);
+ });
+ }
+
+ @Test
+ public void retryCustomProperties() {
+
+ new ApplicationContextRunner().withPropertyValues(
+ // @formatter:off
+ "spring.ai.retry.max-attempts=100",
+ "spring.ai.retry.on-client-errors=false",
+ "spring.ai.retry.exclude-on-http-codes=404,500",
+ "spring.ai.retry.on-http-codes=429",
+ "spring.ai.retry.backoff.initial-interval=1000",
+ "spring.ai.retry.backoff.multiplier=2",
+ "spring.ai.retry.backoff.max-interval=60000")
+ // @formatter:on
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class))
+ .run(context -> {
+ var retryProperties = context.getBean(SpringAiRetryProperties.class);
+
+ assertThat(retryProperties.getMaxAttempts()).isEqualTo(100);
+ assertThat(retryProperties.isOnClientErrors()).isFalse();
+ assertThat(retryProperties.getExcludeOnHttpCodes()).containsExactly(404, 500);
+ assertThat(retryProperties.getOnHttpCodes()).containsExactly(429);
+ assertThat(retryProperties.getBackoff().getInitialInterval().toMillis()).isEqualTo(1000);
+ assertThat(retryProperties.getBackoff().getMultiplier()).isEqualTo(2);
+ assertThat(retryProperties.getBackoff().getMaxInterval().toMillis()).isEqualTo(60000);
+ });
+ }
+
+}
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/autoconfigure/ollama/tool/FunctionCallbackContextKotlinIT.kt b/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackContextKotlinIT.kt
similarity index 93%
rename from spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/autoconfigure/ollama/tool/FunctionCallbackContextKotlinIT.kt
rename to spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackContextKotlinIT.kt
index 884d40d03..1a5564813 100644
--- a/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/autoconfigure/ollama/tool/FunctionCallbackContextKotlinIT.kt
+++ b/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackContextKotlinIT.kt
@@ -14,20 +14,19 @@
* limitations under the License.
*/
-package org.springframework.ai.autoconfigure.ollama.tool
+package org.springframework.ai.model.ollama.autoconfigure.tool
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.slf4j.LoggerFactory
-import org.springframework.ai.autoconfigure.ollama.BaseOllamaIT
-import org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration
+import org.springframework.ai.model.ollama.autoconfigure.BaseOllamaIT
+import org.springframework.ai.model.ollama.autoconfigure.OllamaAutoConfiguration
import org.springframework.ai.chat.messages.UserMessage
import org.springframework.ai.chat.prompt.Prompt
import org.springframework.ai.model.function.FunctionCallingOptions
import org.springframework.ai.ollama.OllamaChatModel
-import org.springframework.ai.ollama.api.OllamaModel
import org.springframework.ai.ollama.api.OllamaOptions
import org.springframework.boot.autoconfigure.AutoConfigurations
import org.springframework.boot.test.context.runner.ApplicationContextRunner
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/autoconfigure/ollama/tool/FunctionCallbackKotlinIT.kt b/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackKotlinIT.kt
similarity index 94%
rename from spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/autoconfigure/ollama/tool/FunctionCallbackKotlinIT.kt
rename to spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackKotlinIT.kt
index 337b1775f..0c46d0292 100644
--- a/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/autoconfigure/ollama/tool/FunctionCallbackKotlinIT.kt
+++ b/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/FunctionCallbackKotlinIT.kt
@@ -14,14 +14,14 @@
* limitations under the License.
*/
-package org.springframework.ai.autoconfigure.ollama.tool
+package org.springframework.ai.model.ollama.autoconfigure.tool
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.slf4j.LoggerFactory
-import org.springframework.ai.autoconfigure.ollama.BaseOllamaIT
-import org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration
+import org.springframework.ai.model.ollama.autoconfigure.BaseOllamaIT
+import org.springframework.ai.model.ollama.autoconfigure.OllamaAutoConfiguration
import org.springframework.ai.chat.messages.UserMessage
import org.springframework.ai.chat.prompt.Prompt
import org.springframework.ai.model.tool.ToolCallingChatOptions
diff --git a/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/autoconfigure/ollama/tool/MockKotlinWeatherService.kt b/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/MockKotlinWeatherService.kt
similarity index 97%
rename from spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/autoconfigure/ollama/tool/MockKotlinWeatherService.kt
rename to spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/MockKotlinWeatherService.kt
index a0811bdb7..3bcdf606e 100644
--- a/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/autoconfigure/ollama/tool/MockKotlinWeatherService.kt
+++ b/spring-ai-spring-boot-autoconfigure/src/test/kotlin/org/springframework/ai/model/ollama/autoconfigure/tool/MockKotlinWeatherService.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package org.springframework.ai.autoconfigure.ollama.tool
+package org.springframework.ai.model.ollama.autoconfigure.tool
import com.fasterxml.jackson.annotation.JsonClassDescription
import com.fasterxml.jackson.annotation.JsonInclude
diff --git a/spring-ai-spring-boot-docker-compose/pom.xml b/spring-ai-spring-boot-docker-compose/pom.xml
index 68e1b02e5..c938d1310 100644
--- a/spring-ai-spring-boot-docker-compose/pom.xml
+++ b/spring-ai-spring-boot-docker-compose/pom.xml
@@ -41,6 +41,8 @@
+
+
org.springframework.ai
spring-ai-spring-boot-autoconfigure
diff --git a/spring-ai-spring-boot-docker-compose/src/main/java/org/springframework/ai/docker/compose/service/connection/ollama/OllamaDockerComposeConnectionDetailsFactory.java b/spring-ai-spring-boot-docker-compose/src/main/java/org/springframework/ai/docker/compose/service/connection/ollama/OllamaDockerComposeConnectionDetailsFactory.java
index 6608cfa0a..f3f1f308c 100644
--- a/spring-ai-spring-boot-docker-compose/src/main/java/org/springframework/ai/docker/compose/service/connection/ollama/OllamaDockerComposeConnectionDetailsFactory.java
+++ b/spring-ai-spring-boot-docker-compose/src/main/java/org/springframework/ai/docker/compose/service/connection/ollama/OllamaDockerComposeConnectionDetailsFactory.java
@@ -16,7 +16,7 @@
package org.springframework.ai.docker.compose.service.connection.ollama;
-import org.springframework.ai.autoconfigure.ollama.OllamaConnectionDetails;
+import org.springframework.ai.model.ollama.autoconfigure.OllamaConnectionDetails;
import org.springframework.boot.docker.compose.core.RunningService;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
diff --git a/spring-ai-spring-boot-docker-compose/src/test/java/org/springframework/ai/docker/compose/service/connection/ollama/OllamaDockerComposeConnectionDetailsFactoryIT.java b/spring-ai-spring-boot-docker-compose/src/test/java/org/springframework/ai/docker/compose/service/connection/ollama/OllamaDockerComposeConnectionDetailsFactoryIT.java
index b37cce486..3c6337f3a 100644
--- a/spring-ai-spring-boot-docker-compose/src/test/java/org/springframework/ai/docker/compose/service/connection/ollama/OllamaDockerComposeConnectionDetailsFactoryIT.java
+++ b/spring-ai-spring-boot-docker-compose/src/test/java/org/springframework/ai/docker/compose/service/connection/ollama/OllamaDockerComposeConnectionDetailsFactoryIT.java
@@ -19,7 +19,7 @@ package org.springframework.ai.docker.compose.service.connection.ollama;
import org.junit.jupiter.api.Test;
import org.testcontainers.utility.DockerImageName;
-import org.springframework.ai.autoconfigure.ollama.OllamaConnectionDetails;
+import org.springframework.ai.model.ollama.autoconfigure.OllamaConnectionDetails;
import org.springframework.boot.docker.compose.service.connection.test.AbstractDockerComposeIT;
import static org.assertj.core.api.Assertions.assertThat;
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-mcp-client-webflux/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-mcp-client-webflux/pom.xml
index 0d2c8b210..531a90b25 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-mcp-client-webflux/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-mcp-client-webflux/pom.xml
@@ -28,7 +28,7 @@
spring-ai-mcp-client-webflux-spring-boot-starter
jar
Spring AI Starter - MCP Client Webflux
- Spring AI MCP Client WebFlux Auto Configuration
+ Spring AI MCP Client WebFlux Spring Boot Starter
https://github.com/spring-projects/spring-ai
@@ -63,4 +63,4 @@
-
\ No newline at end of file
+
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-mcp-client/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-mcp-client/pom.xml
index 06a22b85a..acef84588 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-mcp-client/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-mcp-client/pom.xml
@@ -26,7 +26,7 @@
spring-ai-mcp-client-spring-boot-starter
jar
Spring AI Starter - MCP Client
- Spring AI MCP Client Auto Configuration
+ Spring AI MCP Client Spring Boot Starter
https://github.com/spring-projects/spring-ai
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-mcp-server-webflux/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-mcp-server-webflux/pom.xml
index e847b730f..ddb133f8f 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-mcp-server-webflux/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-mcp-server-webflux/pom.xml
@@ -28,7 +28,7 @@
spring-ai-mcp-server-webflux-spring-boot-starter
jar
Spring AI Starter - MCP Server Webflux
- Spring AI MCP Server WebFlux Auto Configuration
+ Spring AI MCP Server WebFlux Spring Boot Starter
https://github.com/spring-projects/spring-ai
@@ -69,4 +69,4 @@
-
\ No newline at end of file
+
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-mcp-server-webmvc/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-mcp-server-webmvc/pom.xml
index 42d9a66a4..824b37007 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-mcp-server-webmvc/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-mcp-server-webmvc/pom.xml
@@ -28,7 +28,7 @@
spring-ai-mcp-server-webmvc-spring-boot-starter
jar
Spring AI Starter - MCP Server WebMvc
- Spring AI MCP Server WebMvc Auto Configuration
+ Spring AI MCP Server WebMvc Spring Boot Starter
https://github.com/spring-projects/spring-ai
@@ -68,4 +68,4 @@
-
\ No newline at end of file
+
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-mcp-server/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-mcp-server/pom.xml
index 15d0b79fc..1d7dc35bb 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-mcp-server/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-mcp-server/pom.xml
@@ -26,7 +26,7 @@
spring-ai-mcp-server-spring-boot-starter
jar
Spring AI Starter - MCP Server
- Spring AI MCP Server Auto Configuration
+ Spring AI MCP Server Spring Boot Starter
https://github.com/spring-projects/spring-ai
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-anthropic/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-model-anthropic/pom.xml
similarity index 90%
rename from spring-ai-spring-boot-starters/spring-ai-starter-anthropic/pom.xml
rename to spring-ai-spring-boot-starters/spring-ai-starter-model-anthropic/pom.xml
index 31e711ba2..9fb3611ab 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-anthropic/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-model-anthropic/pom.xml
@@ -23,10 +23,10 @@
1.0.0-SNAPSHOT
../../pom.xml
- spring-ai-anthropic-spring-boot-starter
+ spring-ai-starter-model-anthropic
jar
Spring AI Starter - Anthropic
- Spring AI Anthropic Auto Configuration
+ Spring AI Anthropic Spring Boot Starter
https://github.com/spring-projects/spring-ai
@@ -44,7 +44,7 @@
org.springframework.ai
- spring-ai-spring-boot-autoconfigure
+ spring-ai-autoconfigure-model-anthropic
${project.parent.version}
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-azure-openai/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-model-azure-openai/pom.xml
similarity index 90%
rename from spring-ai-spring-boot-starters/spring-ai-starter-azure-openai/pom.xml
rename to spring-ai-spring-boot-starters/spring-ai-starter-model-azure-openai/pom.xml
index 7ccc8ef96..c0ac96592 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-azure-openai/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-model-azure-openai/pom.xml
@@ -23,10 +23,10 @@
1.0.0-SNAPSHOT
../../pom.xml
- spring-ai-azure-openai-spring-boot-starter
+ spring-ai-starter-model-azure-openai
jar
Spring AI Starter - Azure OpenAI
- Spring AI Azure OpenAI Auto Configuration
+ Spring AI Azure OpenAI Spring Boot Starter
https://github.com/spring-projects/spring-ai
@@ -44,7 +44,7 @@
org.springframework.ai
- spring-ai-spring-boot-autoconfigure
+ spring-ai-autoconfigure-model-azure-openai
${project.parent.version}
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-bedrock-converse/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-model-bedrock-converse/pom.xml
similarity index 90%
rename from spring-ai-spring-boot-starters/spring-ai-starter-bedrock-converse/pom.xml
rename to spring-ai-spring-boot-starters/spring-ai-starter-model-bedrock-converse/pom.xml
index e44dc1940..826b988e6 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-bedrock-converse/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-model-bedrock-converse/pom.xml
@@ -24,10 +24,10 @@
1.0.0-SNAPSHOT
../../pom.xml
- spring-ai-bedrock-converse-spring-boot-starter
+ spring-ai-starter-model-bedrock-converse
jar
Spring AI Starter - Bedrock Converse API
- Spring AI Bedrock Converse API Auto Configuration
+ Spring AI Bedrock Converse API Spring Boot Starter
https://github.com/spring-projects/spring-ai
@@ -44,7 +44,7 @@
org.springframework.ai
- spring-ai-spring-boot-autoconfigure
+ spring-ai-autoconfigure-model-bedrock-ai
${project.parent.version}
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-bedrock-ai/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-model-bedrock/pom.xml
similarity index 90%
rename from spring-ai-spring-boot-starters/spring-ai-starter-bedrock-ai/pom.xml
rename to spring-ai-spring-boot-starters/spring-ai-starter-model-bedrock/pom.xml
index fc95593d5..4ccb11d5b 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-bedrock-ai/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-model-bedrock/pom.xml
@@ -24,10 +24,10 @@
1.0.0-SNAPSHOT
../../pom.xml
- spring-ai-bedrock-ai-spring-boot-starter
+ spring-ai-starter-model-bedrock
jar
Spring AI Starter - Bedrock AI
- Spring AI Bedrock AI Auto Configuration
+ Spring AI Bedrock AI Spring Boot Starter
https://github.com/spring-projects/spring-ai
@@ -44,7 +44,7 @@
org.springframework.ai
- spring-ai-spring-boot-autoconfigure
+ spring-ai-autoconfigure-model-bedrock-ai
${project.parent.version}
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-huggingface/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-model-huggingface/pom.xml
similarity index 90%
rename from spring-ai-spring-boot-starters/spring-ai-starter-huggingface/pom.xml
rename to spring-ai-spring-boot-starters/spring-ai-starter-model-huggingface/pom.xml
index 0832116aa..a96ebe930 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-huggingface/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-model-huggingface/pom.xml
@@ -23,10 +23,10 @@
1.0.0-SNAPSHOT
../../pom.xml
- spring-ai-huggingface-spring-boot-starter
+ spring-ai-starter-model-huggingface
jar
Spring AI Starter - Hugging Face
- Spring AI Hugging Face Starter
+ Spring AI Hugging Face Spring Boot Starter
https://github.com/spring-projects/spring-ai
@@ -44,7 +44,7 @@
org.springframework.ai
- spring-ai-spring-boot-autoconfigure
+ spring-ai-autoconfigure-model-huggingface
${project.parent.version}
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-minimax/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-model-minimax/pom.xml
similarity index 91%
rename from spring-ai-spring-boot-starters/spring-ai-starter-minimax/pom.xml
rename to spring-ai-spring-boot-starters/spring-ai-starter-model-minimax/pom.xml
index 124570853..99959b0b2 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-minimax/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-model-minimax/pom.xml
@@ -23,10 +23,10 @@
1.0.0-SNAPSHOT
../../pom.xml
- spring-ai-minimax-spring-boot-starter
+ spring-ai-starter-model-minimax
jar
Spring AI Starter - MiniMax
- Spring AI MiniMax Auto Configuration
+ Spring AI MiniMax Spring Boot Starter
https://github.com/spring-projects/spring-ai
@@ -44,7 +44,7 @@
org.springframework.ai
- spring-ai-spring-boot-autoconfigure
+ spring-ai-autoconfigure-model-minimax
${project.parent.version}
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-mistral-ai/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-model-mistral-ai/pom.xml
similarity index 90%
rename from spring-ai-spring-boot-starters/spring-ai-starter-mistral-ai/pom.xml
rename to spring-ai-spring-boot-starters/spring-ai-starter-model-mistral-ai/pom.xml
index 4c00f8326..1bdaddf5f 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-mistral-ai/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-model-mistral-ai/pom.xml
@@ -23,10 +23,10 @@
1.0.0-SNAPSHOT
../../pom.xml
- spring-ai-mistral-ai-spring-boot-starter
+ spring-ai-starter-model-mistral-ai
jar
Spring AI Starter - MistralAI
- Spring AI MistralAI Auto Configuration
+ Spring AI MistralAI Spring Boot Starter
https://github.com/spring-projects/spring-ai
@@ -44,7 +44,7 @@
org.springframework.ai
- spring-ai-spring-boot-autoconfigure
+ spring-ai-autoconfigure-model-mistral-ai
${project.parent.version}
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-moonshot/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-model-moonshot/pom.xml
similarity index 90%
rename from spring-ai-spring-boot-starters/spring-ai-starter-moonshot/pom.xml
rename to spring-ai-spring-boot-starters/spring-ai-starter-model-moonshot/pom.xml
index 70c34367b..bf07aa352 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-moonshot/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-model-moonshot/pom.xml
@@ -23,10 +23,10 @@
1.0.0-SNAPSHOT
../../pom.xml
- spring-ai-moonshot-spring-boot-starter
+ spring-ai-starter-model-moonshot
jar
Spring AI Starter - Moonshot
- Spring AI Moonshot Auto Configuration
+ Spring AI Moonshot Spring Boot Starter
https://github.com/spring-projects/spring-ai
@@ -44,7 +44,7 @@
org.springframework.ai
- spring-ai-spring-boot-autoconfigure
+ spring-ai-autoconfigure-model-moonshot
${project.parent.version}
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-oci-genai/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-model-oci-genai/pom.xml
similarity index 93%
rename from spring-ai-spring-boot-starters/spring-ai-starter-oci-genai/pom.xml
rename to spring-ai-spring-boot-starters/spring-ai-starter-model-oci-genai/pom.xml
index f8a835287..2f5dc3286 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-oci-genai/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-model-oci-genai/pom.xml
@@ -23,7 +23,7 @@
1.0.0-SNAPSHOT
../../pom.xml
- spring-ai-oci-genai-spring-boot-starter
+ spring-ai-starter-model-oci-genai
jar
Spring AI Starter - OCI GenAI
Spring AI OCI GenAI Auto Configuration
@@ -43,7 +43,7 @@
org.springframework.ai
- spring-ai-spring-boot-autoconfigure
+ spring-ai-autoconfigure-model-oci-genai
${project.parent.version}
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-ollama/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-model-ollama/pom.xml
similarity index 91%
rename from spring-ai-spring-boot-starters/spring-ai-starter-ollama/pom.xml
rename to spring-ai-spring-boot-starters/spring-ai-starter-model-ollama/pom.xml
index 7b16c0c67..871c903e8 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-ollama/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-model-ollama/pom.xml
@@ -23,10 +23,10 @@
1.0.0-SNAPSHOT
../../pom.xml
- spring-ai-ollama-spring-boot-starter
+ spring-ai-starter-model-ollama
jar
Spring AI Starter - Ollama
- Spring AI Ollama Auto Configuration
+ Spring AI Ollama Spring Boot Starter
https://github.com/spring-projects/spring-ai
@@ -44,7 +44,7 @@
org.springframework.ai
- spring-ai-spring-boot-autoconfigure
+ spring-ai-autoconfigure-model-ollama
${project.parent.version}
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-openai/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-model-openai/pom.xml
similarity index 91%
rename from spring-ai-spring-boot-starters/spring-ai-starter-openai/pom.xml
rename to spring-ai-spring-boot-starters/spring-ai-starter-model-openai/pom.xml
index a5bce9888..421907c7d 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-openai/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-model-openai/pom.xml
@@ -23,10 +23,10 @@
1.0.0-SNAPSHOT
../../pom.xml
- spring-ai-openai-spring-boot-starter
+ spring-ai-starter-model-openai
jar
Spring AI Starter - OpenAI
- Spring AI Open AI Auto Configuration
+ Spring AI Open AI Spring Boot Starter
https://github.com/spring-projects/spring-ai
@@ -44,7 +44,7 @@
org.springframework.ai
- spring-ai-spring-boot-autoconfigure
+ spring-ai-autoconfigure-model-openai
${project.parent.version}
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-postgresml-embedding/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-model-postgresml-embedding/pom.xml
similarity index 89%
rename from spring-ai-spring-boot-starters/spring-ai-starter-postgresml-embedding/pom.xml
rename to spring-ai-spring-boot-starters/spring-ai-starter-model-postgresml-embedding/pom.xml
index ffbc4aa55..69681114b 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-postgresml-embedding/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-model-postgresml-embedding/pom.xml
@@ -23,10 +23,10 @@
1.0.0-SNAPSHOT
../../pom.xml
- spring-ai-postgresml-spring-boot-starter
+ spring-ai-starter-model-postgresml-embedding
jar
Spring AI Starter - PostgresML Embedding
- Spring PostgresML Embedding Auto Configuration
+ Spring PostgresML Embedding Spring Boot Starter
https://github.com/spring-projects/spring-ai
@@ -44,7 +44,7 @@
org.springframework.ai
- spring-ai-spring-boot-autoconfigure
+ spring-ai-autoconfigure-model-postgresml-embedding
${project.parent.version}
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-qianfan/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-model-qianfan/pom.xml
similarity index 90%
rename from spring-ai-spring-boot-starters/spring-ai-starter-qianfan/pom.xml
rename to spring-ai-spring-boot-starters/spring-ai-starter-model-qianfan/pom.xml
index 0da3b1776..c003b5810 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-qianfan/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-model-qianfan/pom.xml
@@ -23,10 +23,10 @@
1.0.0-SNAPSHOT
../../pom.xml
- spring-ai-qianfan-spring-boot-starter
+ spring-ai-starter-model-qianfan
jar
Spring AI Starter - QianFan
- Spring AI QianFan Auto Configuration
+ Spring AI QianFan Spring Boot Starter
https://github.com/spring-projects/spring-ai
@@ -44,7 +44,7 @@
org.springframework.ai
- spring-ai-spring-boot-autoconfigure
+ spring-ai-autoconfigure-model-qianfan
${project.parent.version}
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-stability-ai/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-model-stability-ai/pom.xml
similarity index 90%
rename from spring-ai-spring-boot-starters/spring-ai-starter-stability-ai/pom.xml
rename to spring-ai-spring-boot-starters/spring-ai-starter-model-stability-ai/pom.xml
index 64764552e..61e0b21c3 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-stability-ai/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-model-stability-ai/pom.xml
@@ -23,10 +23,10 @@
1.0.0-SNAPSHOT
../../pom.xml
- spring-ai-stability-ai-spring-boot-starter
+ spring-ai-starter-model-stability-ai
jar
Spring AI Starter - Stability AI
- Spring AI Stability Auto Configuration
+ Spring AI Stability Spring Boot Starter
https://github.com/spring-projects/spring-ai
@@ -44,7 +44,7 @@
org.springframework.ai
- spring-ai-spring-boot-autoconfigure
+ spring-ai-autoconfigure-model-stability-ai
${project.parent.version}
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-transformers/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-model-transformers/pom.xml
similarity index 90%
rename from spring-ai-spring-boot-starters/spring-ai-starter-transformers/pom.xml
rename to spring-ai-spring-boot-starters/spring-ai-starter-model-transformers/pom.xml
index 9b0e61643..7fa38c23c 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-transformers/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-model-transformers/pom.xml
@@ -23,10 +23,10 @@
1.0.0-SNAPSHOT
../../pom.xml
- spring-ai-transformers-spring-boot-starter
+ spring-ai-starter-model-transformers
jar
Spring AI Starter - Transformers Embedding
- Spring Transformers Embedding Auto Configuration
+ Spring AI Transformers Embedding Spring Boot Starter
https://github.com/spring-projects/spring-ai
@@ -44,7 +44,7 @@
org.springframework.ai
- spring-ai-spring-boot-autoconfigure
+ spring-ai-autoconfigure-model-transformers
${project.parent.version}
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-vertex-ai-embedding/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-model-vertex-ai-embedding/pom.xml
similarity index 90%
rename from spring-ai-spring-boot-starters/spring-ai-starter-vertex-ai-embedding/pom.xml
rename to spring-ai-spring-boot-starters/spring-ai-starter-model-vertex-ai-embedding/pom.xml
index 36fcee4d1..57029055c 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-vertex-ai-embedding/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-model-vertex-ai-embedding/pom.xml
@@ -23,10 +23,10 @@
1.0.0-SNAPSHOT
../../pom.xml
- spring-ai-vertex-ai-embedding-spring-boot-starter
+ spring-ai-starter-model-vertex-ai-embedding
jar
Spring AI Starter - VertexAI Embedding
- Spring AI Vertex Embedding AI Auto Configuration
+ Spring AI Vertex Embedding AI Spring Boot Starter
https://github.com/spring-projects/spring-ai
@@ -44,7 +44,7 @@
org.springframework.ai
- spring-ai-spring-boot-autoconfigure
+ spring-ai-autoconfigure-model-vertex-ai
${project.parent.version}
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-vertex-ai-gemini/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-model-vertex-ai-gemini/pom.xml
similarity index 90%
rename from spring-ai-spring-boot-starters/spring-ai-starter-vertex-ai-gemini/pom.xml
rename to spring-ai-spring-boot-starters/spring-ai-starter-model-vertex-ai-gemini/pom.xml
index 778b9cccd..aae47d67f 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-vertex-ai-gemini/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-model-vertex-ai-gemini/pom.xml
@@ -23,10 +23,10 @@
1.0.0-SNAPSHOT
../../pom.xml
- spring-ai-vertex-ai-gemini-spring-boot-starter
+ spring-ai-starter-model-vertex-ai-gemini
jar
Spring AI Starter - VertexAI Gemini
- Spring AI Vertex Gemini AI Auto Configuration
+ Spring AI Vertex Gemini AI Spring Boot Starter
https://github.com/spring-projects/spring-ai
@@ -44,7 +44,7 @@
org.springframework.ai
- spring-ai-spring-boot-autoconfigure
+ spring-ai-autoconfigure-model-vertex-ai
${project.parent.version}
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-watsonx-ai/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-model-watsonx-ai/pom.xml
similarity index 90%
rename from spring-ai-spring-boot-starters/spring-ai-starter-watsonx-ai/pom.xml
rename to spring-ai-spring-boot-starters/spring-ai-starter-model-watsonx-ai/pom.xml
index 36e871422..6999e21d7 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-watsonx-ai/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-model-watsonx-ai/pom.xml
@@ -23,10 +23,10 @@
1.0.0-SNAPSHOT
../../pom.xml
- spring-ai-watsonx-ai-spring-boot-starter
+ spring-ai-starter-model-watsonx-ai
jar
Spring AI Starter - Watsonx.AI
- Spring AI Watsonx AI Auto Configuration
+ Spring AI Watsonx AI Spring Boot Starter
https://github.com/spring-projects/spring-ai
@@ -44,7 +44,7 @@
org.springframework.ai
- spring-ai-spring-boot-autoconfigure
+ spring-ai-autoconfigure-model-watsonx-ai
${project.parent.version}
@@ -55,4 +55,4 @@