diff --git a/auto-configurations/chat/spring-ai-chat-client-spring-boot-autoconfigure/pom.xml b/auto-configurations/chat/spring-ai-chat-client-spring-boot-autoconfigure/pom.xml
new file mode 100644
index 00000000000..881402d0ca1
--- /dev/null
+++ b/auto-configurations/chat/spring-ai-chat-client-spring-boot-autoconfigure/pom.xml
@@ -0,0 +1,66 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../../pom.xml
+
+ spring-ai-chat-client-spring-boot-autoconfigure
+ jar
+ Spring AI Chat Client Auto Configuration
+ Spring AI Chat Client Auto Configuration
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-core
+ ${parent.version}
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
diff --git a/auto-configurations/chat/spring-ai-chat-client-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/client/ChatClientAutoConfiguration.java b/auto-configurations/chat/spring-ai-chat-client-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/client/ChatClientAutoConfiguration.java
new file mode 100644
index 00000000000..f815dde2e37
--- /dev/null
+++ b/auto-configurations/chat/spring-ai-chat-client-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/client/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.autoconfigure.chat.client;
+
+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/auto-configurations/chat/spring-ai-chat-client-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/client/ChatClientBuilderConfigurer.java b/auto-configurations/chat/spring-ai-chat-client-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/client/ChatClientBuilderConfigurer.java
new file mode 100644
index 00000000000..a59653855f7
--- /dev/null
+++ b/auto-configurations/chat/spring-ai-chat-client-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/client/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.autoconfigure.chat.client;
+
+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/auto-configurations/chat/spring-ai-chat-client-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/client/ChatClientBuilderProperties.java b/auto-configurations/chat/spring-ai-chat-client-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/client/ChatClientBuilderProperties.java
new file mode 100644
index 00000000000..91065c18904
--- /dev/null
+++ b/auto-configurations/chat/spring-ai-chat-client-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/client/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.autoconfigure.chat.client;
+
+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/auto-configurations/chat/spring-ai-chat-client-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/chat/spring-ai-chat-client-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..d03329eb4fa
--- /dev/null
+++ b/auto-configurations/chat/spring-ai-chat-client-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,16 @@
+#
+# Copyright 2025-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.
+#
+org.springframework.ai.autoconfigure.chat.client.ChatClientAutoConfiguration
diff --git a/auto-configurations/chat/spring-ai-chat-client-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/chat/client/ChatClientObservationAutoConfigurationTests.java b/auto-configurations/chat/spring-ai-chat-client-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/chat/client/ChatClientObservationAutoConfigurationTests.java
new file mode 100644
index 00000000000..1a97de7c756
--- /dev/null
+++ b/auto-configurations/chat/spring-ai-chat-client-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/chat/client/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.autoconfigure.chat.client;
+
+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/auto-configurations/chat/spring-ai-chat-memory-spring-boot-autoconfigure/pom.xml b/auto-configurations/chat/spring-ai-chat-memory-spring-boot-autoconfigure/pom.xml
new file mode 100644
index 00000000000..48b114df6c1
--- /dev/null
+++ b/auto-configurations/chat/spring-ai-chat-memory-spring-boot-autoconfigure/pom.xml
@@ -0,0 +1,98 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../../pom.xml
+
+ spring-ai-chat-memory-spring-boot-autoconfigure
+ jar
+ Spring AI Chat Memory Auto Configuration
+ Spring AI Chat Memory Auto Configuration
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-core
+ ${parent.version}
+
+
+
+ org.springframework.ai
+ spring-ai-cassandra-store
+ ${parent.version}
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.ai
+ spring-ai-chat-client-spring-boot-autoconfigure
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.ai
+ spring-ai-openai
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.testcontainers
+ junit-jupiter
+ test
+
+
+
+ org.testcontainers
+ cassandra
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
diff --git a/auto-configurations/chat/spring-ai-chat-memory-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/memory/CommonChatMemoryProperties.java b/auto-configurations/chat/spring-ai-chat-memory-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/memory/CommonChatMemoryProperties.java
new file mode 100644
index 00000000000..489ee2acff6
--- /dev/null
+++ b/auto-configurations/chat/spring-ai-chat-memory-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/memory/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.autoconfigure.chat.memory;
+
+/**
+ * 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/auto-configurations/chat/spring-ai-chat-memory-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/memory/cassandra/CassandraChatMemoryAutoConfiguration.java b/auto-configurations/chat/spring-ai-chat-memory-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/memory/cassandra/CassandraChatMemoryAutoConfiguration.java
new file mode 100644
index 00000000000..2a2ef509dd3
--- /dev/null
+++ b/auto-configurations/chat/spring-ai-chat-memory-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/memory/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.autoconfigure.chat.memory.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/auto-configurations/chat/spring-ai-chat-memory-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/memory/cassandra/CassandraChatMemoryProperties.java b/auto-configurations/chat/spring-ai-chat-memory-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/memory/cassandra/CassandraChatMemoryProperties.java
new file mode 100644
index 00000000000..96a4c4ee325
--- /dev/null
+++ b/auto-configurations/chat/spring-ai-chat-memory-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/memory/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.autoconfigure.chat.memory.cassandra;
+
+import java.time.Duration;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.ai.autoconfigure.chat.memory.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/auto-configurations/chat/spring-ai-chat-memory-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/chat/spring-ai-chat-memory-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..9b49bd523f4
--- /dev/null
+++ b/auto-configurations/chat/spring-ai-chat-memory-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,16 @@
+#
+# Copyright 2025-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.
+#
+org.springframework.ai.autoconfigure.chat.memory.cassandra.CassandraChatMemoryAutoConfiguration
diff --git a/auto-configurations/chat/spring-ai-chat-memory-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/chat/cassandra/CassandraChatMemoryAutoConfigurationIT.java b/auto-configurations/chat/spring-ai-chat-memory-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/chat/cassandra/CassandraChatMemoryAutoConfigurationIT.java
new file mode 100644
index 00000000000..982c5737a30
--- /dev/null
+++ b/auto-configurations/chat/spring-ai-chat-memory-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/chat/cassandra/CassandraChatMemoryAutoConfigurationIT.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.autoconfigure.chat.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.autoconfigure.chat.memory.cassandra.CassandraChatMemoryAutoConfiguration;
+import org.springframework.ai.autoconfigure.chat.memory.cassandra.CassandraChatMemoryProperties;
+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/auto-configurations/chat/spring-ai-chat-memory-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/chat/cassandra/CassandraChatMemoryPropertiesTest.java b/auto-configurations/chat/spring-ai-chat-memory-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/chat/cassandra/CassandraChatMemoryPropertiesTest.java
new file mode 100644
index 00000000000..9a8ecf5862e
--- /dev/null
+++ b/auto-configurations/chat/spring-ai-chat-memory-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/chat/cassandra/CassandraChatMemoryPropertiesTest.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.autoconfigure.chat.cassandra;
+
+import java.time.Duration;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.autoconfigure.chat.memory.cassandra.CassandraChatMemoryProperties;
+import org.springframework.ai.chat.memory.cassandra.CassandraChatMemoryConfig;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Mick Semb Wever
+ * @author Jihoon Kim
+ * @since 1.0.0
+ */
+class CassandraChatMemoryPropertiesTest {
+
+ @Test
+ void defaultValues() {
+ var props = new CassandraChatMemoryProperties();
+ assertThat(props.getKeyspace()).isEqualTo(CassandraChatMemoryConfig.DEFAULT_KEYSPACE_NAME);
+ assertThat(props.getTable()).isEqualTo(CassandraChatMemoryConfig.DEFAULT_TABLE_NAME);
+ assertThat(props.getAssistantColumn()).isEqualTo(CassandraChatMemoryConfig.DEFAULT_ASSISTANT_COLUMN_NAME);
+ assertThat(props.getUserColumn()).isEqualTo(CassandraChatMemoryConfig.DEFAULT_USER_COLUMN_NAME);
+ assertThat(props.getTimeToLive()).isNull();
+ assertThat(props.isInitializeSchema()).isTrue();
+ }
+
+ @Test
+ void customValues() {
+ var props = new CassandraChatMemoryProperties();
+ props.setKeyspace("my_keyspace");
+ props.setTable("my_table");
+ props.setAssistantColumn("my_assistant_column");
+ props.setUserColumn("my_user_column");
+ props.setTimeToLive(Duration.ofDays(1));
+ props.setInitializeSchema(false);
+
+ assertThat(props.getKeyspace()).isEqualTo("my_keyspace");
+ assertThat(props.getTable()).isEqualTo("my_table");
+ assertThat(props.getAssistantColumn()).isEqualTo("my_assistant_column");
+ assertThat(props.getUserColumn()).isEqualTo("my_user_column");
+ assertThat(props.getTimeToLive()).isEqualTo(Duration.ofDays(1));
+ assertThat(props.isInitializeSchema()).isFalse();
+ }
+
+}
diff --git a/auto-configurations/chat/spring-ai-chat-model-spring-boot-autoconfigure/pom.xml b/auto-configurations/chat/spring-ai-chat-model-spring-boot-autoconfigure/pom.xml
new file mode 100644
index 00000000000..48d2c389ac0
--- /dev/null
+++ b/auto-configurations/chat/spring-ai-chat-model-spring-boot-autoconfigure/pom.xml
@@ -0,0 +1,66 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../../pom.xml
+
+ spring-ai-chat-model-spring-boot-autoconfigure
+ jar
+ Spring AI Chat Model Auto Configuration
+ Spring AI Chat Model Auto Configuration
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-core
+ ${parent.version}
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
diff --git a/auto-configurations/chat/spring-ai-chat-model-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/model/ToolCallingAutoConfiguration.java b/auto-configurations/chat/spring-ai-chat-model-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/model/ToolCallingAutoConfiguration.java
new file mode 100644
index 00000000000..a530bf7dc5a
--- /dev/null
+++ b/auto-configurations/chat/spring-ai-chat-model-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/model/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.autoconfigure.chat.model;
+
+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/auto-configurations/chat/spring-ai-chat-model-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/chat/spring-ai-chat-model-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..5fc47ed68d1
--- /dev/null
+++ b/auto-configurations/chat/spring-ai-chat-model-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,16 @@
+#
+# Copyright 2025-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.
+#
+org.springframework.ai.autoconfigure.chat.model.ToolCallingAutoConfiguration
diff --git a/auto-configurations/chat/spring-ai-chat-model-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/chat/model/ToolCallingAutoConfigurationTests.java b/auto-configurations/chat/spring-ai-chat-model-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/chat/model/ToolCallingAutoConfigurationTests.java
new file mode 100644
index 00000000000..3c6cca70d08
--- /dev/null
+++ b/auto-configurations/chat/spring-ai-chat-model-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/chat/model/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.autoconfigure.chat.model;
+
+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/auto-configurations/common/spring-ai-retry-spring-boot-autoconfigure/pom.xml b/auto-configurations/common/spring-ai-retry-spring-boot-autoconfigure/pom.xml
new file mode 100644
index 00000000000..3c2bda17113
--- /dev/null
+++ b/auto-configurations/common/spring-ai-retry-spring-boot-autoconfigure/pom.xml
@@ -0,0 +1,66 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../../pom.xml
+
+ spring-ai-retry-spring-boot-autoconfigure
+ jar
+ Spring AI Retry Auto Configuration
+ Spring AI Retry Auto Configuration
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-retry
+ ${parent.version}
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
diff --git a/auto-configurations/common/spring-ai-retry-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/retry/SpringAiRetryAutoConfiguration.java b/auto-configurations/common/spring-ai-retry-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/retry/SpringAiRetryAutoConfiguration.java
new file mode 100644
index 00000000000..b17b4bfc871
--- /dev/null
+++ b/auto-configurations/common/spring-ai-retry-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/retry/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.autoconfigure.retry;
+
+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/auto-configurations/common/spring-ai-retry-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/retry/SpringAiRetryProperties.java b/auto-configurations/common/spring-ai-retry-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/retry/SpringAiRetryProperties.java
new file mode 100644
index 00000000000..f9e681f0d64
--- /dev/null
+++ b/auto-configurations/common/spring-ai-retry-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/retry/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.autoconfigure.retry;
+
+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/auto-configurations/common/spring-ai-retry-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/common/spring-ai-retry-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..e08dd4f6ce1
--- /dev/null
+++ b/auto-configurations/common/spring-ai-retry-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,16 @@
+#
+# Copyright 2025-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.
+#
+org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration
diff --git a/auto-configurations/common/spring-ai-retry-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/retry/SpringAiRetryAutoConfigurationIT.java b/auto-configurations/common/spring-ai-retry-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/retry/SpringAiRetryAutoConfigurationIT.java
new file mode 100644
index 00000000000..0c8dba03b7f
--- /dev/null
+++ b/auto-configurations/common/spring-ai-retry-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/retry/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.autoconfigure.retry;
+
+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/auto-configurations/common/spring-ai-retry-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/retry/SpringAiRetryPropertiesTests.java b/auto-configurations/common/spring-ai-retry-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/retry/SpringAiRetryPropertiesTests.java
new file mode 100644
index 00000000000..3f20e19f8db
--- /dev/null
+++ b/auto-configurations/common/spring-ai-retry-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/retry/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.autoconfigure.retry;
+
+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/auto-configurations/spring-ai-mcp-client/pom.xml b/auto-configurations/mcp/spring-ai-mcp-client/pom.xml
similarity index 98%
rename from auto-configurations/spring-ai-mcp-client/pom.xml
rename to auto-configurations/mcp/spring-ai-mcp-client/pom.xml
index 0b2f8ce40d9..61d3e56ee79 100644
--- a/auto-configurations/spring-ai-mcp-client/pom.xml
+++ b/auto-configurations/mcp/spring-ai-mcp-client/pom.xml
@@ -7,7 +7,7 @@
org.springframework.ai
spring-ai
1.0.0-SNAPSHOT
- ../../pom.xml
+ ../../../pom.xml
spring-ai-mcp-client-spring-boot-autoconfigure
jar
diff --git a/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/McpClientAutoConfiguration.java b/auto-configurations/mcp/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/McpClientAutoConfiguration.java
similarity index 100%
rename from auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/McpClientAutoConfiguration.java
rename to auto-configurations/mcp/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/McpClientAutoConfiguration.java
diff --git a/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/NamedClientMcpTransport.java b/auto-configurations/mcp/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/NamedClientMcpTransport.java
similarity index 100%
rename from auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/NamedClientMcpTransport.java
rename to auto-configurations/mcp/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/NamedClientMcpTransport.java
diff --git a/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/SseHttpClientTransportAutoConfiguration.java b/auto-configurations/mcp/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/SseHttpClientTransportAutoConfiguration.java
similarity index 100%
rename from auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/SseHttpClientTransportAutoConfiguration.java
rename to auto-configurations/mcp/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/SseHttpClientTransportAutoConfiguration.java
diff --git a/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/SseWebFluxTransportAutoConfiguration.java b/auto-configurations/mcp/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/SseWebFluxTransportAutoConfiguration.java
similarity index 100%
rename from auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/SseWebFluxTransportAutoConfiguration.java
rename to auto-configurations/mcp/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/SseWebFluxTransportAutoConfiguration.java
diff --git a/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/StdioTransportAutoConfiguration.java b/auto-configurations/mcp/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/StdioTransportAutoConfiguration.java
similarity index 100%
rename from auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/StdioTransportAutoConfiguration.java
rename to auto-configurations/mcp/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/StdioTransportAutoConfiguration.java
diff --git a/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/configurer/McpAsyncClientConfigurer.java b/auto-configurations/mcp/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/configurer/McpAsyncClientConfigurer.java
similarity index 100%
rename from auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/configurer/McpAsyncClientConfigurer.java
rename to auto-configurations/mcp/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/configurer/McpAsyncClientConfigurer.java
diff --git a/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/configurer/McpSyncClientConfigurer.java b/auto-configurations/mcp/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/configurer/McpSyncClientConfigurer.java
similarity index 100%
rename from auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/configurer/McpSyncClientConfigurer.java
rename to auto-configurations/mcp/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/configurer/McpSyncClientConfigurer.java
diff --git a/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/properties/McpClientCommonProperties.java b/auto-configurations/mcp/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/properties/McpClientCommonProperties.java
similarity index 100%
rename from auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/properties/McpClientCommonProperties.java
rename to auto-configurations/mcp/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/properties/McpClientCommonProperties.java
diff --git a/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/properties/McpSseClientProperties.java b/auto-configurations/mcp/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/properties/McpSseClientProperties.java
similarity index 100%
rename from auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/properties/McpSseClientProperties.java
rename to auto-configurations/mcp/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/properties/McpSseClientProperties.java
diff --git a/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/properties/McpStdioClientProperties.java b/auto-configurations/mcp/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/properties/McpStdioClientProperties.java
similarity index 100%
rename from auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/properties/McpStdioClientProperties.java
rename to auto-configurations/mcp/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/properties/McpStdioClientProperties.java
diff --git a/auto-configurations/spring-ai-mcp-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/mcp/spring-ai-mcp-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
similarity index 100%
rename from auto-configurations/spring-ai-mcp-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
rename to auto-configurations/mcp/spring-ai-mcp-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
diff --git a/auto-configurations/spring-ai-mcp-client/src/test/java/org/springframework/ai/autoconfigure/mcp/client/McpClientAutoConfigurationIT.java b/auto-configurations/mcp/spring-ai-mcp-client/src/test/java/org/springframework/ai/autoconfigure/mcp/client/McpClientAutoConfigurationIT.java
similarity index 100%
rename from auto-configurations/spring-ai-mcp-client/src/test/java/org/springframework/ai/autoconfigure/mcp/client/McpClientAutoConfigurationIT.java
rename to auto-configurations/mcp/spring-ai-mcp-client/src/test/java/org/springframework/ai/autoconfigure/mcp/client/McpClientAutoConfigurationIT.java
diff --git a/auto-configurations/spring-ai-mcp-client/src/test/resources/application-test.properties b/auto-configurations/mcp/spring-ai-mcp-client/src/test/resources/application-test.properties
similarity index 100%
rename from auto-configurations/spring-ai-mcp-client/src/test/resources/application-test.properties
rename to auto-configurations/mcp/spring-ai-mcp-client/src/test/resources/application-test.properties
diff --git a/auto-configurations/spring-ai-mcp-server/pom.xml b/auto-configurations/mcp/spring-ai-mcp-server/pom.xml
similarity index 97%
rename from auto-configurations/spring-ai-mcp-server/pom.xml
rename to auto-configurations/mcp/spring-ai-mcp-server/pom.xml
index 3f4a942e6fa..a68c11af5d9 100644
--- a/auto-configurations/spring-ai-mcp-server/pom.xml
+++ b/auto-configurations/mcp/spring-ai-mcp-server/pom.xml
@@ -7,7 +7,7 @@
org.springframework.ai
spring-ai
1.0.0-SNAPSHOT
- ../../pom.xml
+ ../../../pom.xml
spring-ai-mcp-server-spring-boot-autoconfigure
jar
diff --git a/auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpServerAutoConfiguration.java b/auto-configurations/mcp/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpServerAutoConfiguration.java
similarity index 100%
rename from auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpServerAutoConfiguration.java
rename to auto-configurations/mcp/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpServerAutoConfiguration.java
diff --git a/auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpServerProperties.java b/auto-configurations/mcp/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpServerProperties.java
similarity index 100%
rename from auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpServerProperties.java
rename to auto-configurations/mcp/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpServerProperties.java
diff --git a/auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpWebFluxServerAutoConfiguration.java b/auto-configurations/mcp/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpWebFluxServerAutoConfiguration.java
similarity index 100%
rename from auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpWebFluxServerAutoConfiguration.java
rename to auto-configurations/mcp/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpWebFluxServerAutoConfiguration.java
diff --git a/auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpWebMvcServerAutoConfiguration.java b/auto-configurations/mcp/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpWebMvcServerAutoConfiguration.java
similarity index 100%
rename from auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpWebMvcServerAutoConfiguration.java
rename to auto-configurations/mcp/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpWebMvcServerAutoConfiguration.java
diff --git a/auto-configurations/spring-ai-mcp-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/mcp/spring-ai-mcp-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
similarity index 100%
rename from auto-configurations/spring-ai-mcp-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
rename to auto-configurations/mcp/spring-ai-mcp-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
diff --git a/auto-configurations/spring-ai-mcp-server/src/test/java/org/springframework/ai/autoconfigure/mcp/server/McpServerAutoConfigurationIT.java b/auto-configurations/mcp/spring-ai-mcp-server/src/test/java/org/springframework/ai/autoconfigure/mcp/server/McpServerAutoConfigurationIT.java
similarity index 100%
rename from auto-configurations/spring-ai-mcp-server/src/test/java/org/springframework/ai/autoconfigure/mcp/server/McpServerAutoConfigurationIT.java
rename to auto-configurations/mcp/spring-ai-mcp-server/src/test/java/org/springframework/ai/autoconfigure/mcp/server/McpServerAutoConfigurationIT.java
diff --git a/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/pom.xml b/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/pom.xml
new file mode 100644
index 00000000000..f98be6d3232
--- /dev/null
+++ b/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/pom.xml
@@ -0,0 +1,93 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../../pom.xml
+
+ spring-ai-anthropic-spring-boot-autoconfigure
+ jar
+ Spring AI Anthropic Auto Configuration
+ Spring AI Anthropic Auto Configuration
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-anthropic
+ ${project.parent.version}
+ true
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-chat-client-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-chat-model-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-retry-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
diff --git a/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/anthropic/AnthropicAutoConfiguration.java b/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/anthropic/AnthropicAutoConfiguration.java
new file mode 100644
index 00000000000..40addc8d69e
--- /dev/null
+++ b/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/anthropic/AnthropicAutoConfiguration.java
@@ -0,0 +1,102 @@
+/*
+ * 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.autoconfigure.anthropic;
+
+import io.micrometer.observation.ObservationRegistry;
+
+import org.springframework.ai.anthropic.AnthropicChatModel;
+import org.springframework.ai.anthropic.api.AnthropicApi;
+import org.springframework.ai.autoconfigure.chat.model.ToolCallingAutoConfiguration;
+import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.chat.observation.ChatModelObservationConvention;
+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.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.web.client.ResponseErrorHandler;
+import org.springframework.web.client.RestClient;
+import org.springframework.web.reactive.function.client.WebClient;
+
+/**
+ * {@link AutoConfiguration Auto-configuration} for Anthropic Chat Model.
+ *
+ * @author Christian Tzolov
+ * @author Thomas Vitale
+ * @since 1.0.0
+ */
+@AutoConfiguration(after = { RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class,
+ ToolCallingAutoConfiguration.class })
+@EnableConfigurationProperties({ AnthropicChatProperties.class, AnthropicConnectionProperties.class })
+@ConditionalOnClass(AnthropicApi.class)
+@ConditionalOnProperty(prefix = AnthropicChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class,
+ ToolCallingAutoConfiguration.class, WebClientAutoConfiguration.class })
+public class AnthropicAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ public AnthropicApi anthropicApi(AnthropicConnectionProperties connectionProperties,
+ ObjectProvider restClientBuilderProvider,
+ ObjectProvider webClientBuilderProvider, ResponseErrorHandler responseErrorHandler) {
+
+ return new AnthropicApi(connectionProperties.getBaseUrl(), connectionProperties.getApiKey(),
+ connectionProperties.getVersion(), restClientBuilderProvider.getIfAvailable(RestClient::builder),
+ webClientBuilderProvider.getIfAvailable(WebClient::builder), responseErrorHandler,
+ connectionProperties.getBetaVersion());
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public AnthropicChatModel anthropicChatModel(AnthropicApi anthropicApi, AnthropicChatProperties chatProperties,
+ RetryTemplate retryTemplate, ToolCallingManager toolCallingManager,
+ ObjectProvider observationRegistry,
+ ObjectProvider observationConvention) {
+
+ var chatModel = AnthropicChatModel.builder()
+ .anthropicApi(anthropicApi)
+ .defaultOptions(chatProperties.getOptions())
+ .toolCallingManager(toolCallingManager)
+ .retryTemplate(retryTemplate)
+ .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
+ .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/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/anthropic/AnthropicChatProperties.java b/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/anthropic/AnthropicChatProperties.java
new file mode 100644
index 00000000000..4e70c916d04
--- /dev/null
+++ b/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/anthropic/AnthropicChatProperties.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.autoconfigure.anthropic;
+
+import org.springframework.ai.anthropic.AnthropicChatModel;
+import org.springframework.ai.anthropic.AnthropicChatOptions;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+/**
+ * Anthropic Chat autoconfiguration properties.
+ *
+ * @author Christian Tzolov
+ * @author Alexandros Pappas
+ * @since 1.0.0
+ */
+@ConfigurationProperties(AnthropicChatProperties.CONFIG_PREFIX)
+public class AnthropicChatProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.anthropic.chat";
+
+ /**
+ * Enable Anthropic 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 AnthropicChatOptions options = AnthropicChatOptions.builder()
+ .model(AnthropicChatModel.DEFAULT_MODEL_NAME)
+ .maxTokens(AnthropicChatModel.DEFAULT_MAX_TOKENS)
+ .temperature(AnthropicChatModel.DEFAULT_TEMPERATURE)
+ .build();
+
+ public AnthropicChatOptions getOptions() {
+ return this.options;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+}
diff --git a/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/anthropic/AnthropicConnectionProperties.java b/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/anthropic/AnthropicConnectionProperties.java
new file mode 100644
index 00000000000..53a74e35178
--- /dev/null
+++ b/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/anthropic/AnthropicConnectionProperties.java
@@ -0,0 +1,86 @@
+/*
+ * 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.autoconfigure.anthropic;
+
+import org.springframework.ai.anthropic.api.AnthropicApi;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Anthropic API connection properties.
+ *
+ * @author Christian Tzolov
+ * @since 1.0.0
+ */
+@ConfigurationProperties(AnthropicConnectionProperties.CONFIG_PREFIX)
+public class AnthropicConnectionProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.anthropic";
+
+ /**
+ * Anthropic API access key.
+ */
+ private String apiKey;
+
+ /**
+ * Anthropic API base URL.
+ */
+ private String baseUrl = AnthropicApi.DEFAULT_BASE_URL;
+
+ /**
+ * Anthropic API version.
+ */
+ private String version = AnthropicApi.DEFAULT_ANTHROPIC_VERSION;
+
+ /**
+ * Beta features version. Such as tools-2024-04-04 or
+ * max-tokens-3-5-sonnet-2024-07-15.
+ */
+ private String betaVersion = AnthropicApi.DEFAULT_ANTHROPIC_BETA_VERSION;
+
+ 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 getVersion() {
+ return this.version;
+ }
+
+ public void setVersion(String version) {
+ this.version = version;
+ }
+
+ public String getBetaVersion() {
+ return this.betaVersion;
+ }
+
+ public void setBetaVersion(String betaVersion) {
+ this.betaVersion = betaVersion;
+ }
+
+}
diff --git a/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..60b58e80808
--- /dev/null
+++ b/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,16 @@
+#
+# Copyright 2025-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.
+#
+org.springframework.ai.autoconfigure.anthropic.AnthropicAutoConfiguration
diff --git a/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/AnthropicAutoConfigurationIT.java b/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/AnthropicAutoConfigurationIT.java
new file mode 100644
index 00000000000..5b41f15aee7
--- /dev/null
+++ b/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/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.autoconfigure.anthropic;
+
+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/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/AnthropicPropertiesTests.java b/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/AnthropicPropertiesTests.java
new file mode 100644
index 00000000000..e2909971dd8
--- /dev/null
+++ b/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/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.autoconfigure.anthropic;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.anthropic.AnthropicChatModel;
+import org.springframework.ai.autoconfigure.retry.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/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/tool/FunctionCallWithFunctionBeanIT.java b/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/tool/FunctionCallWithFunctionBeanIT.java
new file mode 100644
index 00000000000..51e9e39be2f
--- /dev/null
+++ b/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/tool/FunctionCallWithFunctionBeanIT.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.autoconfigure.anthropic.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.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.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.model.function.FunctionCallingOptions;
+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 = "ANTHROPIC_API_KEY", matches = ".*")
+class FunctionCallWithFunctionBeanIT {
+
+ private final Logger logger = LoggerFactory.getLogger(FunctionCallWithFunctionBeanIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.anthropic.apiKey=" + System.getenv("ANTHROPIC_API_KEY"))
+ .withConfiguration(AutoConfigurations.of(AnthropicAutoConfiguration.class))
+ .withUserConfiguration(Config.class);
+
+ @Test
+ void functionCallTest() {
+
+ this.contextRunner
+ .withPropertyValues(
+ "spring.ai.anthropic.chat.options.model=" + AnthropicApi.ChatModel.CLAUDE_3_5_HAIKU.getValue())
+ .run(context -> {
+
+ AnthropicChatModel chatModel = context.getBean(AnthropicChatModel.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),
+ AnthropicChatOptions.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),
+ AnthropicChatOptions.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.anthropic.chat.options.model=" + AnthropicApi.ChatModel.CLAUDE_3_5_HAIKU.getValue())
+ .run(context -> {
+
+ AnthropicChatModel chatModel = context.getBean(AnthropicChatModel.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),
+ FunctionCallingOptions.builder().function("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. 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/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/tool/FunctionCallWithPromptFunctionIT.java b/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/tool/FunctionCallWithPromptFunctionIT.java
new file mode 100644
index 00000000000..8121991e93d
--- /dev/null
+++ b/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/tool/FunctionCallWithPromptFunctionIT.java
@@ -0,0 +1,76 @@
+/*
+ * 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.autoconfigure.anthropic.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.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.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 = "ANTHROPIC_API_KEY", matches = ".*")
+public class FunctionCallWithPromptFunctionIT {
+
+ private final Logger logger = LoggerFactory.getLogger(FunctionCallWithPromptFunctionIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.anthropic.apiKey=" + System.getenv("ANTHROPIC_API_KEY"))
+ .withConfiguration(AutoConfigurations.of(AnthropicAutoConfiguration.class));
+
+ @Test
+ void functionCallTest() {
+ this.contextRunner
+ .withPropertyValues(
+ "spring.ai.anthropic.chat.options.model=" + AnthropicApi.ChatModel.CLAUDE_3_5_HAIKU.getValue())
+ .run(context -> {
+
+ AnthropicChatModel chatModel = context.getBean(AnthropicChatModel.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 = AnthropicChatOptions.builder()
+ .functionCallbacks(
+ 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/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/tool/MockWeatherService.java b/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/tool/MockWeatherService.java
new file mode 100644
index 00000000000..4d39e27f447
--- /dev/null
+++ b/auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/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.autoconfigure.anthropic.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/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/pom.xml b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/pom.xml
new file mode 100644
index 00000000000..8a74bb11588
--- /dev/null
+++ b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/pom.xml
@@ -0,0 +1,93 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../../pom.xml
+
+ spring-ai-azure-openai-spring-boot-autoconfigure
+ jar
+ Spring AI Azure OpenAI Auto Configuration
+ Spring AI Azure OpenAI Auto Configuration
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-azure-openai
+ ${project.parent.version}
+ true
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-chat-client-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-chat-model-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-retry-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
diff --git a/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAIClientBuilderCustomizer.java b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAIClientBuilderCustomizer.java
new file mode 100644
index 00000000000..3a5c7624a03
--- /dev/null
+++ b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAIClientBuilderCustomizer.java
@@ -0,0 +1,21 @@
+package org.springframework.ai.autoconfigure.azure.openai;
+
+import com.azure.ai.openai.OpenAIClientBuilder;
+
+/**
+ * Callback interface that can be implemented by beans wishing to customize the
+ * {@link OpenAIClientBuilder} whilst retaining the default auto-configuration.
+ *
+ * @author Manuel Andreo Garcia
+ * @since 1.0.0-M6
+ */
+@FunctionalInterface
+public interface AzureOpenAIClientBuilderCustomizer {
+
+ /**
+ * Customize the {@link OpenAIClientBuilder}.
+ * @param clientBuilder the {@link OpenAIClientBuilder} to customize
+ */
+ void customize(OpenAIClientBuilder clientBuilder);
+
+}
diff --git a/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiAudioTranscriptionProperties.java b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiAudioTranscriptionProperties.java
new file mode 100644
index 00000000000..d7b949616f7
--- /dev/null
+++ b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiAudioTranscriptionProperties.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.autoconfigure.azure.openai;
+
+import org.springframework.ai.azure.openai.AzureOpenAiAudioTranscriptionOptions;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+/**
+ * Configuration properties for Azure OpenAI audio transcription.
+ *
+ * @author Piotr Olaszewski
+ */
+@ConfigurationProperties(AzureOpenAiAudioTranscriptionProperties.CONFIG_PREFIX)
+public class AzureOpenAiAudioTranscriptionProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.azure.openai.audio.transcription";
+
+ /**
+ * Enable AzureOpenAI audio transcription model.
+ */
+ private boolean enabled = true;
+
+ @NestedConfigurationProperty
+ private AzureOpenAiAudioTranscriptionOptions options = AzureOpenAiAudioTranscriptionOptions.builder().build();
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public AzureOpenAiAudioTranscriptionOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(AzureOpenAiAudioTranscriptionOptions options) {
+ this.options = options;
+ }
+
+}
diff --git a/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiAutoConfiguration.java b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiAutoConfiguration.java
new file mode 100644
index 00000000000..ae3b77efcc7
--- /dev/null
+++ b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiAutoConfiguration.java
@@ -0,0 +1,193 @@
+/*
+ * 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.autoconfigure.azure.openai;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import com.azure.ai.openai.OpenAIClientBuilder;
+import com.azure.core.credential.AzureKeyCredential;
+import com.azure.core.credential.KeyCredential;
+import com.azure.core.credential.TokenCredential;
+import com.azure.core.util.ClientOptions;
+import com.azure.core.util.Header;
+import io.micrometer.observation.ObservationRegistry;
+
+import org.springframework.ai.autoconfigure.chat.model.ToolCallingAutoConfiguration;
+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.azure.openai.AzureOpenAiImageModel;
+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.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
+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.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * {@link AutoConfiguration Auto-configuration} for Azure OpenAI.
+ *
+ * @author Piotr Olaszewski
+ * @author Soby Chacko
+ * @author Manuel Andreo Garcia
+ * @author Ilayaperumal Gopinathan
+ */
+@AutoConfiguration(after = { ToolCallingAutoConfiguration.class })
+@ConditionalOnClass({ OpenAIClientBuilder.class, AzureOpenAiChatModel.class })
+@EnableConfigurationProperties({ AzureOpenAiChatProperties.class, AzureOpenAiEmbeddingProperties.class,
+ AzureOpenAiConnectionProperties.class, AzureOpenAiImageOptionsProperties.class,
+ AzureOpenAiAudioTranscriptionProperties.class })
+@ImportAutoConfiguration(classes = { ToolCallingAutoConfiguration.class })
+public class AzureOpenAiAutoConfiguration {
+
+ private static final String APPLICATION_ID = "spring-ai";
+
+ @Bean
+ @ConditionalOnMissingBean // ({ OpenAIClient.class, TokenCredential.class })
+ public OpenAIClientBuilder openAIClientBuilder(AzureOpenAiConnectionProperties connectionProperties,
+ ObjectProvider customizers) {
+
+ if (StringUtils.hasText(connectionProperties.getApiKey())) {
+
+ Assert.hasText(connectionProperties.getEndpoint(), "Endpoint must not be empty");
+
+ Map customHeaders = connectionProperties.getCustomHeaders();
+ List headers = customHeaders.entrySet()
+ .stream()
+ .map(entry -> new Header(entry.getKey(), entry.getValue()))
+ .collect(Collectors.toList());
+ ClientOptions clientOptions = new ClientOptions().setApplicationId(APPLICATION_ID).setHeaders(headers);
+ OpenAIClientBuilder clientBuilder = new OpenAIClientBuilder().endpoint(connectionProperties.getEndpoint())
+ .credential(new AzureKeyCredential(connectionProperties.getApiKey()))
+ .clientOptions(clientOptions);
+ applyOpenAIClientBuilderCustomizers(clientBuilder, customizers);
+ return clientBuilder;
+ }
+
+ // Connect to OpenAI (e.g. not the Azure OpenAI). The deploymentName property is
+ // used as OpenAI model name.
+ if (StringUtils.hasText(connectionProperties.getOpenAiApiKey())) {
+ OpenAIClientBuilder clientBuilder = new OpenAIClientBuilder().endpoint("https://api.openai.com/v1")
+ .credential(new KeyCredential(connectionProperties.getOpenAiApiKey()))
+ .clientOptions(new ClientOptions().setApplicationId(APPLICATION_ID));
+ applyOpenAIClientBuilderCustomizers(clientBuilder, customizers);
+ return clientBuilder;
+ }
+
+ throw new IllegalArgumentException("Either API key or OpenAI API key must not be empty");
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnBean(TokenCredential.class)
+ public OpenAIClientBuilder openAIClientWithTokenCredential(AzureOpenAiConnectionProperties connectionProperties,
+ TokenCredential tokenCredential, ObjectProvider customizers) {
+
+ Assert.notNull(tokenCredential, "TokenCredential must not be null");
+ Assert.hasText(connectionProperties.getEndpoint(), "Endpoint must not be empty");
+
+ OpenAIClientBuilder clientBuilder = new OpenAIClientBuilder().endpoint(connectionProperties.getEndpoint())
+ .credential(tokenCredential)
+ .clientOptions(new ClientOptions().setApplicationId(APPLICATION_ID));
+ applyOpenAIClientBuilderCustomizers(clientBuilder, customizers);
+ return clientBuilder;
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = AzureOpenAiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
+ matchIfMissing = true)
+ public AzureOpenAiChatModel azureOpenAiChatModel(OpenAIClientBuilder openAIClientBuilder,
+ AzureOpenAiChatProperties chatProperties, ToolCallingManager toolCallingManager,
+ ObjectProvider observationRegistry,
+ ObjectProvider observationConvention) {
+
+ var chatModel = AzureOpenAiChatModel.builder()
+ .openAIClientBuilder(openAIClientBuilder)
+ .defaultOptions(chatProperties.getOptions())
+ .toolCallingManager(toolCallingManager)
+ .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP))
+ .build();
+ observationConvention.ifAvailable(chatModel::setObservationConvention);
+
+ return chatModel;
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = AzureOpenAiEmbeddingProperties.CONFIG_PREFIX, name = "enabled",
+ havingValue = "true", matchIfMissing = true)
+ public AzureOpenAiEmbeddingModel azureOpenAiEmbeddingModel(OpenAIClientBuilder openAIClient,
+ AzureOpenAiEmbeddingProperties embeddingProperties, ObjectProvider observationRegistry,
+ ObjectProvider observationConvention) {
+
+ var embeddingModel = new AzureOpenAiEmbeddingModel(openAIClient.buildClient(),
+ embeddingProperties.getMetadataMode(), embeddingProperties.getOptions(),
+ observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP));
+
+ observationConvention.ifAvailable(embeddingModel::setObservationConvention);
+
+ return embeddingModel;
+
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) {
+ DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver();
+ manager.setApplicationContext(context);
+ return manager;
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = AzureOpenAiImageOptionsProperties.CONFIG_PREFIX, name = "enabled",
+ havingValue = "true", matchIfMissing = true)
+ public AzureOpenAiImageModel azureOpenAiImageClient(OpenAIClientBuilder openAIClientBuilder,
+ AzureOpenAiImageOptionsProperties imageProperties) {
+
+ return new AzureOpenAiImageModel(openAIClientBuilder.buildClient(), imageProperties.getOptions());
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = AzureOpenAiAudioTranscriptionProperties.CONFIG_PREFIX, name = "enabled",
+ havingValue = "true", matchIfMissing = true)
+ public AzureOpenAiAudioTranscriptionModel azureOpenAiAudioTranscriptionModel(OpenAIClientBuilder openAIClient,
+ AzureOpenAiAudioTranscriptionProperties audioProperties) {
+ return new AzureOpenAiAudioTranscriptionModel(openAIClient.buildClient(), audioProperties.getOptions());
+ }
+
+ private void applyOpenAIClientBuilderCustomizers(OpenAIClientBuilder clientBuilder,
+ ObjectProvider customizers) {
+ customizers.orderedStream().forEach(customizer -> customizer.customize(clientBuilder));
+ }
+
+}
diff --git a/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiChatProperties.java b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiChatProperties.java
new file mode 100644
index 00000000000..3323bafeae5
--- /dev/null
+++ b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiChatProperties.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.autoconfigure.azure.openai;
+
+import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+@ConfigurationProperties(AzureOpenAiChatProperties.CONFIG_PREFIX)
+public class AzureOpenAiChatProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.azure.openai.chat";
+
+ public static final String DEFAULT_DEPLOYMENT_NAME = "gpt-4o";
+
+ private static final Double DEFAULT_TEMPERATURE = 0.7;
+
+ /**
+ * Enable Azure OpenAI chat model.
+ */
+ private boolean enabled = true;
+
+ @NestedConfigurationProperty
+ private AzureOpenAiChatOptions options = AzureOpenAiChatOptions.builder()
+ .deploymentName(DEFAULT_DEPLOYMENT_NAME)
+ .temperature(DEFAULT_TEMPERATURE)
+ .build();
+
+ public AzureOpenAiChatOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(AzureOpenAiChatOptions options) {
+ this.options = options;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+}
diff --git a/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiConnectionProperties.java b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiConnectionProperties.java
new file mode 100644
index 00000000000..6ede4e8b323
--- /dev/null
+++ b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiConnectionProperties.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.autoconfigure.azure.openai;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties(AzureOpenAiConnectionProperties.CONFIG_PREFIX)
+public class AzureOpenAiConnectionProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.azure.openai";
+
+ /**
+ * Azure OpenAI API key. From the Azure AI OpenAI `Keys and Endpoint` section under
+ * `Resource Management`.
+ */
+ private String apiKey;
+
+ /**
+ * (non Azure) OpenAI API key. Used to authenticate with the OpenAI service, instead
+ * of Azure OpenAI. This automatically sets the endpoint to https://api.openai.com/v1.
+ */
+ private String openAiApiKey;
+
+ /**
+ * Azure OpenAI API endpoint. From the Azure AI OpenAI `Keys and Endpoint` section
+ * under `Resource Management`.
+ */
+ private String endpoint;
+
+ private Map customHeaders = new HashMap<>();
+
+ public String getEndpoint() {
+ return this.endpoint;
+ }
+
+ public void setEndpoint(String endpoint) {
+ this.endpoint = endpoint;
+ }
+
+ public String getApiKey() {
+ return this.apiKey;
+ }
+
+ public void setApiKey(String apiKey) {
+ this.apiKey = apiKey;
+ }
+
+ public String getOpenAiApiKey() {
+ return this.openAiApiKey;
+ }
+
+ public void setOpenAiApiKey(String openAiApiKey) {
+ this.openAiApiKey = openAiApiKey;
+ }
+
+ public Map getCustomHeaders() {
+ return this.customHeaders;
+ }
+
+ public void setCustomHeaders(Map customHeaders) {
+ this.customHeaders = customHeaders;
+ }
+
+}
diff --git a/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiEmbeddingProperties.java b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiEmbeddingProperties.java
new file mode 100644
index 00000000000..f4e9c46799d
--- /dev/null
+++ b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiEmbeddingProperties.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.autoconfigure.azure.openai;
+
+import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingOptions;
+import org.springframework.ai.document.MetadataMode;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+import org.springframework.util.Assert;
+
+@ConfigurationProperties(AzureOpenAiEmbeddingProperties.CONFIG_PREFIX)
+public class AzureOpenAiEmbeddingProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.azure.openai.embedding";
+
+ /**
+ * Enable Azure OpenAI embedding model.
+ */
+ private boolean enabled = true;
+
+ @NestedConfigurationProperty
+ private AzureOpenAiEmbeddingOptions options = AzureOpenAiEmbeddingOptions.builder()
+ .deploymentName("text-embedding-ada-002")
+ .build();
+
+ private MetadataMode metadataMode = MetadataMode.EMBED;
+
+ public AzureOpenAiEmbeddingOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(AzureOpenAiEmbeddingOptions options) {
+ Assert.notNull(options, "Options must not be null");
+ this.options = options;
+ }
+
+ public MetadataMode getMetadataMode() {
+ return this.metadataMode;
+ }
+
+ public void setMetadataMode(MetadataMode metadataMode) {
+ Assert.notNull(metadataMode, "Metadata mode must not be null");
+ this.metadataMode = metadataMode;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+}
diff --git a/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiImageOptionsProperties.java b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiImageOptionsProperties.java
new file mode 100644
index 00000000000..4aea1459f4c
--- /dev/null
+++ b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/azure/openai/AzureOpenAiImageOptionsProperties.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.autoconfigure.azure.openai;
+
+import org.springframework.ai.azure.openai.AzureOpenAiImageOptions;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+/**
+ * Configuration properties for Azure OpenAI image generation options.
+ *
+ * @author Benoit Moussaud
+ * @since 1.0.0 M1
+ */
+@ConfigurationProperties(AzureOpenAiImageOptionsProperties.CONFIG_PREFIX)
+public class AzureOpenAiImageOptionsProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.azure.openai.image";
+
+ /**
+ * Enable Azure OpenAI chat client.
+ */
+ private boolean enabled = true;
+
+ @NestedConfigurationProperty
+ private AzureOpenAiImageOptions options = AzureOpenAiImageOptions.builder().build();
+
+ public AzureOpenAiImageOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(AzureOpenAiImageOptions options) {
+ this.options = options;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+}
diff --git a/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json
new file mode 100644
index 00000000000..b547459b8a9
--- /dev/null
+++ b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -0,0 +1,23 @@
+{
+ "groups": [
+ {
+ "name": "spring.ai.azure.openai.chat.options.enhancements",
+ "type": "com.azure.ai.openai.models.AzureChatEnhancementConfiguration",
+ "sourceType": "org.springframework.ai.azure.openai.AzureOpenAiChatOptions",
+ "sourceMethod": "getEnhancements()"
+ }
+ ],
+ "properties": [
+ {
+ "name": "spring.ai.azure.openai.chat.options.enhancements.grounding",
+ "type": "com.azure.ai.openai.models.AzureChatGroundingEnhancementConfiguration",
+ "sourceType": "com.azure.ai.openai.models.AzureChatEnhancementConfiguration"
+ },
+ {
+ "name": "spring.ai.azure.openai.chat.options.enhancements.ocr",
+ "type": "com.azure.ai.openai.models.AzureChatOCREnhancementConfiguration",
+ "sourceType": "com.azure.ai.openai.models.AzureChatEnhancementConfiguration"
+ }
+ ],
+ "hints": []
+}
diff --git a/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..dc0ed698402
--- /dev/null
+++ b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,16 @@
+#
+# Copyright 2025-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.
+#
+org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiAutoConfiguration
diff --git a/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/AzureOpenAiAutoConfigurationIT.java b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/AzureOpenAiAutoConfigurationIT.java
new file mode 100644
index 00000000000..73cf13eabf5
--- /dev/null
+++ b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/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.autoconfigure.azure;
+
+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.autoconfigure.azure.openai.AzureOpenAIClientBuilderCustomizer;
+
+import reactor.core.publisher.Flux;
+
+import org.springframework.ai.autoconfigure.azure.openai.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/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/AzureOpenAiAutoConfigurationPropertyTests.java b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/AzureOpenAiAutoConfigurationPropertyTests.java
new file mode 100644
index 00000000000..e83c75e2327
--- /dev/null
+++ b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/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.autoconfigure.azure;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiAutoConfiguration;
+import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties;
+import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiConnectionProperties;
+import org.springframework.ai.autoconfigure.azure.openai.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/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/AzureOpenAiDirectOpenAiAutoConfigurationIT.java b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/AzureOpenAiDirectOpenAiAutoConfigurationIT.java
new file mode 100644
index 00000000000..88de524d13a
--- /dev/null
+++ b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/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.autoconfigure.azure;
+
+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.autoconfigure.azure.openai.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/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/tool/DeploymentNameUtil.java b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/tool/DeploymentNameUtil.java
new file mode 100644
index 00000000000..435fc9235bb
--- /dev/null
+++ b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/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.autoconfigure.azure.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/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/tool/FunctionCallWithFunctionBeanIT.java b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/tool/FunctionCallWithFunctionBeanIT.java
new file mode 100644
index 00000000000..e3b29e75cda
--- /dev/null
+++ b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/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.autoconfigure.azure.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.autoconfigure.azure.openai.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="
+ + org.springframework.ai.autoconfigure.azure.tool.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="
+ + org.springframework.ai.autoconfigure.azure.tool.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/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/tool/FunctionCallWithFunctionWrapperIT.java b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/tool/FunctionCallWithFunctionWrapperIT.java
new file mode 100644
index 00000000000..7df9cc89d80
--- /dev/null
+++ b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/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.autoconfigure.azure.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.autoconfigure.azure.openai.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="
+ + org.springframework.ai.autoconfigure.azure.tool.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/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/tool/FunctionCallWithPromptFunctionIT.java b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/tool/FunctionCallWithPromptFunctionIT.java
new file mode 100644
index 00000000000..8a50160dc77
--- /dev/null
+++ b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/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.autoconfigure.azure.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.autoconfigure.azure.openai.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="
+ + org.springframework.ai.autoconfigure.azure.tool.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/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/tool/MockWeatherService.java b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/tool/MockWeatherService.java
new file mode 100644
index 00000000000..a6d2658ea0d
--- /dev/null
+++ b/auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/azure/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.autoconfigure.azure.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/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/pom.xml b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/pom.xml
new file mode 100644
index 00000000000..87da575efea
--- /dev/null
+++ b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/pom.xml
@@ -0,0 +1,100 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../../pom.xml
+
+ spring-ai-bedrock-spring-boot-autoconfigure
+ jar
+ Spring AI Bedrock Auto Configuration
+ Spring AI Bedrock Auto Configuration
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-bedrock
+ ${project.parent.version}
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-bedrock-converse
+ ${project.parent.version}
+ true
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-chat-client-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-chat-model-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-retry-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
diff --git a/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/BedrockAwsConnectionConfiguration.java b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/BedrockAwsConnectionConfiguration.java
new file mode 100644
index 00000000000..b083597e6ce
--- /dev/null
+++ b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/BedrockAwsConnectionConfiguration.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.autoconfigure.bedrock;
+
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
+import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.regions.providers.AwsRegionProvider;
+import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain;
+
+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.Configuration;
+import org.springframework.util.StringUtils;
+
+/**
+ * {@link Configuration} for AWS connection.
+ *
+ * @author Christian Tzolov
+ * @author Wei Jiang
+ */
+@Configuration
+@EnableConfigurationProperties({ BedrockAwsConnectionProperties.class })
+public class BedrockAwsConnectionConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ public AwsCredentialsProvider credentialsProvider(BedrockAwsConnectionProperties properties) {
+
+ if (StringUtils.hasText(properties.getAccessKey()) && StringUtils.hasText(properties.getSecretKey())) {
+
+ if (StringUtils.hasText(properties.getSessionToken())) {
+ return StaticCredentialsProvider.create(AwsSessionCredentials.create(properties.getAccessKey(),
+ properties.getSecretKey(), properties.getSessionToken()));
+ }
+
+ return StaticCredentialsProvider
+ .create(AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey()));
+ }
+
+ return DefaultCredentialsProvider.create();
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public AwsRegionProvider regionProvider(BedrockAwsConnectionProperties properties) {
+
+ if (StringUtils.hasText(properties.getRegion())) {
+ return new StaticRegionProvider(properties.getRegion());
+ }
+
+ return DefaultAwsRegionProviderChain.builder().build();
+ }
+
+ static class StaticRegionProvider implements AwsRegionProvider {
+
+ private final Region region;
+
+ StaticRegionProvider(String region) {
+ try {
+ this.region = Region.of(region);
+ }
+ catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("The region '" + region + "' is not a valid region!", e);
+ }
+ }
+
+ @Override
+ public Region getRegion() {
+ return this.region;
+ }
+
+ }
+
+}
diff --git a/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/BedrockAwsConnectionProperties.java b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/BedrockAwsConnectionProperties.java
new file mode 100644
index 00000000000..14c6be535fb
--- /dev/null
+++ b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/BedrockAwsConnectionProperties.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.autoconfigure.bedrock;
+
+import java.time.Duration;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Configuration properties for Bedrock AWS connection.
+ *
+ * @author Christian Tzolov
+ * @since 0.8.0
+ */
+@ConfigurationProperties(BedrockAwsConnectionProperties.CONFIG_PREFIX)
+public class BedrockAwsConnectionProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.bedrock.aws";
+
+ /**
+ * AWS region to use. Defaults to us-east-1.
+ */
+ private String region = "us-east-1";
+
+ /**
+ * AWS access key.
+ */
+ private String accessKey;
+
+ /**
+ * AWS secret key.
+ */
+ private String secretKey;
+
+ /**
+ * AWS session token. (optional) When provided the AwsSessionCredentials are used.
+ * Otherwise the AwsBasicCredentials are used.
+ */
+ private String sessionToken;
+
+ /**
+ * Set model timeout, Defaults 5 min.
+ */
+ private Duration timeout = Duration.ofMinutes(5L);
+
+ public String getRegion() {
+ return this.region;
+ }
+
+ public void setRegion(String awsRegion) {
+ this.region = awsRegion;
+ }
+
+ public String getAccessKey() {
+ return this.accessKey;
+ }
+
+ public void setAccessKey(String accessKey) {
+ this.accessKey = accessKey;
+ }
+
+ public String getSecretKey() {
+ return this.secretKey;
+ }
+
+ public void setSecretKey(String secretKey) {
+ this.secretKey = secretKey;
+ }
+
+ public Duration getTimeout() {
+ return this.timeout;
+ }
+
+ public void setTimeout(Duration timeout) {
+ this.timeout = timeout;
+ }
+
+ public String getSessionToken() {
+ return this.sessionToken;
+ }
+
+ public void setSessionToken(String sessionToken) {
+ this.sessionToken = sessionToken;
+ }
+
+}
diff --git a/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/cohere/BedrockCohereEmbeddingAutoConfiguration.java b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/cohere/BedrockCohereEmbeddingAutoConfiguration.java
new file mode 100644
index 00000000000..86ba3f76b3d
--- /dev/null
+++ b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/cohere/BedrockCohereEmbeddingAutoConfiguration.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.autoconfigure.bedrock.cohere;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.regions.providers.AwsRegionProvider;
+
+import org.springframework.ai.autoconfigure.bedrock.BedrockAwsConnectionConfiguration;
+import org.springframework.ai.autoconfigure.bedrock.BedrockAwsConnectionProperties;
+import org.springframework.ai.bedrock.cohere.BedrockCohereEmbeddingModel;
+import org.springframework.ai.bedrock.cohere.api.CohereEmbeddingBedrockApi;
+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 Cohere Embedding Model.
+ *
+ * @author Christian Tzolov
+ * @author Wei Jiang
+ * @since 0.8.0
+ */
+@AutoConfiguration
+@ConditionalOnClass(CohereEmbeddingBedrockApi.class)
+@EnableConfigurationProperties({ BedrockCohereEmbeddingProperties.class, BedrockAwsConnectionProperties.class })
+@ConditionalOnProperty(prefix = BedrockCohereEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true")
+@Import(BedrockAwsConnectionConfiguration.class)
+public class BedrockCohereEmbeddingAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class })
+ public CohereEmbeddingBedrockApi cohereEmbeddingApi(AwsCredentialsProvider credentialsProvider,
+ AwsRegionProvider regionProvider, BedrockCohereEmbeddingProperties properties,
+ BedrockAwsConnectionProperties awsProperties, ObjectMapper objectMapper) {
+ return new CohereEmbeddingBedrockApi(properties.getModel(), credentialsProvider, regionProvider.getRegion(),
+ objectMapper, awsProperties.getTimeout());
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnBean(CohereEmbeddingBedrockApi.class)
+ public BedrockCohereEmbeddingModel cohereEmbeddingModel(CohereEmbeddingBedrockApi cohereEmbeddingApi,
+ BedrockCohereEmbeddingProperties properties) {
+
+ return new BedrockCohereEmbeddingModel(cohereEmbeddingApi, properties.getOptions());
+ }
+
+}
diff --git a/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/cohere/BedrockCohereEmbeddingProperties.java b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/cohere/BedrockCohereEmbeddingProperties.java
new file mode 100644
index 00000000000..f6f6b3a957b
--- /dev/null
+++ b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/cohere/BedrockCohereEmbeddingProperties.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.autoconfigure.bedrock.cohere;
+
+import org.springframework.ai.bedrock.cohere.BedrockCohereEmbeddingOptions;
+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.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+/**
+ * Bedrock Cohere Embedding autoconfiguration properties.
+ *
+ * @author Christian Tzolov
+ * @since 0.8.0
+ */
+@ConfigurationProperties(BedrockCohereEmbeddingProperties.CONFIG_PREFIX)
+public class BedrockCohereEmbeddingProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.bedrock.cohere.embedding";
+
+ /**
+ * Enable Bedrock Cohere Embedding Model. False by default.
+ */
+ private boolean enabled = false;
+
+ /**
+ * Bedrock Cohere Embedding generative name. Defaults to
+ * 'cohere.embed-multilingual-v3'.
+ */
+ private String model = CohereEmbeddingModel.COHERE_EMBED_MULTILINGUAL_V3.id();
+
+ @NestedConfigurationProperty
+ private BedrockCohereEmbeddingOptions options = BedrockCohereEmbeddingOptions.builder()
+ .inputType(InputType.SEARCH_DOCUMENT)
+ .truncate(CohereEmbeddingRequest.Truncate.NONE)
+ .build();
+
+ 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 BedrockCohereEmbeddingOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(BedrockCohereEmbeddingOptions options) {
+ this.options = options;
+ }
+
+}
diff --git a/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/converse/BedrockConverseProxyChatAutoConfiguration.java b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/converse/BedrockConverseProxyChatAutoConfiguration.java
new file mode 100644
index 00000000000..e732d106286
--- /dev/null
+++ b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/converse/BedrockConverseProxyChatAutoConfiguration.java
@@ -0,0 +1,97 @@
+/*
+ * 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.autoconfigure.bedrock.converse;
+
+import io.micrometer.observation.ObservationRegistry;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.regions.providers.AwsRegionProvider;
+import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeAsyncClient;
+import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient;
+
+import org.springframework.ai.autoconfigure.bedrock.BedrockAwsConnectionConfiguration;
+import org.springframework.ai.autoconfigure.bedrock.BedrockAwsConnectionProperties;
+import org.springframework.ai.autoconfigure.chat.model.ToolCallingAutoConfiguration;
+import org.springframework.ai.bedrock.converse.BedrockProxyChatModel;
+import org.springframework.ai.chat.observation.ChatModelObservationConvention;
+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.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.ApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Import;
+
+/**
+ * {@link AutoConfiguration Auto-configuration} for Bedrock Converse Proxy Chat Client.
+ *
+ * Leverages the Spring Cloud AWS to resolve the {@link AwsCredentialsProvider}.
+ *
+ * @author Christian Tzolov
+ * @author Wei Jiang
+ */
+@AutoConfiguration(after = { ToolCallingAutoConfiguration.class })
+@EnableConfigurationProperties({ BedrockConverseProxyChatProperties.class, BedrockAwsConnectionConfiguration.class })
+@ConditionalOnClass({ BedrockProxyChatModel.class, BedrockRuntimeClient.class, BedrockRuntimeAsyncClient.class })
+@ConditionalOnProperty(prefix = BedrockConverseProxyChatProperties.CONFIG_PREFIX, name = "enabled",
+ havingValue = "true", matchIfMissing = true)
+@Import(BedrockAwsConnectionConfiguration.class)
+@ImportAutoConfiguration({ ToolCallingAutoConfiguration.class })
+public class BedrockConverseProxyChatAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnBean({ AwsCredentialsProvider.class, AwsRegionProvider.class })
+ public BedrockProxyChatModel bedrockProxyChatModel(AwsCredentialsProvider credentialsProvider,
+ AwsRegionProvider regionProvider, BedrockAwsConnectionProperties connectionProperties,
+ BedrockConverseProxyChatProperties chatProperties, ToolCallingManager toolCallingManager,
+ ObjectProvider observationRegistry,
+ ObjectProvider observationConvention,
+ ObjectProvider bedrockRuntimeClient,
+ ObjectProvider 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/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/converse/BedrockConverseProxyChatProperties.java b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/converse/BedrockConverseProxyChatProperties.java
new file mode 100644
index 00000000000..128eaf71b02
--- /dev/null
+++ b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/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.autoconfigure.bedrock.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/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/titan/BedrockTitanEmbeddingAutoConfiguration.java b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/titan/BedrockTitanEmbeddingAutoConfiguration.java
new file mode 100644
index 00000000000..96c6cfa8c18
--- /dev/null
+++ b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/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.autoconfigure.bedrock.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.autoconfigure.bedrock.BedrockAwsConnectionConfiguration;
+import org.springframework.ai.autoconfigure.bedrock.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/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/titan/BedrockTitanEmbeddingProperties.java b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/titan/BedrockTitanEmbeddingProperties.java
new file mode 100644
index 00000000000..b0c1e604861
--- /dev/null
+++ b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/bedrock/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.autoconfigure.bedrock.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/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..3e29358e68d
--- /dev/null
+++ b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,18 @@
+#
+# Copyright 2025-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.
+#
+org.springframework.ai.autoconfigure.bedrock.cohere.BedrockCohereEmbeddingAutoConfiguration
+org.springframework.ai.autoconfigure.bedrock.titan.BedrockTitanEmbeddingAutoConfiguration
+org.springframework.ai.autoconfigure.bedrock.converse.BedrockConverseProxyChatAutoConfiguration
diff --git a/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/BedrockAwsConnectionConfigurationIT.java b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/BedrockAwsConnectionConfigurationIT.java
new file mode 100644
index 00000000000..70d480bc150
--- /dev/null
+++ b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/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.autoconfigure.bedrock;
+
+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/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/BedrockTestUtils.java b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/BedrockTestUtils.java
new file mode 100644
index 00000000000..991da658134
--- /dev/null
+++ b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/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.autoconfigure.bedrock;
+
+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/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/RequiresAwsCredentials.java b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/RequiresAwsCredentials.java
new file mode 100644
index 00000000000..006cb4e1690
--- /dev/null
+++ b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/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.autoconfigure.bedrock;
+
+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/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/cohere/BedrockCohereEmbeddingAutoConfigurationIT.java b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/cohere/BedrockCohereEmbeddingAutoConfigurationIT.java
new file mode 100644
index 00000000000..0ed6cddf4ea
--- /dev/null
+++ b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/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.autoconfigure.bedrock.cohere;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.regions.Region;
+
+import org.springframework.ai.autoconfigure.bedrock.BedrockAwsConnectionProperties;
+import org.springframework.ai.autoconfigure.bedrock.BedrockTestUtils;
+import org.springframework.ai.autoconfigure.bedrock.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/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/converse/BedrockConverseProxyChatAutoConfigurationIT.java b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/converse/BedrockConverseProxyChatAutoConfigurationIT.java
new file mode 100644
index 00000000000..936f4b59f6e
--- /dev/null
+++ b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/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.autoconfigure.bedrock.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.autoconfigure.bedrock.BedrockTestUtils;
+import org.springframework.ai.autoconfigure.bedrock.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/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/converse/BedrockConverseProxyChatPropertiesTests.java b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/converse/BedrockConverseProxyChatPropertiesTests.java
new file mode 100644
index 00000000000..4b4bb809a44
--- /dev/null
+++ b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/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.autoconfigure.bedrock.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/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/converse/tool/FunctionCallWithFunctionBeanIT.java b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/converse/tool/FunctionCallWithFunctionBeanIT.java
new file mode 100644
index 00000000000..6344b3e81fd
--- /dev/null
+++ b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/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.autoconfigure.bedrock.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.autoconfigure.bedrock.BedrockTestUtils;
+import org.springframework.ai.autoconfigure.bedrock.RequiresAwsCredentials;
+import org.springframework.ai.autoconfigure.bedrock.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/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/converse/tool/FunctionCallWithPromptFunctionIT.java b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/converse/tool/FunctionCallWithPromptFunctionIT.java
new file mode 100644
index 00000000000..62af91d2996
--- /dev/null
+++ b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/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.autoconfigure.bedrock.converse.tool;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.ai.autoconfigure.bedrock.BedrockTestUtils;
+import org.springframework.ai.autoconfigure.bedrock.RequiresAwsCredentials;
+import org.springframework.ai.autoconfigure.bedrock.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/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/converse/tool/MockWeatherService.java b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/converse/tool/MockWeatherService.java
new file mode 100644
index 00000000000..176d42f66b3
--- /dev/null
+++ b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/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.autoconfigure.bedrock.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/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/titan/BedrockTitanEmbeddingAutoConfigurationIT.java b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/titan/BedrockTitanEmbeddingAutoConfigurationIT.java
new file mode 100644
index 00000000000..31ca213da9c
--- /dev/null
+++ b/auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/bedrock/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.autoconfigure.bedrock.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.autoconfigure.bedrock.BedrockAwsConnectionProperties;
+import org.springframework.ai.autoconfigure.bedrock.BedrockTestUtils;
+import org.springframework.ai.autoconfigure.bedrock.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/auto-configurations/models/spring-ai-huggingface-spring-boot-autoconfigure/pom.xml b/auto-configurations/models/spring-ai-huggingface-spring-boot-autoconfigure/pom.xml
new file mode 100644
index 00000000000..beb6698f19d
--- /dev/null
+++ b/auto-configurations/models/spring-ai-huggingface-spring-boot-autoconfigure/pom.xml
@@ -0,0 +1,86 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../../pom.xml
+
+ spring-ai-huggingface-spring-boot-autoconfigure
+ jar
+ Spring AI Huggingface Auto Configuration
+ Spring AI Huggingface Auto Configuration
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-huggingface
+ ${project.parent.version}
+ true
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-chat-client-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.ai
+ spring-ai-openai-spring-boot-autoconfigure
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
diff --git a/auto-configurations/models/spring-ai-huggingface-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/huggingface/HuggingfaceChatAutoConfiguration.java b/auto-configurations/models/spring-ai-huggingface-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/huggingface/HuggingfaceChatAutoConfiguration.java
new file mode 100644
index 00000000000..eaf74d75f86
--- /dev/null
+++ b/auto-configurations/models/spring-ai-huggingface-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/huggingface/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.autoconfigure.huggingface;
+
+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/auto-configurations/models/spring-ai-huggingface-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/huggingface/HuggingfaceChatProperties.java b/auto-configurations/models/spring-ai-huggingface-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/huggingface/HuggingfaceChatProperties.java
new file mode 100644
index 00000000000..ff7fda7b066
--- /dev/null
+++ b/auto-configurations/models/spring-ai-huggingface-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/huggingface/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.autoconfigure.huggingface;
+
+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/auto-configurations/models/spring-ai-huggingface-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-huggingface-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..a084b2ad18d
--- /dev/null
+++ b/auto-configurations/models/spring-ai-huggingface-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,16 @@
+#
+# Copyright 2025-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.
+#
+org.springframework.ai.autoconfigure.huggingface.HuggingfaceChatAutoConfiguration
diff --git a/auto-configurations/models/spring-ai-huggingface-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/huggingface/HuggingfaceChatAutoConfigurationIT.java b/auto-configurations/models/spring-ai-huggingface-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/huggingface/HuggingfaceChatAutoConfigurationIT.java
new file mode 100644
index 00000000000..e3980a13ab0
--- /dev/null
+++ b/auto-configurations/models/spring-ai-huggingface-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/huggingface/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.autoconfigure.huggingface;
+
+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/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/pom.xml b/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/pom.xml
new file mode 100644
index 00000000000..e8535d75dbd
--- /dev/null
+++ b/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/pom.xml
@@ -0,0 +1,93 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../../pom.xml
+
+ spring-ai-minimax-spring-boot-autoconfigure
+ jar
+ Spring AI Minimax Auto Configuration
+ Spring AI Minimax Auto Configuration
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-minimax
+ ${project.parent.version}
+ true
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-chat-client-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-chat-model-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-retry-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
diff --git a/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/minimax/MiniMaxAutoConfiguration.java b/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/minimax/MiniMaxAutoConfiguration.java
new file mode 100644
index 00000000000..b47536a6cdf
--- /dev/null
+++ b/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/minimax/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.autoconfigure.minimax;
+
+import java.util.List;
+
+import io.micrometer.observation.ObservationRegistry;
+
+import org.springframework.ai.autoconfigure.retry.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/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/minimax/MiniMaxChatProperties.java b/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/minimax/MiniMaxChatProperties.java
new file mode 100644
index 00000000000..89c0e0258a0
--- /dev/null
+++ b/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/minimax/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.autoconfigure.minimax;
+
+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/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/minimax/MiniMaxConnectionProperties.java b/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/minimax/MiniMaxConnectionProperties.java
new file mode 100644
index 00000000000..59d5ff0cadf
--- /dev/null
+++ b/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/minimax/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.autoconfigure.minimax;
+
+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/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/minimax/MiniMaxEmbeddingProperties.java b/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/minimax/MiniMaxEmbeddingProperties.java
new file mode 100644
index 00000000000..13293a71b77
--- /dev/null
+++ b/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/minimax/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.autoconfigure.minimax;
+
+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/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/minimax/MiniMaxParentProperties.java b/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/minimax/MiniMaxParentProperties.java
new file mode 100644
index 00000000000..34b98c0c122
--- /dev/null
+++ b/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/minimax/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.autoconfigure.minimax;
+
+/**
+ * @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/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..7eadaf1d2ad
--- /dev/null
+++ b/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,16 @@
+#
+# Copyright 2025-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.
+#
+org.springframework.ai.autoconfigure.minimax.MiniMaxAutoConfiguration
diff --git a/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackInPromptIT.java b/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackInPromptIT.java
new file mode 100644
index 00000000000..5e9c88ad9ff
--- /dev/null
+++ b/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/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.autoconfigure.minimax;
+
+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.autoconfigure.retry.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/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackWithPlainFunctionBeanIT.java b/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackWithPlainFunctionBeanIT.java
new file mode 100644
index 00000000000..bedb41f5d7b
--- /dev/null
+++ b/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/FunctionCallbackWithPlainFunctionBeanIT.java
@@ -0,0 +1,177 @@
+/*
+ * 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.autoconfigure.minimax;
+
+import java.util.List;
+import java.util.function.Function;
+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.autoconfigure.retry.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.FunctionCallingOptions;
+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 org.springframework.context.annotation.Description;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Geng Rong
+ */
+@EnabledIfEnvironmentVariable(named = "MINIMAX_API_KEY", matches = ".*")
+class FunctionCallbackWithPlainFunctionBeanIT {
+
+ private final Logger logger = LoggerFactory.getLogger(FunctionCallbackWithPlainFunctionBeanIT.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);
+
+ // FIXME: multiple function calls may stop prematurely due to model performance
+ @Test
+ void functionCallTest() {
+ this.contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6.5s-chat").run(context -> {
+
+ MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class);
+
+ // Test weatherFunction
+ 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("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),
+ MiniMaxChatOptions.builder().function("weatherFunctionTwo").build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+
+ });
+ }
+
+ @Test
+ void functionCallWithPortableFunctionCallingOptions() {
+ this.contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6.5s-chat").run(context -> {
+
+ MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class);
+
+ // Test weatherFunction
+ UserMessage userMessage = new UserMessage(
+ "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius.");
+
+ FunctionCallingOptions functionOptions = FunctionCallingOptions.builder()
+ .function("weatherFunction")
+ .build();
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), functionOptions));
+
+ logger.info("Response: {}", response);
+ });
+ }
+
+ // FIXME: multiple function calls may stop prematurely due to model performance
+ @Test
+ void streamFunctionCallTest() {
+ this.contextRunner.withPropertyValues("spring.ai.minimax.chat.options.model=abab6.5s-chat").run(context -> {
+
+ MiniMaxChatModel chatModel = context.getBean(MiniMaxChatModel.class);
+
+ // Test weatherFunction
+ 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("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).containsAnyOf("30.0", "30");
+ assertThat(content).containsAnyOf("10.0", "10");
+ assertThat(content).containsAnyOf("15.0", "15");
+
+ // Test weatherFunctionTwo
+ response = chatModel.stream(new Prompt(List.of(userMessage),
+ MiniMaxChatOptions.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).containsAnyOf("30.0", "30");
+ assertThat(content).containsAnyOf("10.0", "10");
+ assertThat(content).containsAnyOf("15.0", "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 weatherFunctionTwo() {
+ MockWeatherService weatherService = new MockWeatherService();
+ return (weatherService::apply);
+ }
+
+ }
+
+}
diff --git a/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/MiniMaxAutoConfigurationIT.java b/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/MiniMaxAutoConfigurationIT.java
new file mode 100644
index 00000000000..6a3893274a6
--- /dev/null
+++ b/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/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.autoconfigure.minimax;
+
+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.autoconfigure.retry.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/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/MiniMaxFunctionCallbackIT.java b/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/MiniMaxFunctionCallbackIT.java
new file mode 100644
index 00000000000..c3937895cdf
--- /dev/null
+++ b/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/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.autoconfigure.minimax;
+
+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.autoconfigure.retry.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/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/MiniMaxPropertiesTests.java b/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/MiniMaxPropertiesTests.java
new file mode 100644
index 00000000000..4140f029b4b
--- /dev/null
+++ b/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/MiniMaxPropertiesTests.java
@@ -0,0 +1,331 @@
+/*
+ * 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.autoconfigure.minimax;
+
+import org.junit.jupiter.api.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.skyscreamer.jsonassert.JSONCompareMode;
+
+import org.springframework.ai.autoconfigure.retry.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 org.springframework.ai.autoconfigure.minimax.MiniMaxConnectionProperties},
+ * {@link org.springframework.ai.autoconfigure.minimax.MiniMaxChatProperties} and
+ * {@link org.springframework.ai.autoconfigure.minimax.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/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/MockWeatherService.java b/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/MockWeatherService.java
new file mode 100644
index 00000000000..bb0ef923c79
--- /dev/null
+++ b/auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/minimax/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.autoconfigure.minimax;
+
+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/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/pom.xml b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/pom.xml
new file mode 100644
index 00000000000..a71f61edfcb
--- /dev/null
+++ b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/pom.xml
@@ -0,0 +1,107 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../../pom.xml
+
+ spring-ai-mistral-ai-spring-boot-autoconfigure
+ jar
+ Spring AI Mistral Auto Configuration
+ Spring AI Mistral Auto Configuration
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-mistral-ai
+ ${project.parent.version}
+ true
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-chat-client-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-chat-model-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-retry-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.ai
+ spring-ai-openai-spring-boot-autoconfigure
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.ai
+ spring-ai-openai
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
diff --git a/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mistralai/MistralAiAutoConfiguration.java b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mistralai/MistralAiAutoConfiguration.java
new file mode 100644
index 00000000000..712756504fe
--- /dev/null
+++ b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mistralai/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.autoconfigure.mistralai;
+
+import io.micrometer.observation.ObservationRegistry;
+
+import org.springframework.ai.autoconfigure.chat.model.ToolCallingAutoConfiguration;
+import org.springframework.ai.autoconfigure.retry.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/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mistralai/MistralAiChatProperties.java b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mistralai/MistralAiChatProperties.java
new file mode 100644
index 00000000000..6047f804e3b
--- /dev/null
+++ b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mistralai/MistralAiChatProperties.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.autoconfigure.mistralai;
+
+import org.springframework.ai.mistralai.MistralAiChatOptions;
+import org.springframework.ai.mistralai.api.MistralAiApi;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
+
+/**
+ * Configuration properties for Mistral AI chat.
+ *
+ * @author Ricken Bazolo
+ * @author Christian Tzolov
+ * @author Thomas Vitale
+ * @author Alexandros Pappas
+ * @since 0.8.1
+ */
+@ConfigurationProperties(MistralAiChatProperties.CONFIG_PREFIX)
+public class MistralAiChatProperties extends MistralAiParentProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.mistralai.chat";
+
+ public static final String DEFAULT_CHAT_MODEL = MistralAiApi.ChatModel.OPEN_MISTRAL_7B.getValue();
+
+ private static final Double DEFAULT_TEMPERATURE = 0.7;
+
+ private static final Double DEFAULT_TOP_P = 1.0;
+
+ private static final Boolean IS_ENABLED = false;
+
+ /**
+ * Enable OpenAI chat model.
+ */
+ private boolean enabled = true;
+
+ @NestedConfigurationProperty
+ private MistralAiChatOptions options = MistralAiChatOptions.builder()
+ .model(DEFAULT_CHAT_MODEL)
+ .temperature(DEFAULT_TEMPERATURE)
+ .safePrompt(!IS_ENABLED)
+ .topP(DEFAULT_TOP_P)
+ .build();
+
+ public MistralAiChatProperties() {
+ super.setBaseUrl(MistralAiCommonProperties.DEFAULT_BASE_URL);
+ }
+
+ public MistralAiChatOptions getOptions() {
+ return this.options;
+ }
+
+ public void setOptions(MistralAiChatOptions options) {
+ this.options = options;
+ }
+
+ public boolean isEnabled() {
+ return this.enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+}
diff --git a/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mistralai/MistralAiCommonProperties.java b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mistralai/MistralAiCommonProperties.java
new file mode 100644
index 00000000000..22d0efada20
--- /dev/null
+++ b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mistralai/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.autoconfigure.mistralai;
+
+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/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mistralai/MistralAiEmbeddingProperties.java b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mistralai/MistralAiEmbeddingProperties.java
new file mode 100644
index 00000000000..380a320ffbb
--- /dev/null
+++ b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mistralai/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.autoconfigure.mistralai;
+
+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/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mistralai/MistralAiParentProperties.java b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mistralai/MistralAiParentProperties.java
new file mode 100644
index 00000000000..c44b10340d2
--- /dev/null
+++ b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mistralai/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.autoconfigure.mistralai;
+
+/**
+ * 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/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json
new file mode 100644
index 00000000000..97efb082ca6
--- /dev/null
+++ b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -0,0 +1,11 @@
+{
+ "groups": [
+ {
+ "name": "spring.ai.mistralai.chat.options.tool-choice",
+ "type": "org.springframework.ai.mistralai.api.MistralAiApi$ChatCompletionRequest$ToolChoice",
+ "sourceType": "org.springframework.ai.mistralai.MistralAiChatOptions"
+ }
+ ],
+ "properties": [],
+ "hints": []
+}
diff --git a/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..f6198ab3697
--- /dev/null
+++ b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,16 @@
+#
+# Copyright 2025-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.
+#
+org.springframework.ai.autoconfigure.mistralai.MistralAiAutoConfiguration
diff --git a/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/mistralai/MistralAiAutoConfigurationIT.java b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/mistralai/MistralAiAutoConfigurationIT.java
new file mode 100644
index 00000000000..99d91c6f3b0
--- /dev/null
+++ b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/mistralai/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.autoconfigure.mistralai;
+
+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/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/mistralai/MistralAiPropertiesTests.java b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/mistralai/MistralAiPropertiesTests.java
new file mode 100644
index 00000000000..46c814bc380
--- /dev/null
+++ b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/mistralai/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.autoconfigure.mistralai;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.autoconfigure.retry.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/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/mistralai/tool/PaymentStatusBeanIT.java b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/mistralai/tool/PaymentStatusBeanIT.java
new file mode 100644
index 00000000000..36f7c8b3880
--- /dev/null
+++ b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/mistralai/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.autoconfigure.mistralai.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.autoconfigure.mistralai.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/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/mistralai/tool/PaymentStatusBeanOpenAiIT.java b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/mistralai/tool/PaymentStatusBeanOpenAiIT.java
new file mode 100644
index 00000000000..321d00f646b
--- /dev/null
+++ b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/mistralai/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.autoconfigure.mistralai.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.autoconfigure.openai.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/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/mistralai/tool/PaymentStatusPromptIT.java b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/mistralai/tool/PaymentStatusPromptIT.java
new file mode 100644
index 00000000000..a0a88984d72
--- /dev/null
+++ b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/mistralai/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.autoconfigure.mistralai.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.autoconfigure.mistralai.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/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/mistralai/tool/WeatherServicePromptIT.java b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/mistralai/tool/WeatherServicePromptIT.java
new file mode 100644
index 00000000000..7c4e56a7d42
--- /dev/null
+++ b/auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/mistralai/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.autoconfigure.mistralai.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.autoconfigure.mistralai.MistralAiAutoConfiguration;
+import org.springframework.ai.autoconfigure.mistralai.tool.WeatherServicePromptIT.MyWeatherService.Request;
+import org.springframework.ai.autoconfigure.mistralai.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/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/pom.xml b/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/pom.xml
new file mode 100644
index 00000000000..fdca98beec6
--- /dev/null
+++ b/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/pom.xml
@@ -0,0 +1,93 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../../pom.xml
+
+ spring-ai-moonshot-spring-boot-autoconfigure
+ jar
+ Spring AI Moonshot AI Auto Configuration
+ Spring AI Moonshot AI Auto Configuration
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-moonshot
+ ${project.parent.version}
+ true
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-chat-client-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-chat-model-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-retry-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
diff --git a/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/moonshot/MoonshotAutoConfiguration.java b/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/moonshot/MoonshotAutoConfiguration.java
new file mode 100644
index 00000000000..12cdc8cf25a
--- /dev/null
+++ b/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/moonshot/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.autoconfigure.moonshot;
+
+import java.util.List;
+
+import io.micrometer.observation.ObservationRegistry;
+
+import org.springframework.ai.autoconfigure.retry.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/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/moonshot/MoonshotChatProperties.java b/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/moonshot/MoonshotChatProperties.java
new file mode 100644
index 00000000000..68d87ea25f9
--- /dev/null
+++ b/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/moonshot/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.autoconfigure.moonshot;
+
+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/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/moonshot/MoonshotCommonProperties.java b/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/moonshot/MoonshotCommonProperties.java
new file mode 100644
index 00000000000..db8989316b8
--- /dev/null
+++ b/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/moonshot/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.autoconfigure.moonshot;
+
+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/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/moonshot/MoonshotParentProperties.java b/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/moonshot/MoonshotParentProperties.java
new file mode 100644
index 00000000000..db0475c296c
--- /dev/null
+++ b/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/moonshot/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.autoconfigure.moonshot;
+
+/**
+ * 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/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..7488990b98e
--- /dev/null
+++ b/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,16 @@
+#
+# Copyright 2025-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.
+#
+org.springframework.ai.autoconfigure.moonshot.MoonshotAutoConfiguration
diff --git a/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/MoonshotAutoConfigurationIT.java b/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/MoonshotAutoConfigurationIT.java
new file mode 100644
index 00000000000..cf8cdd647a3
--- /dev/null
+++ b/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/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.autoconfigure.moonshot;
+
+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.autoconfigure.retry.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/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/MoonshotPropertiesTests.java b/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/MoonshotPropertiesTests.java
new file mode 100644
index 00000000000..79e7bd14265
--- /dev/null
+++ b/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/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.autoconfigure.moonshot;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.autoconfigure.retry.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/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/tool/FunctionCallbackInPromptIT.java b/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/tool/FunctionCallbackInPromptIT.java
new file mode 100644
index 00000000000..bd20fbb9d16
--- /dev/null
+++ b/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/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.autoconfigure.moonshot.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.autoconfigure.moonshot.MoonshotAutoConfiguration;
+import org.springframework.ai.autoconfigure.retry.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/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/tool/FunctionCallbackWithPlainFunctionBeanIT.java b/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/tool/FunctionCallbackWithPlainFunctionBeanIT.java
new file mode 100644
index 00000000000..a9beae2cf90
--- /dev/null
+++ b/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/tool/FunctionCallbackWithPlainFunctionBeanIT.java
@@ -0,0 +1,177 @@
+/*
+ * 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.autoconfigure.moonshot.tool;
+
+import java.util.List;
+import java.util.function.Function;
+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.autoconfigure.moonshot.MoonshotAutoConfiguration;
+import org.springframework.ai.autoconfigure.retry.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.FunctionCallingOptions;
+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 org.springframework.context.annotation.Description;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Geng Rong
+ * @author Alexandros Pappas
+ */
+@EnabledIfEnvironmentVariable(named = "MOONSHOT_API_KEY", matches = ".*")
+class FunctionCallbackWithPlainFunctionBeanIT {
+
+ private final Logger logger = LoggerFactory.getLogger(FunctionCallbackWithPlainFunctionBeanIT.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);
+
+ // Test weatherFunction
+ 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("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),
+ MoonshotChatOptions.builder().function("weatherFunctionTwo").build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+
+ });
+ }
+
+ @Test
+ void functionCallWithPortableFunctionCallingOptions() {
+ this.contextRunner.run(context -> {
+
+ MoonshotChatModel chatModel = context.getBean(MoonshotChatModel.class);
+
+ // Test weatherFunction
+ UserMessage userMessage = new UserMessage(
+ "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius");
+
+ FunctionCallingOptions functionOptions = FunctionCallingOptions.builder()
+ .function("weatherFunction")
+ .build();
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), functionOptions));
+
+ logger.info("Response: {}", response);
+ });
+ }
+
+ @Test
+ void streamFunctionCallTest() {
+ this.contextRunner.run(context -> {
+
+ MoonshotChatModel chatModel = context.getBean(MoonshotChatModel.class);
+
+ // Test weatherFunction
+ 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("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).containsAnyOf("30.0", "30");
+ assertThat(content).containsAnyOf("10.0", "10");
+ assertThat(content).containsAnyOf("15.0", "15");
+
+ // Test weatherFunctionTwo
+ response = chatModel.stream(new Prompt(List.of(userMessage),
+ MoonshotChatOptions.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).containsAnyOf("30.0", "30");
+ assertThat(content).containsAnyOf("10.0", "10");
+ assertThat(content).containsAnyOf("15.0", "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 weatherFunctionTwo() {
+ MockWeatherService weatherService = new MockWeatherService();
+ return (weatherService::apply);
+ }
+
+ }
+
+}
diff --git a/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/tool/MockWeatherService.java b/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/tool/MockWeatherService.java
new file mode 100644
index 00000000000..935464dca4e
--- /dev/null
+++ b/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/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.autoconfigure.moonshot.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/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/tool/MoonshotFunctionCallbackIT.java b/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/tool/MoonshotFunctionCallbackIT.java
new file mode 100644
index 00000000000..fed437ef665
--- /dev/null
+++ b/auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/moonshot/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.autoconfigure.moonshot.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.autoconfigure.moonshot.MoonshotAutoConfiguration;
+import org.springframework.ai.autoconfigure.retry.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/auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure/pom.xml b/auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure/pom.xml
new file mode 100644
index 00000000000..faae9d53d7b
--- /dev/null
+++ b/auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure/pom.xml
@@ -0,0 +1,93 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../../pom.xml
+
+ spring-ai-oci-genai-spring-boot-autoconfigure
+ jar
+ Spring AI OCI GenAI Auto Configuration
+ Spring AI OCI GenAI Auto Configuration
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-oci-genai
+ ${project.parent.version}
+ true
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-chat-client-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-chat-model-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-retry-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
diff --git a/auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/OCICohereChatModelProperties.java b/auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/OCICohereChatModelProperties.java
new file mode 100644
index 00000000000..9f97e5978ae
--- /dev/null
+++ b/auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/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.autoconfigure.oci.genai;
+
+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/auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/OCIConnectionProperties.java b/auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/OCIConnectionProperties.java
new file mode 100644
index 00000000000..e22f573d099
--- /dev/null
+++ b/auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/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.autoconfigure.oci.genai;
+
+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/auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/OCIEmbeddingModelProperties.java b/auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/OCIEmbeddingModelProperties.java
new file mode 100644
index 00000000000..bbfeaf0a25a
--- /dev/null
+++ b/auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/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.autoconfigure.oci.genai;
+
+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/auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/OCIGenAiAutoConfiguration.java b/auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/OCIGenAiAutoConfiguration.java
new file mode 100644
index 00000000000..fbf651edbef
--- /dev/null
+++ b/auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/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.autoconfigure.oci.genai;
+
+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/auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/ServingMode.java b/auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/ServingMode.java
new file mode 100644
index 00000000000..d777b8d958e
--- /dev/null
+++ b/auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/oci/genai/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.autoconfigure.oci.genai;
+
+/**
+ * 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/auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..d87ac5f0c09
--- /dev/null
+++ b/auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,16 @@
+#
+# Copyright 2025-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.
+#
+org.springframework.ai.autoconfigure.oci.genai.OCIGenAiAutoConfiguration
diff --git a/auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/oci/genai/OCIGenAIAutoConfigurationTest.java b/auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/oci/genai/OCIGenAIAutoConfigurationTest.java
new file mode 100644
index 00000000000..d7b9573ff78
--- /dev/null
+++ b/auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/oci/genai/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.autoconfigure.oci.genai;
+
+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/auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/oci/genai/OCIGenAiAutoConfigurationIT.java b/auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/oci/genai/OCIGenAiAutoConfigurationIT.java
new file mode 100644
index 00000000000..24b7e5262bb
--- /dev/null
+++ b/auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/oci/genai/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.autoconfigure.oci.genai;
+
+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/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/pom.xml b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/pom.xml
new file mode 100644
index 00000000000..1b0373531b2
--- /dev/null
+++ b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/pom.xml
@@ -0,0 +1,98 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../../pom.xml
+
+ spring-ai-ollama-spring-boot-autoconfigure
+ jar
+ Spring AI Ollama Auto Configuration
+ Spring AI Ollama Auto Configuration
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-ollama
+ ${project.parent.version}
+ true
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-chat-client-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-chat-model-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+ org.testcontainers
+ junit-jupiter
+ test
+
+
+
+ org.testcontainers
+ ollama
+ test
+
+
+
+
diff --git a/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaAutoConfiguration.java b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaAutoConfiguration.java
new file mode 100644
index 00000000000..ecde0c935a6
--- /dev/null
+++ b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/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.autoconfigure.ollama;
+
+import io.micrometer.observation.ObservationRegistry;
+
+import org.springframework.ai.autoconfigure.chat.model.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/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaChatProperties.java b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaChatProperties.java
new file mode 100644
index 00000000000..a835495b5c3
--- /dev/null
+++ b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/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.autoconfigure.ollama;
+
+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/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaConnectionDetails.java b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaConnectionDetails.java
new file mode 100644
index 00000000000..8673c3e361e
--- /dev/null
+++ b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/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.autoconfigure.ollama;
+
+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/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaConnectionProperties.java b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaConnectionProperties.java
new file mode 100644
index 00000000000..46f127e1310
--- /dev/null
+++ b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/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.autoconfigure.ollama;
+
+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/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaEmbeddingProperties.java b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaEmbeddingProperties.java
new file mode 100644
index 00000000000..3a12ebc2d44
--- /dev/null
+++ b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/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.autoconfigure.ollama;
+
+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/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaInitializationProperties.java b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/OllamaInitializationProperties.java
new file mode 100644
index 00000000000..54c764f274c
--- /dev/null
+++ b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/ollama/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.autoconfigure.ollama;
+
+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/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..82bc5c614b6
--- /dev/null
+++ b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,16 @@
+#
+# Copyright 2025-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.
+#
+org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration
diff --git a/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/BaseOllamaIT.java b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/BaseOllamaIT.java
new file mode 100644
index 00000000000..138ec88f13a
--- /dev/null
+++ b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/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.autoconfigure.ollama;
+
+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/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/OllamaChatAutoConfigurationIT.java b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/OllamaChatAutoConfigurationIT.java
new file mode 100644
index 00000000000..40d936968c4
--- /dev/null
+++ b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/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.autoconfigure.ollama;
+
+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/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/OllamaChatAutoConfigurationTests.java b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/OllamaChatAutoConfigurationTests.java
new file mode 100644
index 00000000000..14493bcb20c
--- /dev/null
+++ b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/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.autoconfigure.ollama;
+
+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/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/OllamaEmbeddingAutoConfigurationIT.java b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/OllamaEmbeddingAutoConfigurationIT.java
new file mode 100644
index 00000000000..fc6fd6770de
--- /dev/null
+++ b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/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.autoconfigure.ollama;
+
+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/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/OllamaEmbeddingAutoConfigurationTests.java b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/OllamaEmbeddingAutoConfigurationTests.java
new file mode 100644
index 00000000000..bd2a8bfd2df
--- /dev/null
+++ b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/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.autoconfigure.ollama;
+
+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/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/OllamaImage.java b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/OllamaImage.java
new file mode 100644
index 00000000000..807975a49f7
--- /dev/null
+++ b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/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.autoconfigure.ollama;
+
+public final class OllamaImage {
+
+ public static final String DEFAULT_IMAGE = "ollama/ollama:0.5.7";
+
+ private OllamaImage() {
+
+ }
+
+}
diff --git a/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/tool/FunctionCallbackInPromptIT.java b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/tool/FunctionCallbackInPromptIT.java
new file mode 100644
index 00000000000..a703033a128
--- /dev/null
+++ b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/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.autoconfigure.ollama.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.autoconfigure.ollama.BaseOllamaIT;
+import org.springframework.ai.autoconfigure.ollama.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/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/tool/MockWeatherService.java b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/tool/MockWeatherService.java
new file mode 100644
index 00000000000..88154dd3e73
--- /dev/null
+++ b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/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.autoconfigure.ollama.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/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/tool/OllamaFunctionCallbackIT.java b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/tool/OllamaFunctionCallbackIT.java
new file mode 100644
index 00000000000..3e739c8b60f
--- /dev/null
+++ b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/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.autoconfigure.ollama.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.autoconfigure.ollama.BaseOllamaIT;
+import org.springframework.ai.autoconfigure.ollama.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/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/tool/OllamaFunctionToolBeanIT.java b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/tool/OllamaFunctionToolBeanIT.java
new file mode 100644
index 00000000000..6b6380f74a7
--- /dev/null
+++ b/auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/ollama/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.autoconfigure.ollama.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.autoconfigure.ollama.BaseOllamaIT;
+import org.springframework.ai.autoconfigure.ollama.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/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/pom.xml b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/pom.xml
new file mode 100644
index 00000000000..84a91e65b5e
--- /dev/null
+++ b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/pom.xml
@@ -0,0 +1,101 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../../pom.xml
+
+ spring-ai-openai-spring-boot-autoconfigure
+ jar
+ Spring AI OpenAI Auto Configuration
+ Spring AI OpenAI Auto Configuration
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-openai
+ ${project.parent.version}
+ true
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-chat-client-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-chat-model-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-retry-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
diff --git a/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiAudioSpeechProperties.java b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiAudioSpeechProperties.java
new file mode 100644
index 00000000000..8428607ea81
--- /dev/null
+++ b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/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.autoconfigure.openai;
+
+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/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiAudioTranscriptionProperties.java b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiAudioTranscriptionProperties.java
new file mode 100644
index 00000000000..63a1c9870e5
--- /dev/null
+++ b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/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.autoconfigure.openai;
+
+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/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiAutoConfiguration.java b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiAutoConfiguration.java
new file mode 100644
index 00000000000..af934cd70cb
--- /dev/null
+++ b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/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.autoconfigure.openai;
+
+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.autoconfigure.chat.model.ToolCallingAutoConfiguration;
+import org.springframework.ai.autoconfigure.retry.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/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiChatProperties.java b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiChatProperties.java
new file mode 100644
index 00000000000..cf0ee1971a0
--- /dev/null
+++ b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/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.autoconfigure.openai;
+
+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/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiConnectionProperties.java b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiConnectionProperties.java
new file mode 100644
index 00000000000..e6c6f582d1b
--- /dev/null
+++ b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/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.autoconfigure.openai;
+
+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/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiEmbeddingProperties.java b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiEmbeddingProperties.java
new file mode 100644
index 00000000000..ea17041b5b3
--- /dev/null
+++ b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/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.autoconfigure.openai;
+
+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/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiImageProperties.java b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiImageProperties.java
new file mode 100644
index 00000000000..b521e34ef17
--- /dev/null
+++ b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/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.autoconfigure.openai;
+
+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/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiModerationProperties.java b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiModerationProperties.java
new file mode 100644
index 00000000000..d9e709862ef
--- /dev/null
+++ b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/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.autoconfigure.openai;
+
+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/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiParentProperties.java b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/OpenAiParentProperties.java
new file mode 100644
index 00000000000..7516ba84460
--- /dev/null
+++ b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/openai/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.autoconfigure.openai;
+
+/**
+ * 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/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json
new file mode 100644
index 00000000000..17529588fd3
--- /dev/null
+++ b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -0,0 +1,22 @@
+{
+ "groups": [
+ {
+ "name": "spring.ai.openai.chat.output-audio",
+ "type": "org.springframework.ai.openai.api.OpenAiApi$ChatCompletionRequest$AudioParameters",
+ "sourceType": "org.springframework.ai.openai.OpenAiChatOptions"
+ }
+ ],
+ "properties": [
+ {
+ "name": "spring.ai.openai.chat.output-audio.voice",
+ "type": "org.springframework.ai.openai.api.OpenAiApi$ChatCompletionRequest$AudioParameters$Voice",
+ "sourceType": "org.springframework.ai.openai.api.OpenAiApi$ChatCompletionRequest$AudioParameters"
+ },
+ {
+ "name": "spring.ai.openai.chat.output-audio.format",
+ "type": "org.springframework.ai.openai.api.OpenAiApi$ChatCompletionRequest$AudioParameters$AudioResponseFormat",
+ "sourceType": "org.springframework.ai.openai.api.OpenAiApi$ChatCompletionRequest$AudioParameters"
+ }
+ ],
+ "hints": []
+}
diff --git a/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..60b58e80808
--- /dev/null
+++ b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,16 @@
+#
+# Copyright 2025-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.
+#
+org.springframework.ai.autoconfigure.anthropic.AnthropicAutoConfiguration
diff --git a/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/ChatClientAutoConfigurationIT.java b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/ChatClientAutoConfigurationIT.java
new file mode 100644
index 00000000000..55d2567ad17
--- /dev/null
+++ b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/ChatClientAutoConfigurationIT.java
@@ -0,0 +1,121 @@
+/*
+ * 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.autoconfigure.openai;
+
+import java.util.List;
+
+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.springframework.ai.autoconfigure.chat.client.ChatClientAutoConfiguration;
+import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.chat.client.ChatClientCustomizer;
+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 Christian Tzolov
+ */
+@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*")
+public class ChatClientAutoConfigurationIT {
+
+ private static final Log logger = LogFactory.getLog(ChatClientAutoConfigurationIT.class);
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY"),
+ "spring.ai.openai.chat.options.model=gpt-4o")
+ .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class,
+ RestClientAutoConfiguration.class, OpenAiAutoConfiguration.class, ChatClientAutoConfiguration.class));
+
+ @Test
+ void implicitlyEnabled() {
+ this.contextRunner.run(context -> assertThat(context.getBeansOfType(ChatClient.Builder.class)).isNotEmpty());
+ }
+
+ @Test
+ void explicitlyEnabled() {
+ this.contextRunner.withPropertyValues("spring.ai.chat.client.enabled=true")
+ .run(context -> assertThat(context.getBeansOfType(ChatClient.Builder.class)).isNotEmpty());
+ }
+
+ @Test
+ void explicitlyDisabled() {
+ this.contextRunner.withPropertyValues("spring.ai.chat.client.enabled=false")
+ .run(context -> assertThat(context.getBeansOfType(ChatClient.Builder.class)).isEmpty());
+ }
+
+ @Test
+ void generate() {
+ this.contextRunner.run(context -> {
+ ChatClient.Builder builder = context.getBean(ChatClient.Builder.class);
+
+ assertThat(builder).isNotNull();
+
+ ChatClient chatClient = builder.build();
+
+ String response = chatClient.prompt().user("Hello").call().content();
+
+ assertThat(response).isNotEmpty();
+ logger.info("Response: " + response);
+ });
+ }
+
+ @Test
+ void testChatClientCustomizers() {
+ this.contextRunner.withUserConfiguration(Config.class).run(context -> {
+
+ ChatClient.Builder builder = context.getBean(ChatClient.Builder.class);
+
+ ChatClient chatClient = builder.build();
+
+ assertThat(chatClient).isNotNull();
+
+ ActorsFilms actorsFilms = chatClient.prompt()
+ .user(u -> u.param("actor", "Tom Hanks"))
+ .call()
+ .entity(ActorsFilms.class);
+
+ logger.info("" + actorsFilms);
+ assertThat(actorsFilms.actor()).isEqualTo("Tom Hanks");
+ assertThat(actorsFilms.movies()).hasSize(5);
+ });
+ }
+
+ record ActorsFilms(String actor, List movies) {
+
+ }
+
+ @Configuration
+ static class Config {
+
+ @Bean
+ public ChatClientCustomizer chatClientCustomizer() {
+ return b -> b.defaultSystem("You are a movie expert.")
+ .defaultUser("Generate the filmography of 5 movies for {actor}.");
+ }
+
+ }
+
+}
diff --git a/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/OpenAiAutoConfigurationIT.java b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/OpenAiAutoConfigurationIT.java
new file mode 100644
index 00000000000..0ebe376bcaf
--- /dev/null
+++ b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/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.autoconfigure.openai;
+
+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/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/OpenAiPropertiesTests.java b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/OpenAiPropertiesTests.java
new file mode 100644
index 00000000000..fdbc0252444
--- /dev/null
+++ b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/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.autoconfigure.openai;
+
+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/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/OpenAiResponseFormatPropertiesTests.java b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/OpenAiResponseFormatPropertiesTests.java
new file mode 100644
index 00000000000..c7f5baaec12
--- /dev/null
+++ b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/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.autoconfigure.openai;
+
+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/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPrompt2IT.java b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPrompt2IT.java
new file mode 100644
index 00000000000..21c11f7af7b
--- /dev/null
+++ b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/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.autoconfigure.openai.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.autoconfigure.openai.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/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPromptIT.java b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackInPromptIT.java
new file mode 100644
index 00000000000..14e60ab6801
--- /dev/null
+++ b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/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.autoconfigure.openai.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.autoconfigure.openai.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/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWithPlainFunctionBeanIT.java b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/FunctionCallbackWithPlainFunctionBeanIT.java
new file mode 100644
index 00000000000..bed893c1089
--- /dev/null
+++ b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/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.autoconfigure.openai.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.autoconfigure.openai.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/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/MockWeatherService.java b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/MockWeatherService.java
new file mode 100644
index 00000000000..489942fcc20
--- /dev/null
+++ b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/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.autoconfigure.openai.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/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/OpenAiFunctionCallback2IT.java b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/OpenAiFunctionCallback2IT.java
new file mode 100644
index 00000000000..aab1aabd8c3
--- /dev/null
+++ b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/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.autoconfigure.openai.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.autoconfigure.openai.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/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/OpenAiFunctionCallbackIT.java b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/tool/OpenAiFunctionCallbackIT.java
new file mode 100644
index 00000000000..d4914146cec
--- /dev/null
+++ b/auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/openai/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.autoconfigure.openai.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.autoconfigure.openai.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/auto-configurations/models/spring-ai-postgresml-spring-boot-autoconfigure/pom.xml b/auto-configurations/models/spring-ai-postgresml-spring-boot-autoconfigure/pom.xml
new file mode 100644
index 00000000000..b04b72f5d70
--- /dev/null
+++ b/auto-configurations/models/spring-ai-postgresml-spring-boot-autoconfigure/pom.xml
@@ -0,0 +1,111 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../../pom.xml
+
+ spring-ai-postgresml-spring-boot-autoconfigure
+ jar
+ Spring AI PostgresML Auto Configuration
+ Spring AI PostgresML Auto Configuration
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-postgresml
+ ${project.parent.version}
+ true
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-chat-client-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-chat-model-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-retry-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-testcontainers
+ test
+
+
+
+ org.testcontainers
+ junit-jupiter
+ test
+
+
+
+ org.testcontainers
+ postgresql
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
diff --git a/auto-configurations/models/spring-ai-postgresml-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/postgresml/PostgresMlAutoConfiguration.java b/auto-configurations/models/spring-ai-postgresml-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/postgresml/PostgresMlAutoConfiguration.java
new file mode 100644
index 00000000000..e9bbac87e6e
--- /dev/null
+++ b/auto-configurations/models/spring-ai-postgresml-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/postgresml/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.autoconfigure.postgresml;
+
+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/auto-configurations/models/spring-ai-postgresml-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/postgresml/PostgresMlEmbeddingProperties.java b/auto-configurations/models/spring-ai-postgresml-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/postgresml/PostgresMlEmbeddingProperties.java
new file mode 100644
index 00000000000..91c53ae1e5a
--- /dev/null
+++ b/auto-configurations/models/spring-ai-postgresml-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/postgresml/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.autoconfigure.postgresml;
+
+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/auto-configurations/models/spring-ai-postgresml-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-postgresml-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..1664474937a
--- /dev/null
+++ b/auto-configurations/models/spring-ai-postgresml-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,16 @@
+#
+# Copyright 2025-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.
+#
+org.springframework.ai.autoconfigure.postgresml.PostgresMlAutoConfiguration
diff --git a/auto-configurations/models/spring-ai-postgresml-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/postgresml/PostgresMlAutoConfigurationIT.java b/auto-configurations/models/spring-ai-postgresml-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/postgresml/PostgresMlAutoConfigurationIT.java
new file mode 100644
index 00000000000..b41b2fe5e67
--- /dev/null
+++ b/auto-configurations/models/spring-ai-postgresml-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/postgresml/PostgresMlAutoConfigurationIT.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.autoconfigure.postgresml;
+
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+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.LogMessageWaitStrategy;
+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/auto-configurations/models/spring-ai-postgresml-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/postgresml/PostgresMlEmbeddingPropertiesTests.java b/auto-configurations/models/spring-ai-postgresml-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/postgresml/PostgresMlEmbeddingPropertiesTests.java
new file mode 100644
index 00000000000..be6a5358595
--- /dev/null
+++ b/auto-configurations/models/spring-ai-postgresml-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/postgresml/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.autoconfigure.postgresml;
+
+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/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/pom.xml b/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/pom.xml
new file mode 100644
index 00000000000..372c46f6105
--- /dev/null
+++ b/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/pom.xml
@@ -0,0 +1,86 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../../pom.xml
+
+ spring-ai-qianfan-spring-boot-autoconfigure
+ jar
+ Spring AI QianFan AI Auto Configuration
+ Spring AI QianFan AI Auto Configuration
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-qianfan
+ ${project.parent.version}
+ true
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-chat-client-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-retry-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
diff --git a/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/qianfan/QianFanAutoConfiguration.java b/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/qianfan/QianFanAutoConfiguration.java
new file mode 100644
index 00000000000..7af2dc19913
--- /dev/null
+++ b/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/qianfan/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.autoconfigure.qianfan;
+
+import io.micrometer.observation.ObservationRegistry;
+
+import org.springframework.ai.autoconfigure.retry.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/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/qianfan/QianFanChatProperties.java b/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/qianfan/QianFanChatProperties.java
new file mode 100644
index 00000000000..2d2487e7be7
--- /dev/null
+++ b/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/qianfan/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.autoconfigure.qianfan;
+
+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/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/qianfan/QianFanConnectionProperties.java b/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/qianfan/QianFanConnectionProperties.java
new file mode 100644
index 00000000000..90cb8c7a22f
--- /dev/null
+++ b/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/qianfan/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.autoconfigure.qianfan;
+
+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/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/qianfan/QianFanEmbeddingProperties.java b/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/qianfan/QianFanEmbeddingProperties.java
new file mode 100644
index 00000000000..b1514424e2c
--- /dev/null
+++ b/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/qianfan/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.autoconfigure.qianfan;
+
+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/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/qianfan/QianFanImageProperties.java b/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/qianfan/QianFanImageProperties.java
new file mode 100644
index 00000000000..50d00f04c1b
--- /dev/null
+++ b/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/qianfan/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.autoconfigure.qianfan;
+
+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/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/qianfan/QianFanParentProperties.java b/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/qianfan/QianFanParentProperties.java
new file mode 100644
index 00000000000..543bec0c6fe
--- /dev/null
+++ b/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/qianfan/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.autoconfigure.qianfan;
+
+/**
+ * @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/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..444ccbed095
--- /dev/null
+++ b/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,16 @@
+#
+# Copyright 2025-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.
+#
+org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration
diff --git a/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/qianfan/QianFanAutoConfigurationIT.java b/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/qianfan/QianFanAutoConfigurationIT.java
new file mode 100644
index 00000000000..78842991257
--- /dev/null
+++ b/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/qianfan/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.autoconfigure.qianfan;
+
+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.autoconfigure.retry.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/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/qianfan/QianFanPropertiesTests.java b/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/qianfan/QianFanPropertiesTests.java
new file mode 100644
index 00000000000..1a0ee813f35
--- /dev/null
+++ b/auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/qianfan/QianFanPropertiesTests.java
@@ -0,0 +1,440 @@
+/*
+ * 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.autoconfigure.qianfan;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.autoconfigure.retry.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 org.springframework.ai.autoconfigure.qianfan.QianFanConnectionProperties},
+ * {@link org.springframework.ai.autoconfigure.qianfan.QianFanChatProperties} and
+ * {@link org.springframework.ai.autoconfigure.qianfan.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/auto-configurations/models/spring-ai-stability-ai-spring-boot-autoconfigure/pom.xml b/auto-configurations/models/spring-ai-stability-ai-spring-boot-autoconfigure/pom.xml
new file mode 100644
index 00000000000..321b0be1296
--- /dev/null
+++ b/auto-configurations/models/spring-ai-stability-ai-spring-boot-autoconfigure/pom.xml
@@ -0,0 +1,86 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../../pom.xml
+
+ spring-ai-stability-ai-spring-boot-autoconfigure
+ jar
+ Spring AI Stability AI Auto Configuration
+ Spring AI Stability AI Auto Configuration
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-stability-ai
+ ${project.parent.version}
+ true
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-chat-client-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-retry-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
diff --git a/auto-configurations/models/spring-ai-stability-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/stabilityai/StabilityAiConnectionProperties.java b/auto-configurations/models/spring-ai-stability-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/stabilityai/StabilityAiConnectionProperties.java
new file mode 100644
index 00000000000..e39d36e7f2d
--- /dev/null
+++ b/auto-configurations/models/spring-ai-stability-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/stabilityai/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.autoconfigure.stabilityai;
+
+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/auto-configurations/models/spring-ai-stability-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/stabilityai/StabilityAiImageAutoConfiguration.java b/auto-configurations/models/spring-ai-stability-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/stabilityai/StabilityAiImageAutoConfiguration.java
new file mode 100644
index 00000000000..266507b86fd
--- /dev/null
+++ b/auto-configurations/models/spring-ai-stability-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/stabilityai/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.autoconfigure.stabilityai;
+
+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/auto-configurations/models/spring-ai-stability-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/stabilityai/StabilityAiImageProperties.java b/auto-configurations/models/spring-ai-stability-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/stabilityai/StabilityAiImageProperties.java
new file mode 100644
index 00000000000..2c7ad567270
--- /dev/null
+++ b/auto-configurations/models/spring-ai-stability-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/stabilityai/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.autoconfigure.stabilityai;
+
+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/auto-configurations/models/spring-ai-stability-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/stabilityai/StabilityAiParentProperties.java b/auto-configurations/models/spring-ai-stability-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/stabilityai/StabilityAiParentProperties.java
new file mode 100644
index 00000000000..b62d9e5e312
--- /dev/null
+++ b/auto-configurations/models/spring-ai-stability-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/stabilityai/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.autoconfigure.stabilityai;
+
+/**
+ * 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/auto-configurations/models/spring-ai-stability-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-stability-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..dd1b2b257c0
--- /dev/null
+++ b/auto-configurations/models/spring-ai-stability-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,16 @@
+#
+# Copyright 2025-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.
+#
+org.springframework.ai.autoconfigure.stabilityai.StabilityAiImageAutoConfiguration
diff --git a/auto-configurations/models/spring-ai-stability-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/stabilityai/StabilityAiAutoConfigurationIT.java b/auto-configurations/models/spring-ai-stability-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/stabilityai/StabilityAiAutoConfigurationIT.java
new file mode 100644
index 00000000000..ca7c38069e4
--- /dev/null
+++ b/auto-configurations/models/spring-ai-stability-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/stabilityai/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.autoconfigure.stabilityai;
+
+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/auto-configurations/models/spring-ai-stability-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/stabilityai/StabilityAiImagePropertiesTests.java b/auto-configurations/models/spring-ai-stability-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/stabilityai/StabilityAiImagePropertiesTests.java
new file mode 100644
index 00000000000..606dbace005
--- /dev/null
+++ b/auto-configurations/models/spring-ai-stability-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/stabilityai/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.autoconfigure.stabilityai;
+
+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/auto-configurations/models/spring-ai-transformers-spring-boot-autoconfigure/pom.xml b/auto-configurations/models/spring-ai-transformers-spring-boot-autoconfigure/pom.xml
new file mode 100644
index 00000000000..55003e63bf3
--- /dev/null
+++ b/auto-configurations/models/spring-ai-transformers-spring-boot-autoconfigure/pom.xml
@@ -0,0 +1,86 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../../pom.xml
+
+ spring-ai-transformers-spring-boot-autoconfigure
+ jar
+ Spring AI ONNX Transformers Auto Configuration
+ Spring AI ONNX Transformers Auto Configuration
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-transformers
+ ${project.parent.version}
+ true
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-chat-client-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-retry-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
diff --git a/auto-configurations/models/spring-ai-transformers-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/transformers/TransformersEmbeddingModelAutoConfiguration.java b/auto-configurations/models/spring-ai-transformers-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/transformers/TransformersEmbeddingModelAutoConfiguration.java
new file mode 100644
index 00000000000..30652cf3bff
--- /dev/null
+++ b/auto-configurations/models/spring-ai-transformers-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/transformers/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.autoconfigure.transformers;
+
+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/auto-configurations/models/spring-ai-transformers-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/transformers/TransformersEmbeddingModelProperties.java b/auto-configurations/models/spring-ai-transformers-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/transformers/TransformersEmbeddingModelProperties.java
new file mode 100644
index 00000000000..6f090aaa28e
--- /dev/null
+++ b/auto-configurations/models/spring-ai-transformers-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/transformers/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.autoconfigure.transformers;
+
+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(org.springframework.ai.autoconfigure.transformers.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/auto-configurations/models/spring-ai-transformers-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-transformers-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..d64f9554e4a
--- /dev/null
+++ b/auto-configurations/models/spring-ai-transformers-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,16 @@
+#
+# Copyright 2025-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.
+#
+org.springframework.ai.autoconfigure.transformers.TransformersEmbeddingModelAutoConfiguration
diff --git a/auto-configurations/models/spring-ai-transformers-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/transformers/TransformersEmbeddingModelAutoConfigurationIT.java b/auto-configurations/models/spring-ai-transformers-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/transformers/TransformersEmbeddingModelAutoConfigurationIT.java
new file mode 100644
index 00000000000..9b3a597bed9
--- /dev/null
+++ b/auto-configurations/models/spring-ai-transformers-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/transformers/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.autoconfigure.transformers;
+
+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/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/pom.xml b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/pom.xml
new file mode 100644
index 00000000000..0f0549f27e6
--- /dev/null
+++ b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/pom.xml
@@ -0,0 +1,114 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../../pom.xml
+
+ spring-ai-vertex-ai-spring-boot-autoconfigure
+ jar
+ Spring AI Vertex AI Auto Configuration
+ Spring AI Vertex AI Auto Configuration
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-vertex-ai-embedding
+ ${project.parent.version}
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-vertex-ai-gemini
+ ${project.parent.version}
+ true
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-chat-client-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-chat-model-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-retry-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+ org.testcontainers
+ junit-jupiter
+ test
+
+
+
+ org.testcontainers
+ ollama
+ test
+
+
+
+
diff --git a/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/embedding/VertexAiEmbeddingAutoConfiguration.java b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/embedding/VertexAiEmbeddingAutoConfiguration.java
new file mode 100644
index 00000000000..c6f32025e9d
--- /dev/null
+++ b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/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.autoconfigure.vertexai.embedding;
+
+import java.io.IOException;
+
+import com.google.cloud.vertexai.VertexAI;
+import io.micrometer.observation.ObservationRegistry;
+
+import org.springframework.ai.autoconfigure.retry.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
+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/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/embedding/VertexAiEmbeddingConnectionProperties.java b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/embedding/VertexAiEmbeddingConnectionProperties.java
new file mode 100644
index 00000000000..a86462d396d
--- /dev/null
+++ b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/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.autoconfigure.vertexai.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/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/embedding/VertexAiMultimodalEmbeddingProperties.java b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/embedding/VertexAiMultimodalEmbeddingProperties.java
new file mode 100644
index 00000000000..912084bd5b1
--- /dev/null
+++ b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/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.autoconfigure.vertexai.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/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/embedding/VertexAiTextEmbeddingProperties.java b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/embedding/VertexAiTextEmbeddingProperties.java
new file mode 100644
index 00000000000..7addfab95cb
--- /dev/null
+++ b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/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.autoconfigure.vertexai.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/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/gemini/VertexAiGeminiAutoConfiguration.java b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/gemini/VertexAiGeminiAutoConfiguration.java
new file mode 100644
index 00000000000..69b4321d8d9
--- /dev/null
+++ b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/gemini/VertexAiGeminiAutoConfiguration.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.autoconfigure.vertexai.gemini;
+
+import java.io.IOException;
+import java.util.List;
+
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.cloud.vertexai.VertexAI;
+import io.micrometer.observation.ObservationRegistry;
+
+import org.springframework.ai.autoconfigure.chat.model.ToolCallingAutoConfiguration;
+import org.springframework.ai.autoconfigure.retry.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.FunctionCallback.SchemaType;
+import org.springframework.ai.model.function.FunctionCallbackResolver;
+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/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/gemini/VertexAiGeminiChatProperties.java b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/gemini/VertexAiGeminiChatProperties.java
new file mode 100644
index 00000000000..9ce224a7a8c
--- /dev/null
+++ b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/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.autoconfigure.vertexai.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/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/gemini/VertexAiGeminiConnectionProperties.java b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/gemini/VertexAiGeminiConnectionProperties.java
new file mode 100644
index 00000000000..e47d41863f1
--- /dev/null
+++ b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/vertexai/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.autoconfigure.vertexai.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/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..c5f9bc953cf
--- /dev/null
+++ b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,17 @@
+#
+# Copyright 2025-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.
+#
+org.springframework.ai.autoconfigure.vertexai.gemini.VertexAiGeminiAutoConfiguration
+org.springframework.ai.autoconfigure.vertexai.embedding.VertexAiEmbeddingAutoConfiguration
diff --git a/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/embedding/VertexAiTextEmbeddingModelAutoConfigurationIT.java b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/embedding/VertexAiTextEmbeddingModelAutoConfigurationIT.java
new file mode 100644
index 00000000000..9268b92f6df
--- /dev/null
+++ b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/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.autoconfigure.vertexai.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/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/gemini/VertexAiGeminiAutoConfigurationIT.java b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/gemini/VertexAiGeminiAutoConfigurationIT.java
new file mode 100644
index 00000000000..ca2cdeb3a02
--- /dev/null
+++ b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/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.autoconfigure.vertexai.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/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/gemini/tool/FunctionCallWithFunctionBeanIT.java b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/gemini/tool/FunctionCallWithFunctionBeanIT.java
new file mode 100644
index 00000000000..eb31e3b8840
--- /dev/null
+++ b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/gemini/tool/FunctionCallWithFunctionBeanIT.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.autoconfigure.vertexai.gemini.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.autoconfigure.vertexai.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.model.function.FunctionCallingOptions;
+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 org.springframework.context.annotation.Description;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_PROJECT_ID", matches = ".*")
+@EnabledIfEnvironmentVariable(named = "VERTEX_AI_GEMINI_LOCATION", matches = ".*")
+class FunctionCallWithFunctionBeanIT {
+
+ private final Logger logger = LoggerFactory.getLogger(FunctionCallWithFunctionBeanIT.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_PRO_1_5_PRO.getValue())
+ + 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.
+ Perform multiple funciton execution if necessary.
+ """);
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
+ VertexAiGeminiChatOptions.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),
+ VertexAiGeminiChatOptions.builder().function("weatherFunction3").build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+
+ response = chatModel
+ .call(new Prompt(List.of(userMessage), VertexAiGeminiChatOptions.builder().build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).doesNotContain("30", "10", "15");
+
+ });
+ }
+
+ @Test
+ void functionCallWithPortableFunctionCallingOptions() {
+
+ this.contextRunner.withPropertyValues("spring.ai.vertex.ai.gemini.chat.options.model="
+ // + VertexAiGeminiChatModel.ChatModel.GEMINI_PRO_1_5_PRO.getValue())
+ + 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.
+ Perform multiple funciton execution if necessary.
+ """);
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage),
+ FunctionCallingOptions.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),
+ VertexAiGeminiChatOptions.builder().function("weatherFunction3").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/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/gemini/tool/FunctionCallWithFunctionWrapperIT.java b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/gemini/tool/FunctionCallWithFunctionWrapperIT.java
new file mode 100644
index 00000000000..bfcdb7c6433
--- /dev/null
+++ b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/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.autoconfigure.vertexai.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.autoconfigure.vertexai.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/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/gemini/tool/FunctionCallWithPromptFunctionIT.java b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/gemini/tool/FunctionCallWithPromptFunctionIT.java
new file mode 100644
index 00000000000..2227f80c623
--- /dev/null
+++ b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/gemini/tool/FunctionCallWithPromptFunctionIT.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.autoconfigure.vertexai.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.autoconfigure.vertexai.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.model.function.FunctionCallback;
+import org.springframework.ai.model.function.FunctionCallback.SchemaType;
+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/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/gemini/tool/MockWeatherService.java b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/gemini/tool/MockWeatherService.java
new file mode 100644
index 00000000000..4518a5194f5
--- /dev/null
+++ b/auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/vertexai/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.autoconfigure.vertexai.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/auto-configurations/models/spring-ai-watsonx-ai-spring-boot-autoconfigure/pom.xml b/auto-configurations/models/spring-ai-watsonx-ai-spring-boot-autoconfigure/pom.xml
new file mode 100644
index 00000000000..43b7548f7c5
--- /dev/null
+++ b/auto-configurations/models/spring-ai-watsonx-ai-spring-boot-autoconfigure/pom.xml
@@ -0,0 +1,79 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../../pom.xml
+
+ spring-ai-watsonx-ai-spring-boot-autoconfigure
+ jar
+ Spring AI Watsonx AI Auto Configuration
+ Spring AI Watsonx Auto Configuration
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-watsonx-ai
+ ${project.parent.version}
+ true
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-chat-client-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
diff --git a/auto-configurations/models/spring-ai-watsonx-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiAutoConfiguration.java b/auto-configurations/models/spring-ai-watsonx-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiAutoConfiguration.java
new file mode 100644
index 00000000000..e5d53314bb6
--- /dev/null
+++ b/auto-configurations/models/spring-ai-watsonx-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/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.autoconfigure.watsonxai;
+
+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/auto-configurations/models/spring-ai-watsonx-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiChatProperties.java b/auto-configurations/models/spring-ai-watsonx-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiChatProperties.java
new file mode 100644
index 00000000000..fbd14c698c3
--- /dev/null
+++ b/auto-configurations/models/spring-ai-watsonx-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/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.autoconfigure.watsonxai;
+
+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/auto-configurations/models/spring-ai-watsonx-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiConnectionProperties.java b/auto-configurations/models/spring-ai-watsonx-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiConnectionProperties.java
new file mode 100644
index 00000000000..5e4fecf2133
--- /dev/null
+++ b/auto-configurations/models/spring-ai-watsonx-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/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.autoconfigure.watsonxai;
+
+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/auto-configurations/models/spring-ai-watsonx-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiEmbeddingProperties.java b/auto-configurations/models/spring-ai-watsonx-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiEmbeddingProperties.java
new file mode 100644
index 00000000000..983291d21ed
--- /dev/null
+++ b/auto-configurations/models/spring-ai-watsonx-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/watsonxai/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.autoconfigure.watsonxai;
+
+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/auto-configurations/models/spring-ai-watsonx-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-watsonx-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..58b25b9763a
--- /dev/null
+++ b/auto-configurations/models/spring-ai-watsonx-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,16 @@
+#
+# Copyright 2025-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.
+#
+org.springframework.ai.autoconfigure.watsonxai.WatsonxAiAutoConfiguration
diff --git a/auto-configurations/models/spring-ai-watsonx-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiAutoConfigurationTests.java b/auto-configurations/models/spring-ai-watsonx-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/watsonxai/WatsonxAiAutoConfigurationTests.java
new file mode 100644
index 00000000000..1017332b342
--- /dev/null
+++ b/auto-configurations/models/spring-ai-watsonx-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/watsonxai/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.autoconfigure.watsonxai;
+
+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/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/pom.xml b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/pom.xml
new file mode 100644
index 00000000000..3f137202e91
--- /dev/null
+++ b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/pom.xml
@@ -0,0 +1,86 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../../pom.xml
+
+ spring-ai-zhipuai-spring-boot-autoconfigure
+ jar
+ Spring AI ZhipuAI Auto Configuration
+ Spring AI ZhipuAI Auto Configuration
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-zhipuai
+ ${project.parent.version}
+ true
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-chat-client-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+ org.springframework.ai
+ spring-ai-retry-spring-boot-autoconfigure
+ ${project.parent.version}
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
diff --git a/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiAutoConfiguration.java b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiAutoConfiguration.java
new file mode 100644
index 00000000000..4ff96a55525
--- /dev/null
+++ b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/zhipuai/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.autoconfigure.zhipuai;
+
+import java.util.List;
+
+import io.micrometer.observation.ObservationRegistry;
+
+import org.springframework.ai.autoconfigure.retry.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/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiChatProperties.java b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiChatProperties.java
new file mode 100644
index 00000000000..516105c2552
--- /dev/null
+++ b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/zhipuai/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.autoconfigure.zhipuai;
+
+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/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiConnectionProperties.java b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiConnectionProperties.java
new file mode 100644
index 00000000000..798afd43a97
--- /dev/null
+++ b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/zhipuai/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.autoconfigure.zhipuai;
+
+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/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiEmbeddingProperties.java b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiEmbeddingProperties.java
new file mode 100644
index 00000000000..f069170517d
--- /dev/null
+++ b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/zhipuai/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.autoconfigure.zhipuai;
+
+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/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiImageProperties.java b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiImageProperties.java
new file mode 100644
index 00000000000..7266eeb13bb
--- /dev/null
+++ b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/zhipuai/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.autoconfigure.zhipuai;
+
+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/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiParentProperties.java b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiParentProperties.java
new file mode 100644
index 00000000000..c89102ec103
--- /dev/null
+++ b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/zhipuai/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.autoconfigure.zhipuai;
+
+/**
+ * @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/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..741d559d4bd
--- /dev/null
+++ b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,16 @@
+#
+# Copyright 2025-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.
+#
+org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiAutoConfiguration
diff --git a/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiAutoConfigurationIT.java b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiAutoConfigurationIT.java
new file mode 100644
index 00000000000..f60814cb4cf
--- /dev/null
+++ b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/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.autoconfigure.zhipuai;
+
+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.autoconfigure.retry.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/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiPropertiesTests.java b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/ZhiPuAiPropertiesTests.java
new file mode 100644
index 00000000000..ac6e743e53f
--- /dev/null
+++ b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/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.autoconfigure.zhipuai;
+
+import org.junit.jupiter.api.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.skyscreamer.jsonassert.JSONCompareMode;
+
+import org.springframework.ai.autoconfigure.retry.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/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackInPromptIT.java b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackInPromptIT.java
new file mode 100644
index 00000000000..556e7df8c19
--- /dev/null
+++ b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/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.autoconfigure.zhipuai.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.autoconfigure.retry.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.autoconfigure.zhipuai.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/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackWithPlainFunctionBeanIT.java b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackWithPlainFunctionBeanIT.java
new file mode 100644
index 00000000000..714ea619c44
--- /dev/null
+++ b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/FunctionCallbackWithPlainFunctionBeanIT.java
@@ -0,0 +1,176 @@
+/*
+ * 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.autoconfigure.zhipuai.tool;
+
+import java.util.List;
+import java.util.function.Function;
+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.autoconfigure.retry.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.autoconfigure.zhipuai.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.FunctionCallingOptions;
+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 org.springframework.context.annotation.Description;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Geng Rong
+ */
+@EnabledIfEnvironmentVariable(named = "ZHIPU_AI_API_KEY", matches = ".*")
+class FunctionCallbackWithPlainFunctionBeanIT {
+
+ private final Logger logger = LoggerFactory.getLogger(FunctionCallbackWithPlainFunctionBeanIT.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);
+
+ // Test weatherFunction
+ 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("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),
+ ZhiPuAiChatOptions.builder().function("weatherFunctionTwo").build()));
+
+ logger.info("Response: {}", response);
+
+ assertThat(response.getResult().getOutput().getText()).contains("30", "10", "15");
+
+ });
+ }
+
+ @Test
+ void functionCallWithPortableFunctionCallingOptions() {
+ this.contextRunner.withPropertyValues("spring.ai.zhipuai.chat.options.model=glm-4").run(context -> {
+
+ ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.class);
+
+ // Test weatherFunction
+ UserMessage userMessage = new UserMessage(
+ "What's the weather like in San Francisco, Tokyo, and Paris? Return the temperature in Celsius.");
+
+ FunctionCallingOptions functionOptions = FunctionCallingOptions.builder()
+ .function("weatherFunction")
+ .build();
+
+ ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), functionOptions));
+
+ logger.info("Response: {}", response);
+ });
+ }
+
+ @Test
+ void streamFunctionCallTest() {
+ this.contextRunner.withPropertyValues("spring.ai.zhipuai.chat.options.model=glm-4").run(context -> {
+
+ ZhiPuAiChatModel chatModel = context.getBean(ZhiPuAiChatModel.class);
+
+ // Test weatherFunction
+ 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("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).containsAnyOf("30.0", "30");
+ assertThat(content).containsAnyOf("10.0", "10");
+ assertThat(content).containsAnyOf("15.0", "15");
+
+ // Test weatherFunctionTwo
+ response = chatModel.stream(new Prompt(List.of(userMessage),
+ ZhiPuAiChatOptions.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).containsAnyOf("30.0", "30");
+ assertThat(content).containsAnyOf("10.0", "10");
+ assertThat(content).containsAnyOf("15.0", "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 weatherFunctionTwo() {
+ MockWeatherService weatherService = new MockWeatherService();
+ return (weatherService::apply);
+ }
+
+ }
+
+}
diff --git a/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/MockWeatherService.java b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/MockWeatherService.java
new file mode 100644
index 00000000000..43b4af8a57e
--- /dev/null
+++ b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/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.autoconfigure.zhipuai.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/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/ZhipuAiFunctionCallbackIT.java b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/tool/ZhipuAiFunctionCallbackIT.java
new file mode 100644
index 00000000000..6b258e98d1a
--- /dev/null
+++ b/auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/zhipuai/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.autoconfigure.zhipuai.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.autoconfigure.retry.SpringAiRetryAutoConfiguration;
+import org.springframework.ai.autoconfigure.zhipuai.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/auto-configurations/observation/chat/spring-ai-chat-observation-spring-boot-autoconfigure/pom.xml b/auto-configurations/observation/chat/spring-ai-chat-observation-spring-boot-autoconfigure/pom.xml
new file mode 100644
index 00000000000..8bd46177cf0
--- /dev/null
+++ b/auto-configurations/observation/chat/spring-ai-chat-observation-spring-boot-autoconfigure/pom.xml
@@ -0,0 +1,72 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../../../pom.xml
+
+ spring-ai-chat-observation-spring-boot-autoconfigure
+ jar
+ Spring AI Chat Observation Auto Configuration
+ Spring AI Chat Observation Auto Configuration
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-core
+ ${parent.version}
+
+
+
+ io.micrometer
+ micrometer-tracing-bridge-otel
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
diff --git a/auto-configurations/observation/chat/spring-ai-chat-observation-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationAutoConfiguration.java b/auto-configurations/observation/chat/spring-ai-chat-observation-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationAutoConfiguration.java
new file mode 100644
index 00000000000..f439263fdad
--- /dev/null
+++ b/auto-configurations/observation/chat/spring-ai-chat-observation-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationAutoConfiguration.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.autoconfigure.chat.observation;
+
+import java.util.List;
+
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.tracing.Tracer;
+import io.micrometer.tracing.otel.bridge.OtelTracer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.ai.chat.client.advisor.observation.AdvisorObservationContext;
+import org.springframework.ai.chat.client.observation.ChatClientObservationContext;
+import org.springframework.ai.chat.model.ChatModel;
+import org.springframework.ai.chat.observation.ChatModelCompletionObservationFilter;
+import org.springframework.ai.chat.observation.ChatModelCompletionObservationHandler;
+import org.springframework.ai.chat.observation.ChatModelMeterObservationHandler;
+import org.springframework.ai.chat.observation.ChatModelObservationContext;
+import org.springframework.ai.chat.observation.ChatModelPromptContentObservationFilter;
+import org.springframework.ai.chat.observation.ChatModelPromptContentObservationHandler;
+import org.springframework.ai.embedding.observation.EmbeddingModelObservationContext;
+import org.springframework.ai.image.observation.ImageModelObservationContext;
+import org.springframework.ai.model.observation.ErrorLoggingObservationHandler;
+import org.springframework.ai.vectorstore.observation.VectorStoreObservationContext;
+import org.springframework.beans.factory.ObjectProvider;
+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.ConditionalOnMissingClass;
+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.Configuration;
+
+/**
+ * Auto-configuration for Spring AI chat model observations.
+ *
+ * @author Thomas Vitale
+ * @since 1.0.0
+ */
+@AutoConfiguration(
+ afterName = { "org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration" })
+@ConditionalOnClass(ChatModel.class)
+@EnableConfigurationProperties({ ChatObservationProperties.class })
+public class ChatObservationAutoConfiguration {
+
+ private static final Logger logger = LoggerFactory.getLogger(ChatObservationAutoConfiguration.class);
+
+ private static void logPromptContentWarning() {
+ logger.warn(
+ "You have enabled the inclusion of the prompt content in the observations, with the risk of exposing sensitive or private information. Please, be careful!");
+ }
+
+ private static void logCompletionWarning() {
+ logger.warn(
+ "You have enabled the inclusion of the completion content in the observations, with the risk of exposing sensitive or private information. Please, be careful!");
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnBean(MeterRegistry.class)
+ ChatModelMeterObservationHandler chatModelMeterObservationHandler(ObjectProvider meterRegistry) {
+ return new ChatModelMeterObservationHandler(meterRegistry.getObject());
+ }
+
+ /**
+ * The chat content is typically too big to be included in an observation as span
+ * attributes. That's why the preferred way to store it is as span events, which are
+ * supported by OpenTelemetry but not yet surfaced through the Micrometer APIs. This
+ * primary/fallback configuration is a temporary solution until
+ * https://github.com/micrometer-metrics/micrometer/issues/5238 is delivered.
+ */
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnClass(OtelTracer.class)
+ @ConditionalOnBean(OtelTracer.class)
+ static class PrimaryChatContentObservationConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = ChatObservationProperties.CONFIG_PREFIX, name = "include-prompt",
+ havingValue = "true")
+ ChatModelPromptContentObservationHandler chatModelPromptContentObservationHandler() {
+ logPromptContentWarning();
+ return new ChatModelPromptContentObservationHandler();
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = ChatObservationProperties.CONFIG_PREFIX, name = "include-completion",
+ havingValue = "true")
+ ChatModelCompletionObservationHandler chatModelCompletionObservationHandler() {
+ logCompletionWarning();
+ return new ChatModelCompletionObservationHandler();
+ }
+
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnMissingClass("io.micrometer.tracing.otel.bridge.OtelTracer")
+ static class FallbackChatContentObservationConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = ChatObservationProperties.CONFIG_PREFIX, name = "include-prompt",
+ havingValue = "true")
+ ChatModelPromptContentObservationFilter chatModelPromptObservationFilter() {
+ logPromptContentWarning();
+ return new ChatModelPromptContentObservationFilter();
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = ChatObservationProperties.CONFIG_PREFIX, name = "include-completion",
+ havingValue = "true")
+ ChatModelCompletionObservationFilter chatModelCompletionObservationFilter() {
+ logCompletionWarning();
+ return new ChatModelCompletionObservationFilter();
+ }
+
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ @ConditionalOnClass(Tracer.class)
+ @ConditionalOnBean(Tracer.class)
+ static class TracingChatContentObservationConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = ChatObservationProperties.CONFIG_PREFIX, name = "include-error-logging",
+ havingValue = "true")
+ public ErrorLoggingObservationHandler errorLoggingObservationHandler(Tracer tracer) {
+ return new ErrorLoggingObservationHandler(tracer,
+ List.of(EmbeddingModelObservationContext.class, ImageModelObservationContext.class,
+ ChatModelObservationContext.class, ChatClientObservationContext.class,
+ AdvisorObservationContext.class, VectorStoreObservationContext.class));
+ }
+
+ }
+
+}
diff --git a/auto-configurations/observation/chat/spring-ai-chat-observation-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationProperties.java b/auto-configurations/observation/chat/spring-ai-chat-observation-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationProperties.java
new file mode 100644
index 00000000000..cd353ac0b97
--- /dev/null
+++ b/auto-configurations/observation/chat/spring-ai-chat-observation-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationProperties.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.autoconfigure.chat.observation;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Configuration properties for chat model observations.
+ *
+ * @author Thomas Vitale
+ * @since 1.0.0
+ */
+@ConfigurationProperties(ChatObservationProperties.CONFIG_PREFIX)
+public class ChatObservationProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.chat.observations";
+
+ /**
+ * Whether to include the completion content in the observations.
+ */
+ private boolean includeCompletion = false;
+
+ /**
+ * Whether to include the prompt content in the observations.
+ */
+ private boolean includePrompt = false;
+
+ /**
+ * Whether to include error logging in the observations.
+ */
+ private boolean includeErrorLogging = false;
+
+ public boolean isIncludeCompletion() {
+ return this.includeCompletion;
+ }
+
+ public void setIncludeCompletion(boolean includeCompletion) {
+ this.includeCompletion = includeCompletion;
+ }
+
+ public boolean isIncludePrompt() {
+ return this.includePrompt;
+ }
+
+ public void setIncludePrompt(boolean includePrompt) {
+ this.includePrompt = includePrompt;
+ }
+
+ public boolean isIncludeErrorLogging() {
+ return this.includeErrorLogging;
+ }
+
+ public void setIncludeErrorLogging(boolean includeErrorLogging) {
+ this.includeErrorLogging = includeErrorLogging;
+ }
+
+}
diff --git a/auto-configurations/observation/chat/spring-ai-chat-observation-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/package-info.java b/auto-configurations/observation/chat/spring-ai-chat-observation-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/package-info.java
new file mode 100644
index 00000000000..6032da63a0a
--- /dev/null
+++ b/auto-configurations/observation/chat/spring-ai-chat-observation-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/chat/observation/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+/**
+ * Auto-configuration for chat observation.
+ */
+@NonNullApi
+@NonNullFields
+package org.springframework.ai.autoconfigure.chat.observation;
+
+import org.springframework.lang.NonNullApi;
+import org.springframework.lang.NonNullFields;
diff --git a/auto-configurations/observation/chat/spring-ai-chat-observation-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/observation/chat/spring-ai-chat-observation-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..9493b75876d
--- /dev/null
+++ b/auto-configurations/observation/chat/spring-ai-chat-observation-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,16 @@
+#
+# Copyright 2025-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.
+#
+org.springframework.ai.autoconfigure.chat.observation.ChatObservationAutoConfiguration
diff --git a/auto-configurations/observation/chat/spring-ai-chat-observation-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationAutoConfigurationTests.java b/auto-configurations/observation/chat/spring-ai-chat-observation-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationAutoConfigurationTests.java
new file mode 100644
index 00000000000..d1601098168
--- /dev/null
+++ b/auto-configurations/observation/chat/spring-ai-chat-observation-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/chat/observation/ChatObservationAutoConfigurationTests.java
@@ -0,0 +1,108 @@
+/*
+ * 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.autoconfigure.chat.observation;
+
+import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
+import io.micrometer.tracing.otel.bridge.OtelCurrentTraceContext;
+import io.micrometer.tracing.otel.bridge.OtelTracer;
+import io.opentelemetry.api.OpenTelemetry;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.chat.observation.ChatModelCompletionObservationFilter;
+import org.springframework.ai.chat.observation.ChatModelCompletionObservationHandler;
+import org.springframework.ai.chat.observation.ChatModelMeterObservationHandler;
+import org.springframework.ai.chat.observation.ChatModelPromptContentObservationFilter;
+import org.springframework.ai.chat.observation.ChatModelPromptContentObservationHandler;
+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 ChatObservationAutoConfiguration}.
+ *
+ * @author Thomas Vitale
+ */
+class ChatObservationAutoConfigurationTests {
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(ChatObservationAutoConfiguration.class));
+
+ @Test
+ void meterObservationHandlerEnabled() {
+ this.contextRunner.withBean(CompositeMeterRegistry.class)
+ .run(context -> assertThat(context).hasSingleBean(ChatModelMeterObservationHandler.class));
+ }
+
+ @Test
+ void meterObservationHandlerDisabled() {
+ this.contextRunner.run(context -> assertThat(context).doesNotHaveBean(ChatModelMeterObservationHandler.class));
+ }
+
+ @Test
+ void promptFilterDefault() {
+ this.contextRunner
+ .run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationFilter.class));
+ }
+
+ @Test
+ void promptHandlerDefault() {
+ this.contextRunner
+ .run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class));
+ }
+
+ @Test
+ void promptHandlerEnabled() {
+ this.contextRunner
+ .withBean(OtelTracer.class, OpenTelemetry.noop().getTracer("test"), new OtelCurrentTraceContext(), null)
+ .withPropertyValues("spring.ai.chat.observations.include-prompt=true")
+ .run(context -> assertThat(context).hasSingleBean(ChatModelPromptContentObservationHandler.class));
+ }
+
+ @Test
+ void promptHandlerDisabled() {
+ this.contextRunner.withPropertyValues("spring.ai.chat.observations.include-prompt=true")
+ .run(context -> assertThat(context).doesNotHaveBean(ChatModelPromptContentObservationHandler.class));
+ }
+
+ @Test
+ void completionFilterDefault() {
+ this.contextRunner
+ .run(context -> assertThat(context).doesNotHaveBean(ChatModelCompletionObservationFilter.class));
+ }
+
+ @Test
+ void completionHandlerDefault() {
+ this.contextRunner
+ .run(context -> assertThat(context).doesNotHaveBean(ChatModelCompletionObservationHandler.class));
+ }
+
+ @Test
+ void completionHandlerEnabled() {
+ this.contextRunner
+ .withBean(OtelTracer.class, OpenTelemetry.noop().getTracer("test"), new OtelCurrentTraceContext(), null)
+ .withPropertyValues("spring.ai.chat.observations.include-completion=true")
+ .run(context -> assertThat(context).hasSingleBean(ChatModelCompletionObservationHandler.class));
+ }
+
+ @Test
+ void completionHandlerDisabled() {
+ this.contextRunner.withPropertyValues("spring.ai.chat.observations.include-completion=true")
+ .run(context -> assertThat(context).doesNotHaveBean(ChatModelCompletionObservationHandler.class));
+ }
+
+}
diff --git a/auto-configurations/observation/embedding/spring-ai-embedding-observation-spring-boot-autoconfigure/pom.xml b/auto-configurations/observation/embedding/spring-ai-embedding-observation-spring-boot-autoconfigure/pom.xml
new file mode 100644
index 00000000000..d252bb6e7cb
--- /dev/null
+++ b/auto-configurations/observation/embedding/spring-ai-embedding-observation-spring-boot-autoconfigure/pom.xml
@@ -0,0 +1,66 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../../../pom.xml
+
+ spring-ai-embedding-observation-spring-boot-autoconfigure
+ jar
+ Spring AI Embedding Observation Auto Configuration
+ Spring AI Embedding Observation Auto Configuration
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-core
+ ${parent.version}
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
diff --git a/auto-configurations/observation/embedding/spring-ai-embedding-observation-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/embedding/observation/EmbeddingObservationAutoConfiguration.java b/auto-configurations/observation/embedding/spring-ai-embedding-observation-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/embedding/observation/EmbeddingObservationAutoConfiguration.java
new file mode 100644
index 00000000000..ba1d61886f2
--- /dev/null
+++ b/auto-configurations/observation/embedding/spring-ai-embedding-observation-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/embedding/observation/EmbeddingObservationAutoConfiguration.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.autoconfigure.embedding.observation;
+
+import io.micrometer.core.instrument.MeterRegistry;
+
+import org.springframework.ai.embedding.EmbeddingModel;
+import org.springframework.ai.embedding.observation.EmbeddingModelMeterObservationHandler;
+import org.springframework.beans.factory.ObjectProvider;
+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.context.annotation.Bean;
+
+/**
+ * Auto-configuration for Spring AI embedding model observations.
+ *
+ * @author Thomas Vitale
+ * @since 1.0.0
+ */
+@AutoConfiguration(
+ afterName = "org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration")
+@ConditionalOnClass(EmbeddingModel.class)
+public class EmbeddingObservationAutoConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnBean(MeterRegistry.class)
+ EmbeddingModelMeterObservationHandler embeddingModelMeterObservationHandler(
+ ObjectProvider meterRegistry) {
+ return new EmbeddingModelMeterObservationHandler(meterRegistry.getObject());
+ }
+
+}
diff --git a/auto-configurations/observation/embedding/spring-ai-embedding-observation-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/embedding/observation/package-info.java b/auto-configurations/observation/embedding/spring-ai-embedding-observation-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/embedding/observation/package-info.java
new file mode 100644
index 00000000000..2275a771ee8
--- /dev/null
+++ b/auto-configurations/observation/embedding/spring-ai-embedding-observation-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/embedding/observation/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+/**
+ * Auto-configuration for embedding observation.
+ */
+@NonNullApi
+@NonNullFields
+package org.springframework.ai.autoconfigure.embedding.observation;
+
+import org.springframework.lang.NonNullApi;
+import org.springframework.lang.NonNullFields;
diff --git a/auto-configurations/observation/embedding/spring-ai-embedding-observation-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/observation/embedding/spring-ai-embedding-observation-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..162692d8ab9
--- /dev/null
+++ b/auto-configurations/observation/embedding/spring-ai-embedding-observation-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,16 @@
+#
+# Copyright 2025-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.
+#
+org.springframework.ai.autoconfigure.embedding.observation.EmbeddingObservationAutoConfiguration
diff --git a/auto-configurations/observation/embedding/spring-ai-embedding-observation-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/embedding/observation/EmbeddingObservationAutoConfigurationTests.java b/auto-configurations/observation/embedding/spring-ai-embedding-observation-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/embedding/observation/EmbeddingObservationAutoConfigurationTests.java
new file mode 100644
index 00000000000..42037c9e064
--- /dev/null
+++ b/auto-configurations/observation/embedding/spring-ai-embedding-observation-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/embedding/observation/EmbeddingObservationAutoConfigurationTests.java
@@ -0,0 +1,50 @@
+/*
+ * 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.autoconfigure.embedding.observation;
+
+import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.embedding.observation.EmbeddingModelMeterObservationHandler;
+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 EmbeddingObservationAutoConfiguration}.
+ *
+ * @author Thomas Vitale
+ */
+class EmbeddingObservationAutoConfigurationTests {
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(EmbeddingObservationAutoConfiguration.class));
+
+ @Test
+ void meterObservationHandlerEnabled() {
+ this.contextRunner.withBean(CompositeMeterRegistry.class)
+ .run(context -> assertThat(context).hasSingleBean(EmbeddingModelMeterObservationHandler.class));
+ }
+
+ @Test
+ void meterObservationHandlerDisabled() {
+ this.contextRunner
+ .run(context -> assertThat(context).doesNotHaveBean(EmbeddingModelMeterObservationHandler.class));
+ }
+
+}
diff --git a/auto-configurations/observation/image/spring-ai-image-observation-spring-boot-autoconfigure/pom.xml b/auto-configurations/observation/image/spring-ai-image-observation-spring-boot-autoconfigure/pom.xml
new file mode 100644
index 00000000000..82a12f1002e
--- /dev/null
+++ b/auto-configurations/observation/image/spring-ai-image-observation-spring-boot-autoconfigure/pom.xml
@@ -0,0 +1,66 @@
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai
+ 1.0.0-SNAPSHOT
+ ../../../../pom.xml
+
+ spring-ai-image-observation-spring-boot-autoconfigure
+ jar
+ Spring AI Image Observation Auto Configuration
+ Spring AI Image Observation Auto Configuration
+ https://github.com/spring-projects/spring-ai
+
+
+ https://github.com/spring-projects/spring-ai
+ git://github.com/spring-projects/spring-ai.git
+ git@github.com:spring-projects/spring-ai.git
+
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-core
+ ${parent.version}
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
diff --git a/auto-configurations/observation/image/spring-ai-image-observation-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationAutoConfiguration.java b/auto-configurations/observation/image/spring-ai-image-observation-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationAutoConfiguration.java
new file mode 100644
index 00000000000..a89a42102a3
--- /dev/null
+++ b/auto-configurations/observation/image/spring-ai-image-observation-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationAutoConfiguration.java
@@ -0,0 +1,55 @@
+/*
+ * 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.autoconfigure.image.observation;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.ai.image.ImageModel;
+import org.springframework.ai.image.observation.ImageModelPromptContentObservationFilter;
+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;
+
+/**
+ * Auto-configuration for Spring AI image model observations.
+ *
+ * @author Thomas Vitale
+ * @since 1.0.0
+ */
+@AutoConfiguration(
+ afterName = "org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration.class")
+@ConditionalOnClass(ImageModel.class)
+@EnableConfigurationProperties({ ImageObservationProperties.class })
+public class ImageObservationAutoConfiguration {
+
+ private static final Logger logger = LoggerFactory.getLogger(ImageObservationAutoConfiguration.class);
+
+ @Bean
+ @ConditionalOnMissingBean
+ @ConditionalOnProperty(prefix = ImageObservationProperties.CONFIG_PREFIX, name = "include-prompt",
+ havingValue = "true")
+ ImageModelPromptContentObservationFilter imageModelPromptObservationFilter() {
+ logger.warn(
+ "You have enabled the inclusion of the image prompt content in the observations, with the risk of exposing sensitive or private information. Please, be careful!");
+ return new ImageModelPromptContentObservationFilter();
+ }
+
+}
diff --git a/auto-configurations/observation/image/spring-ai-image-observation-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationProperties.java b/auto-configurations/observation/image/spring-ai-image-observation-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationProperties.java
new file mode 100644
index 00000000000..3e454ee8d20
--- /dev/null
+++ b/auto-configurations/observation/image/spring-ai-image-observation-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationProperties.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.autoconfigure.image.observation;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Configuration properties for image model observations.
+ *
+ * @author Thomas Vitale
+ * @since 1.0.0
+ */
+@ConfigurationProperties(ImageObservationProperties.CONFIG_PREFIX)
+public class ImageObservationProperties {
+
+ public static final String CONFIG_PREFIX = "spring.ai.image.observations";
+
+ /**
+ * Whether to include the prompt content in the observations.
+ */
+ private boolean includePrompt = false;
+
+ public boolean isIncludePrompt() {
+ return this.includePrompt;
+ }
+
+ public void setIncludePrompt(boolean includePrompt) {
+ this.includePrompt = includePrompt;
+ }
+
+}
diff --git a/auto-configurations/observation/image/spring-ai-image-observation-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/image/observation/package-info.java b/auto-configurations/observation/image/spring-ai-image-observation-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/image/observation/package-info.java
new file mode 100644
index 00000000000..559f61df956
--- /dev/null
+++ b/auto-configurations/observation/image/spring-ai-image-observation-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/image/observation/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+/**
+ * Auto-configuration for image observation.
+ */
+@NonNullApi
+@NonNullFields
+package org.springframework.ai.autoconfigure.image.observation;
+
+import org.springframework.lang.NonNullApi;
+import org.springframework.lang.NonNullFields;
diff --git a/auto-configurations/observation/image/spring-ai-image-observation-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/observation/image/spring-ai-image-observation-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 00000000000..78af4bcffe8
--- /dev/null
+++ b/auto-configurations/observation/image/spring-ai-image-observation-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,16 @@
+#
+# Copyright 2025-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.
+#
+org.springframework.ai.autoconfigure.image.observation.ImageObservationAutoConfiguration
diff --git a/auto-configurations/observation/image/spring-ai-image-observation-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationAutoConfigurationTests.java b/auto-configurations/observation/image/spring-ai-image-observation-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationAutoConfigurationTests.java
new file mode 100644
index 00000000000..deb0a22ade3
--- /dev/null
+++ b/auto-configurations/observation/image/spring-ai-image-observation-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/image/observation/ImageObservationAutoConfigurationTests.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.autoconfigure.image.observation;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.image.observation.ImageModelPromptContentObservationFilter;
+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 ImageObservationAutoConfiguration}.
+ *
+ * @author Thomas Vitale
+ */
+class ImageObservationAutoConfigurationTests {
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(ImageObservationAutoConfiguration.class));
+
+ @Test
+ void promptFilterDefault() {
+ this.contextRunner
+ .run(context -> assertThat(context).doesNotHaveBean(ImageModelPromptContentObservationFilter.class));
+ }
+
+ @Test
+ void promptFilterEnabled() {
+ this.contextRunner.withPropertyValues("spring.ai.image.observations.include-prompt=true")
+ .run(context -> assertThat(context).hasSingleBean(ImageModelPromptContentObservationFilter.class));
+ }
+
+}
diff --git a/pom.xml b/pom.xml
index 2d1236bd4b3..f95d3448c6c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -35,12 +35,41 @@
spring-ai-core
spring-ai-test
- spring-ai-spring-boot-autoconfigure
+ auto-configurations/common/spring-ai-retry-spring-boot-autoconfigure
+
+ auto-configurations/chat/spring-ai-chat-client-spring-boot-autoconfigure
+ auto-configurations/chat/spring-ai-chat-model-spring-boot-autoconfigure
+ auto-configurations/chat/spring-ai-chat-memory-spring-boot-autoconfigure
+
+ auto-configurations/models/spring-ai-anthropic-spring-boot-autoconfigure
+ auto-configurations/models/spring-ai-azure-openai-spring-boot-autoconfigure
+ auto-configurations/models/spring-ai-bedrock-spring-boot-autoconfigure
+ auto-configurations/models/spring-ai-huggingface-spring-boot-autoconfigure
+ auto-configurations/models/spring-ai-minimax-spring-boot-autoconfigure
+ auto-configurations/models/spring-ai-mistral-ai-spring-boot-autoconfigure
+ auto-configurations/models/spring-ai-moonshot-spring-boot-autoconfigure
+ auto-configurations/models/spring-ai-oci-genai-spring-boot-autoconfigure
+ auto-configurations/models/spring-ai-ollama-spring-boot-autoconfigure
+ auto-configurations/models/spring-ai-openai-spring-boot-autoconfigure
+ auto-configurations/models/spring-ai-postgresml-spring-boot-autoconfigure
+ auto-configurations/models/spring-ai-qianfan-spring-boot-autoconfigure
+ auto-configurations/models/spring-ai-stability-ai-spring-boot-autoconfigure
+ auto-configurations/models/spring-ai-transformers-spring-boot-autoconfigure
+ auto-configurations/models/spring-ai-vertex-ai-spring-boot-autoconfigure
+ auto-configurations/models/spring-ai-watsonx-ai-spring-boot-autoconfigure
+ auto-configurations/models/spring-ai-zhipuai-spring-boot-autoconfigure
+
+ auto-configurations/observation/chat/spring-ai-chat-observation-spring-boot-autoconfigure
+ auto-configurations/observation/embedding/spring-ai-embedding-observation-spring-boot-autoconfigure
+ auto-configurations/observation/image/spring-ai-image-observation-spring-boot-autoconfigure
+
+ auto-configurations/mcp/spring-ai-mcp-client
+ auto-configurations/mcp/spring-ai-mcp-server
- auto-configurations/spring-ai-mcp-client
- auto-configurations/spring-ai-mcp-server
auto-configurations/vector-stores/spring-ai-weaviate-store-spring-boot-autoconfigure
+ spring-ai-spring-boot-autoconfigure
+
spring-ai-retry
spring-ai-spring-boot-docker-compose
spring-ai-spring-boot-testcontainers
diff --git a/spring-ai-bom/pom.xml b/spring-ai-bom/pom.xml
index 093caee4dc5..7b039bfc729 100644
--- a/spring-ai-bom/pom.xml
+++ b/spring-ai-bom/pom.xml
@@ -304,12 +304,44 @@
${project.version}
+
org.springframework.ai
spring-ai-spring-boot-autoconfigure
${project.version}
+
+
+
+
+ org.springframework.ai
+ spring-ai-retry-spring-boot-autoconfigure
+ ${project.version}
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-chat-client-spring-boot-autoconfigure
+ ${project.version}
+
+
+
+ org.springframework.ai
+ spring-ai-chat-model-spring-boot-autoconfigure
+ ${project.version}
+
+
+
+ org.springframework.ai
+ spring-ai-chat-memory-spring-boot-autoconfigure
+ ${project.version}
+
+
+
+
org.springframework.ai
spring-ai-mcp-client-spring-boot-autoconfigure
@@ -322,6 +354,125 @@
${project.version}
+
+
+
+ org.springframework.ai
+ spring-ai-anthroic-spring-boot-autoconfigure
+ ${project.version}
+
+
+
+ org.springframework.ai
+ spring-ai-azure-openai-spring-boot-autoconfigure
+ ${project.version}
+
+
+
+ org.springframework.ai
+ spring-ai-bedrock-spring-boot-autoconfigure
+ ${project.version}
+
+
+
+ org.springframework.ai
+ spring-ai-huggingface-spring-boot-autoconfigure
+ ${project.version}
+
+
+
+ org.springframework.ai
+ spring-ai-minimax-spring-boot-autoconfigure
+ ${project.version}
+
+
+
+ org.springframework.ai
+ spring-ai-mistral-ai-spring-boot-autoconfigure
+ ${project.version}
+
+
+
+ org.springframework.ai
+ spring-ai-moonshot-spring-boot-autoconfigure
+ ${project.version}
+
+
+
+ org.springframework.ai
+ spring-ai-oci-genai-spring-boot-autoconfigure
+ ${project.version}
+
+
+
+ org.springframework.ai
+ spring-ai-ollama-spring-boot-autoconfigure
+ ${project.version}
+
+
+
+ org.springframework.ai
+ spring-ai-openai-spring-boot-autoconfigure
+ ${project.version}
+
+
+
+ org.springframework.ai
+ spring-ai-postgresml-spring-boot-autoconfigure
+ ${project.version}
+
+
+
+ org.springframework.ai
+ spring-ai-qianfan-spring-boot-autoconfigure
+ ${project.version}
+
+
+
+ org.springframework.ai
+ spring-ai-stability-ai-spring-boot-autoconfigure
+ ${project.version}
+
+
+
+ org.springframework.ai
+ spring-ai-transformers-spring-boot-autoconfigure
+ ${project.version}
+
+
+
+ org.springframework.ai
+ spring-ai-vertex-ai-spring-boot-autoconfigure
+ ${project.version}
+
+
+
+ org.springframework.ai
+ spring-ai-watsonx-ai-spring-boot-autoconfigure
+ ${project.version}
+
+
+
+ org.springframework.ai
+ spring-ai-zhipuai-spring-boot-autoconfigure
+ ${project.version}
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-chat-observation-spring-boot-autoconfigure
+ ${project.version}
+
+
+
+ org.springframework.ai
+ spring-ai-embedding-observation-spring-boot-autoconfigure
+ ${project.version}
+
+
+
org.springframework.ai
diff --git a/spring-ai-spring-boot-docker-compose/pom.xml b/spring-ai-spring-boot-docker-compose/pom.xml
index 68e1b02e53d..c938d1310e5 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-starters/spring-ai-starter-anthropic/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-anthropic/pom.xml
index 31e711ba2b7..45e904cef78 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-anthropic/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-anthropic/pom.xml
@@ -26,7 +26,7 @@
spring-ai-anthropic-spring-boot-starter
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-anthropic-spring-boot-autoconfigure
${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-azure-openai/pom.xml
index 7ccc8ef96bf..a856f1d1e19 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-azure-openai/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-azure-openai/pom.xml
@@ -26,7 +26,7 @@
spring-ai-azure-openai-spring-boot-starter
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-azure-openai-spring-boot-autoconfigure
${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-bedrock-ai/pom.xml
index fc95593d500..f858a5af6e4 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-bedrock-ai/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-bedrock-ai/pom.xml
@@ -27,7 +27,7 @@
spring-ai-bedrock-ai-spring-boot-starter
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-bedrock-spring-boot-autoconfigure
${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-bedrock-converse/pom.xml
index e44dc1940b4..1780c301514 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-bedrock-converse/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-bedrock-converse/pom.xml
@@ -27,7 +27,7 @@
spring-ai-bedrock-converse-spring-boot-starter
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-bedrock-spring-boot-autoconfigure
${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-huggingface/pom.xml
index 0832116aa3c..a2bdd674dee 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-huggingface/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-huggingface/pom.xml
@@ -26,7 +26,7 @@
spring-ai-huggingface-spring-boot-starter
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-huggingface-spring-boot-autoconfigure
${project.parent.version}
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 0d2c8b210e8..531a90b25a6 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 06a22b85a89..acef845887c 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 e847b730fe9..ddb133f8fee 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 42d9a66a440..824b37007e6 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 15d0b79fc56..1d7dc35bbd5 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-minimax/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-minimax/pom.xml
index 124570853ac..f2e737d8d6f 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-minimax/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-minimax/pom.xml
@@ -26,7 +26,7 @@
spring-ai-minimax-spring-boot-starter
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-minimax-spring-boot-autoconfigure
${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-mistral-ai/pom.xml
index 4c00f83262b..c0fa588c36d 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-mistral-ai/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-mistral-ai/pom.xml
@@ -26,7 +26,7 @@
spring-ai-mistral-ai-spring-boot-starter
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-mistral-ai-spring-boot-autoconfigure
${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-moonshot/pom.xml
index 70c34367bf0..01a3ef3cf62 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-moonshot/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-moonshot/pom.xml
@@ -26,7 +26,7 @@
spring-ai-moonshot-spring-boot-starter
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-moonshot-spring-boot-autoconfigure
${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-oci-genai/pom.xml
index f8a83528786..9bd7c52c661 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-oci-genai/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-oci-genai/pom.xml
@@ -43,7 +43,7 @@
org.springframework.ai
- spring-ai-spring-boot-autoconfigure
+ spring-ai-oci-genai-spring-boot-autoconfigure
${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-ollama/pom.xml
index 7b16c0c672d..d3f67d565ed 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-ollama/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-ollama/pom.xml
@@ -26,7 +26,7 @@
spring-ai-ollama-spring-boot-starter
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-ollama-spring-boot-autoconfigure
${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-openai/pom.xml
index a5bce988897..8a16fe673a6 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-openai/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-openai/pom.xml
@@ -26,7 +26,7 @@
spring-ai-openai-spring-boot-starter
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-ollama-spring-boot-autoconfigure
${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-postgresml-embedding/pom.xml
index ffbc4aa5543..26ef2e7e891 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-postgresml-embedding/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-postgresml-embedding/pom.xml
@@ -26,7 +26,7 @@
spring-ai-postgresml-spring-boot-starter
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-postgresml-spring-boot-autoconfigure
${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-qianfan/pom.xml
index 0da3b1776ae..25a08a0508b 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-qianfan/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-qianfan/pom.xml
@@ -26,7 +26,7 @@
spring-ai-qianfan-spring-boot-starter
jar
Spring AI Starter - QianFan
- Spring AI QianFan Auto Configuration
+ Spring AI QianFan Spring Boot Starter
https://github.com/spring-projects/spring-ai
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-stability-ai/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-stability-ai/pom.xml
index 64764552e2b..97146ac98df 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-stability-ai/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-stability-ai/pom.xml
@@ -26,7 +26,7 @@
spring-ai-stability-ai-spring-boot-starter
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-stability-ai-spring-boot-autoconfigure
${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-transformers/pom.xml
index 9b0e6164338..a57ca7c7047 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-transformers/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-transformers/pom.xml
@@ -26,7 +26,7 @@
spring-ai-transformers-spring-boot-starter
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-transformers-spring-boot-autoconfigure
${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-vertex-ai-embedding/pom.xml
index 36fcee4d1e9..3b3bb8b5cf3 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-vertex-ai-embedding/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-vertex-ai-embedding/pom.xml
@@ -26,7 +26,7 @@
spring-ai-vertex-ai-embedding-spring-boot-starter
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-vertex-ai-spring-boot-autoconfigure
${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-vertex-ai-gemini/pom.xml
index 778b9cccde3..6d90f89b776 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-vertex-ai-gemini/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-vertex-ai-gemini/pom.xml
@@ -26,7 +26,7 @@
spring-ai-vertex-ai-gemini-spring-boot-starter
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-vertex-ai-spring-boot-autoconfigure
${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-watsonx-ai/pom.xml
index 36e871422a4..44e7e8ac840 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-watsonx-ai/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-watsonx-ai/pom.xml
@@ -26,7 +26,7 @@
spring-ai-watsonx-ai-spring-boot-starter
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-watsonx-ai-spring-boot-autoconfigure
${project.parent.version}
@@ -55,4 +55,4 @@
-
\ No newline at end of file
+
diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-zhipuai/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-zhipuai/pom.xml
index 9eb4283c110..10554aff120 100644
--- a/spring-ai-spring-boot-starters/spring-ai-starter-zhipuai/pom.xml
+++ b/spring-ai-spring-boot-starters/spring-ai-starter-zhipuai/pom.xml
@@ -26,7 +26,7 @@
spring-ai-zhipuai-spring-boot-starter
jar
Spring AI Starter - ZhiPuAI
- Spring AI ZhiPuAI Auto Configuration
+ Spring AI ZhiPuAI Spring Boot Starter
https://github.com/spring-projects/spring-ai
@@ -44,7 +44,7 @@
org.springframework.ai
- spring-ai-spring-boot-autoconfigure
+ spring-ai-zhipuai-spring-boot-autoconfigure
${project.parent.version}
diff --git a/spring-ai-spring-boot-testcontainers/pom.xml b/spring-ai-spring-boot-testcontainers/pom.xml
index 11a8dda11af..7c192ff2588 100644
--- a/spring-ai-spring-boot-testcontainers/pom.xml
+++ b/spring-ai-spring-boot-testcontainers/pom.xml
@@ -42,6 +42,8 @@
+
+
org.springframework.ai
spring-ai-spring-boot-autoconfigure