diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/pom.xml b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/pom.xml index 61d3e56ee..35a65286e 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/pom.xml +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/pom.xml @@ -9,7 +9,7 @@ 1.0.0-SNAPSHOT ../../../pom.xml - spring-ai-mcp-client-spring-boot-autoconfigure + spring-ai-autoconfigure-mcp-client jar Spring AI MCP Client Auto Configuration Spring AI MCP Client Auto Configuration diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/pom.xml b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/pom.xml index a68c11af5..68fc8eaf9 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/pom.xml +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/pom.xml @@ -9,7 +9,7 @@ 1.0.0-SNAPSHOT ../../../pom.xml - spring-ai-mcp-server-spring-boot-autoconfigure + spring-ai-autoconfigure-mcp-server jar Spring AI MCP Server Auto Configuration Spring AI MCP Server Auto Configuration diff --git a/auto-configurations/chat/spring-ai-autoconfigure-model-chat-client/pom.xml b/auto-configurations/models/chat/client/spring-ai-autoconfigure-model-chat-client/pom.xml similarity index 97% rename from auto-configurations/chat/spring-ai-autoconfigure-model-chat-client/pom.xml rename to auto-configurations/models/chat/client/spring-ai-autoconfigure-model-chat-client/pom.xml index 699d59ab5..2277c3167 100644 --- a/auto-configurations/chat/spring-ai-autoconfigure-model-chat-client/pom.xml +++ b/auto-configurations/models/chat/client/spring-ai-autoconfigure-model-chat-client/pom.xml @@ -7,7 +7,7 @@ org.springframework.ai spring-ai 1.0.0-SNAPSHOT - ../../../pom.xml + ../../../../../pom.xml spring-ai-autoconfigure-model-chat-client jar diff --git a/auto-configurations/chat/spring-ai-autoconfigure-model-chat-client/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientAutoConfiguration.java b/auto-configurations/models/chat/client/spring-ai-autoconfigure-model-chat-client/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientAutoConfiguration.java similarity index 100% rename from auto-configurations/chat/spring-ai-autoconfigure-model-chat-client/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientAutoConfiguration.java rename to auto-configurations/models/chat/client/spring-ai-autoconfigure-model-chat-client/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientAutoConfiguration.java diff --git a/auto-configurations/chat/spring-ai-autoconfigure-model-chat-client/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientBuilderConfigurer.java b/auto-configurations/models/chat/client/spring-ai-autoconfigure-model-chat-client/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientBuilderConfigurer.java similarity index 100% rename from auto-configurations/chat/spring-ai-autoconfigure-model-chat-client/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientBuilderConfigurer.java rename to auto-configurations/models/chat/client/spring-ai-autoconfigure-model-chat-client/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientBuilderConfigurer.java diff --git a/auto-configurations/chat/spring-ai-autoconfigure-model-chat-client/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientBuilderProperties.java b/auto-configurations/models/chat/client/spring-ai-autoconfigure-model-chat-client/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientBuilderProperties.java similarity index 100% rename from auto-configurations/chat/spring-ai-autoconfigure-model-chat-client/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientBuilderProperties.java rename to auto-configurations/models/chat/client/spring-ai-autoconfigure-model-chat-client/src/main/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientBuilderProperties.java diff --git a/auto-configurations/chat/spring-ai-autoconfigure-model-chat-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/chat/client/spring-ai-autoconfigure-model-chat-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports similarity index 100% rename from auto-configurations/chat/spring-ai-autoconfigure-model-chat-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports rename to auto-configurations/models/chat/client/spring-ai-autoconfigure-model-chat-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports diff --git a/auto-configurations/chat/spring-ai-autoconfigure-model-chat-client/src/test/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientObservationAutoConfigurationTests.java b/auto-configurations/models/chat/client/spring-ai-autoconfigure-model-chat-client/src/test/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientObservationAutoConfigurationTests.java similarity index 100% rename from auto-configurations/chat/spring-ai-autoconfigure-model-chat-client/src/test/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientObservationAutoConfigurationTests.java rename to auto-configurations/models/chat/client/spring-ai-autoconfigure-model-chat-client/src/test/java/org/springframework/ai/model/chat/client/autoconfigure/ChatClientObservationAutoConfigurationTests.java diff --git a/auto-configurations/chat/spring-ai-autoconfigure-model-chat-memory/pom.xml b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory/pom.xml similarity index 98% rename from auto-configurations/chat/spring-ai-autoconfigure-model-chat-memory/pom.xml rename to auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory/pom.xml index 397554baa..31f7a6dbe 100644 --- a/auto-configurations/chat/spring-ai-autoconfigure-model-chat-memory/pom.xml +++ b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory/pom.xml @@ -7,7 +7,7 @@ org.springframework.ai spring-ai 1.0.0-SNAPSHOT - ../../../pom.xml + ../../../../../pom.xml spring-ai-autoconfigure-model-chat-memory jar diff --git a/auto-configurations/chat/spring-ai-autoconfigure-model-chat-memory/src/main/java/org/springframework/ai/model/chat/memory/autoconfigure/CommonChatMemoryProperties.java b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory/src/main/java/org/springframework/ai/model/chat/memory/autoconfigure/CommonChatMemoryProperties.java similarity index 100% rename from auto-configurations/chat/spring-ai-autoconfigure-model-chat-memory/src/main/java/org/springframework/ai/model/chat/memory/autoconfigure/CommonChatMemoryProperties.java rename to auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory/src/main/java/org/springframework/ai/model/chat/memory/autoconfigure/CommonChatMemoryProperties.java diff --git a/auto-configurations/chat/spring-ai-autoconfigure-model-chat-memory/src/main/java/org/springframework/ai/model/chat/memory/cassandra/autoconfigure/CassandraChatMemoryAutoConfiguration.java b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory/src/main/java/org/springframework/ai/model/chat/memory/cassandra/autoconfigure/CassandraChatMemoryAutoConfiguration.java similarity index 100% rename from auto-configurations/chat/spring-ai-autoconfigure-model-chat-memory/src/main/java/org/springframework/ai/model/chat/memory/cassandra/autoconfigure/CassandraChatMemoryAutoConfiguration.java rename to auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory/src/main/java/org/springframework/ai/model/chat/memory/cassandra/autoconfigure/CassandraChatMemoryAutoConfiguration.java diff --git a/auto-configurations/chat/spring-ai-autoconfigure-model-chat-memory/src/main/java/org/springframework/ai/model/chat/memory/cassandra/autoconfigure/CassandraChatMemoryProperties.java b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory/src/main/java/org/springframework/ai/model/chat/memory/cassandra/autoconfigure/CassandraChatMemoryProperties.java similarity index 100% rename from auto-configurations/chat/spring-ai-autoconfigure-model-chat-memory/src/main/java/org/springframework/ai/model/chat/memory/cassandra/autoconfigure/CassandraChatMemoryProperties.java rename to auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory/src/main/java/org/springframework/ai/model/chat/memory/cassandra/autoconfigure/CassandraChatMemoryProperties.java diff --git a/auto-configurations/chat/spring-ai-autoconfigure-model-chat-memory/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports similarity index 100% rename from auto-configurations/chat/spring-ai-autoconfigure-model-chat-memory/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports rename to auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports diff --git a/auto-configurations/chat/spring-ai-autoconfigure-model-chat-memory/src/test/java/org/springframework/ai/model/chat/memory/autoconfigure/CassandraChatMemoryPropertiesTest.java b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory/src/test/java/org/springframework/ai/model/chat/memory/autoconfigure/CassandraChatMemoryPropertiesTest.java similarity index 100% rename from auto-configurations/chat/spring-ai-autoconfigure-model-chat-memory/src/test/java/org/springframework/ai/model/chat/memory/autoconfigure/CassandraChatMemoryPropertiesTest.java rename to auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory/src/test/java/org/springframework/ai/model/chat/memory/autoconfigure/CassandraChatMemoryPropertiesTest.java diff --git a/auto-configurations/chat/spring-ai-autoconfigure-model-chat-memory/src/test/java/org/springframework/ai/model/chat/memory/cassandra/autoconfigure/CassandraChatMemoryAutoConfigurationIT.java b/auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory/src/test/java/org/springframework/ai/model/chat/memory/cassandra/autoconfigure/CassandraChatMemoryAutoConfigurationIT.java similarity index 100% rename from auto-configurations/chat/spring-ai-autoconfigure-model-chat-memory/src/test/java/org/springframework/ai/model/chat/memory/cassandra/autoconfigure/CassandraChatMemoryAutoConfigurationIT.java rename to auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory/src/test/java/org/springframework/ai/model/chat/memory/cassandra/autoconfigure/CassandraChatMemoryAutoConfigurationIT.java diff --git a/auto-configurations/models/chat/observation/spring-ai-autoconfigure-model-chat-observation/pom.xml b/auto-configurations/models/chat/observation/spring-ai-autoconfigure-model-chat-observation/pom.xml new file mode 100644 index 000000000..cef7c81bb --- /dev/null +++ b/auto-configurations/models/chat/observation/spring-ai-autoconfigure-model-chat-observation/pom.xml @@ -0,0 +1,72 @@ + + + 4.0.0 + + org.springframework.ai + spring-ai + 1.0.0-SNAPSHOT + ../../../../../pom.xml + + spring-ai-autoconfigure-model-chat-observation + 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/models/chat/observation/spring-ai-autoconfigure-model-chat-observation/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/ChatObservationAutoConfiguration.java b/auto-configurations/models/chat/observation/spring-ai-autoconfigure-model-chat-observation/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/ChatObservationAutoConfiguration.java new file mode 100644 index 000000000..81aee1992 --- /dev/null +++ b/auto-configurations/models/chat/observation/spring-ai-autoconfigure-model-chat-observation/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/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.model.chat.observation.autoconfigure; + +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/models/chat/observation/spring-ai-autoconfigure-model-chat-observation/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/ChatObservationProperties.java b/auto-configurations/models/chat/observation/spring-ai-autoconfigure-model-chat-observation/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/ChatObservationProperties.java new file mode 100644 index 000000000..be37d1b26 --- /dev/null +++ b/auto-configurations/models/chat/observation/spring-ai-autoconfigure-model-chat-observation/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/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.model.chat.observation.autoconfigure; + +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/models/chat/observation/spring-ai-autoconfigure-model-chat-observation/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/package-info.java b/auto-configurations/models/chat/observation/spring-ai-autoconfigure-model-chat-observation/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/package-info.java new file mode 100644 index 000000000..66bef736b --- /dev/null +++ b/auto-configurations/models/chat/observation/spring-ai-autoconfigure-model-chat-observation/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/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.model.chat.observation.autoconfigure; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/auto-configurations/models/chat/observation/spring-ai-autoconfigure-model-chat-observation/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/chat/observation/spring-ai-autoconfigure-model-chat-observation/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..37557ec19 --- /dev/null +++ b/auto-configurations/models/chat/observation/spring-ai-autoconfigure-model-chat-observation/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.model.chat.observation.autoconfigure.ChatObservationAutoConfiguration diff --git a/auto-configurations/models/chat/observation/spring-ai-autoconfigure-model-chat-observation/src/test/java/org/springframework/ai/model/chat/observation/autoconfigure/ChatObservationAutoConfigurationTests.java b/auto-configurations/models/chat/observation/spring-ai-autoconfigure-model-chat-observation/src/test/java/org/springframework/ai/model/chat/observation/autoconfigure/ChatObservationAutoConfigurationTests.java new file mode 100644 index 000000000..3ac3f12ca --- /dev/null +++ b/auto-configurations/models/chat/observation/spring-ai-autoconfigure-model-chat-observation/src/test/java/org/springframework/ai/model/chat/observation/autoconfigure/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.model.chat.observation.autoconfigure; + +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/models/embedding/observation/spring-ai-autoconfigure-model-embedding-observation/pom.xml b/auto-configurations/models/embedding/observation/spring-ai-autoconfigure-model-embedding-observation/pom.xml new file mode 100644 index 000000000..b41cdd752 --- /dev/null +++ b/auto-configurations/models/embedding/observation/spring-ai-autoconfigure-model-embedding-observation/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + org.springframework.ai + spring-ai + 1.0.0-SNAPSHOT + ../../../../../pom.xml + + spring-ai-autoconfigure-model-embedding-observation + 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/models/embedding/observation/spring-ai-autoconfigure-model-embedding-observation/src/main/java/org/springframework/ai/model/embedding/observation/autoconfigure/EmbeddingObservationAutoConfiguration.java b/auto-configurations/models/embedding/observation/spring-ai-autoconfigure-model-embedding-observation/src/main/java/org/springframework/ai/model/embedding/observation/autoconfigure/EmbeddingObservationAutoConfiguration.java new file mode 100644 index 000000000..ee0902343 --- /dev/null +++ b/auto-configurations/models/embedding/observation/spring-ai-autoconfigure-model-embedding-observation/src/main/java/org/springframework/ai/model/embedding/observation/autoconfigure/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.model.embedding.observation.autoconfigure; + +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/models/embedding/observation/spring-ai-autoconfigure-model-embedding-observation/src/main/java/org/springframework/ai/model/embedding/observation/autoconfigure/package-info.java b/auto-configurations/models/embedding/observation/spring-ai-autoconfigure-model-embedding-observation/src/main/java/org/springframework/ai/model/embedding/observation/autoconfigure/package-info.java new file mode 100644 index 000000000..654b59b6f --- /dev/null +++ b/auto-configurations/models/embedding/observation/spring-ai-autoconfigure-model-embedding-observation/src/main/java/org/springframework/ai/model/embedding/observation/autoconfigure/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.model.embedding.observation.autoconfigure; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/auto-configurations/models/embedding/observation/spring-ai-autoconfigure-model-embedding-observation/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/embedding/observation/spring-ai-autoconfigure-model-embedding-observation/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..2d3c503d8 --- /dev/null +++ b/auto-configurations/models/embedding/observation/spring-ai-autoconfigure-model-embedding-observation/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.model.embedding.observation.autoconfigure.EmbeddingObservationAutoConfiguration diff --git a/auto-configurations/models/embedding/observation/spring-ai-autoconfigure-model-embedding-observation/src/test/java/org/springframework/ai/model/embedding/observation/autoconfigure/EmbeddingObservationAutoConfigurationTests.java b/auto-configurations/models/embedding/observation/spring-ai-autoconfigure-model-embedding-observation/src/test/java/org/springframework/ai/model/embedding/observation/autoconfigure/EmbeddingObservationAutoConfigurationTests.java new file mode 100644 index 000000000..10c9feb7c --- /dev/null +++ b/auto-configurations/models/embedding/observation/spring-ai-autoconfigure-model-embedding-observation/src/test/java/org/springframework/ai/model/embedding/observation/autoconfigure/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.model.embedding.observation.autoconfigure; + +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/models/image/observation/spring-ai-autoconfigure-model-image-observation/pom.xml b/auto-configurations/models/image/observation/spring-ai-autoconfigure-model-image-observation/pom.xml new file mode 100644 index 000000000..428a5953f --- /dev/null +++ b/auto-configurations/models/image/observation/spring-ai-autoconfigure-model-image-observation/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + org.springframework.ai + spring-ai + 1.0.0-SNAPSHOT + ../../../../../pom.xml + + spring-ai-autoconfigure-model-image-observation + 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/models/image/observation/spring-ai-autoconfigure-model-image-observation/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/ImageObservationAutoConfiguration.java b/auto-configurations/models/image/observation/spring-ai-autoconfigure-model-image-observation/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/ImageObservationAutoConfiguration.java new file mode 100644 index 000000000..f97e12f03 --- /dev/null +++ b/auto-configurations/models/image/observation/spring-ai-autoconfigure-model-image-observation/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/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.model.image.observation.autoconfigure; + +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/models/image/observation/spring-ai-autoconfigure-model-image-observation/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/ImageObservationProperties.java b/auto-configurations/models/image/observation/spring-ai-autoconfigure-model-image-observation/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/ImageObservationProperties.java new file mode 100644 index 000000000..7d372d986 --- /dev/null +++ b/auto-configurations/models/image/observation/spring-ai-autoconfigure-model-image-observation/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/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.model.image.observation.autoconfigure; + +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/models/image/observation/spring-ai-autoconfigure-model-image-observation/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/package-info.java b/auto-configurations/models/image/observation/spring-ai-autoconfigure-model-image-observation/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/package-info.java new file mode 100644 index 000000000..983cc5a16 --- /dev/null +++ b/auto-configurations/models/image/observation/spring-ai-autoconfigure-model-image-observation/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/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.model.image.observation.autoconfigure; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/auto-configurations/models/image/observation/spring-ai-autoconfigure-model-image-observation/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/image/observation/spring-ai-autoconfigure-model-image-observation/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..788c24087 --- /dev/null +++ b/auto-configurations/models/image/observation/spring-ai-autoconfigure-model-image-observation/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.model.image.observation.autoconfigure.ImageObservationAutoConfiguration diff --git a/auto-configurations/models/image/observation/spring-ai-autoconfigure-model-image-observation/src/test/java/org/springframework/ai/model/image/observation/autoconfigure/ImageObservationAutoConfigurationTests.java b/auto-configurations/models/image/observation/spring-ai-autoconfigure-model-image-observation/src/test/java/org/springframework/ai/model/image/observation/autoconfigure/ImageObservationAutoConfigurationTests.java new file mode 100644 index 000000000..23417b67b --- /dev/null +++ b/auto-configurations/models/image/observation/spring-ai-autoconfigure-model-image-observation/src/test/java/org/springframework/ai/model/image/observation/autoconfigure/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.model.image.observation.autoconfigure; + +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/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/pom.xml index 50a6f2359..890375ca2 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/pom.xml @@ -35,13 +35,6 @@ - - org.springframework.ai - spring-ai-autoconfigure-model-chat-client - ${project.parent.version} - true - - org.springframework.ai spring-ai-autoconfigure-model-tool @@ -58,21 +51,7 @@ org.springframework.ai - spring-ai-autoconfigure-observation-chat - ${project.parent.version} - true - - - - org.springframework.ai - spring-ai-autoconfigure-observation-embedding - ${project.parent.version} - true - - - - org.springframework.ai - spring-ai-autoconfigure-observation-image + spring-ai-autoconfigure-model-chat-observation ${project.parent.version} true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicAutoConfiguration.java index 4c422e37a..eb064bec9 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/main/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicAutoConfiguration.java @@ -20,6 +20,8 @@ import io.micrometer.observation.ObservationRegistry; import org.springframework.ai.anthropic.AnthropicChatModel; import org.springframework.ai.anthropic.api.AnthropicApi; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration; import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.chat.observation.ChatModelObservationConvention; @@ -47,14 +49,13 @@ import org.springframework.web.reactive.function.client.WebClient; * * @author Christian Tzolov * @author Thomas Vitale + * @author Ilayaperumal Gopinathan * @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 { @@ -73,6 +74,8 @@ public class AnthropicAutoConfiguration { @Bean @ConditionalOnMissingBean + @ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.ANTHROPIC, + matchIfMissing = true) public AnthropicChatModel anthropicChatModel(AnthropicApi anthropicApi, AnthropicChatProperties chatProperties, RetryTemplate retryTemplate, ToolCallingManager toolCallingManager, ObjectProvider observationRegistry, diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicModelConfigurationTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicModelConfigurationTests.java new file mode 100644 index 000000000..387556f6f --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicModelConfigurationTests.java @@ -0,0 +1,55 @@ +/* + * 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. + */ + +package org.springframework.ai.model.anthropic.autoconfigure; + +import org.junit.jupiter.api.Test; + +import org.springframework.ai.anthropic.AnthropicChatModel; +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 AnthropicAutoConfiguration}'s conditional enabling of models. + * + * @author Ilayaperumal Gopinathan + */ +public class AnthropicModelConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("spring.ai.anthropic.apiKey=" + System.getenv("ANTHROPIC_API_KEY")) + .withConfiguration(AutoConfigurations.of(AnthropicAutoConfiguration.class)); + + @Test + void chatModelActivation() { + this.contextRunner.run(context -> { + assertThat(context.getBeansOfType(AnthropicChatModel.class)).isNotEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.chat=none").run(context -> { + assertThat(context.getBeansOfType(AnthropicChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AnthropicChatModel.class)).isEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.chat=anthropic").run(context -> { + assertThat(context.getBeansOfType(AnthropicChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AnthropicChatModel.class)).isNotEmpty(); + }); + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicPropertiesTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicPropertiesTests.java index ceb714227..186437c48 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicPropertiesTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/AnthropicPropertiesTests.java @@ -120,11 +120,10 @@ public class AnthropicPropertiesTests { }); // Explicitly disable the chat auto-configuration. - new ApplicationContextRunner().withPropertyValues("spring.ai.anthropic.chat.enabled=false") + new ApplicationContextRunner().withPropertyValues("spring.ai.model.chat=none") .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-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java index 2af2da9c0..740cf066a 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-anthropic/src/test/java/org/springframework/ai/model/anthropic/autoconfigure/tool/FunctionCallWithFunctionBeanIT.java @@ -34,6 +34,7 @@ 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.model.tool.ToolCallingChatOptions; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -96,7 +97,7 @@ class FunctionCallWithFunctionBeanIT { "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())); + ToolCallingChatOptions.builder().toolNames("weatherFunction").build())); logger.info("Response: {}", response); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/pom.xml index f6bb5feb7..595864404 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/pom.xml @@ -35,13 +35,6 @@ - - org.springframework.ai - spring-ai-autoconfigure-model-chat-client - ${project.parent.version} - true - - org.springframework.ai spring-ai-autoconfigure-model-tool @@ -58,21 +51,21 @@ org.springframework.ai - spring-ai-autoconfigure-observation-chat + spring-ai-autoconfigure-model-chat-observation ${project.parent.version} true org.springframework.ai - spring-ai-autoconfigure-observation-embedding + spring-ai-autoconfigure-model-embedding-observation ${project.parent.version} true org.springframework.ai - spring-ai-autoconfigure-observation-image + spring-ai-autoconfigure-model-image-observation ${project.parent.version} true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfiguration.java index f01962f43..c442995b1 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/main/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiAutoConfiguration.java @@ -28,6 +28,8 @@ import com.azure.core.util.ClientOptions; import com.azure.core.util.Header; import io.micrometer.observation.ObservationRegistry; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration; import org.springframework.ai.azure.openai.AzureOpenAiAudioTranscriptionModel; import org.springframework.ai.azure.openai.AzureOpenAiChatModel; @@ -122,7 +124,7 @@ public class AzureOpenAiAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = AzureOpenAiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + @ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.AZURE_OPENAI, matchIfMissing = true) public AzureOpenAiChatModel azureOpenAiChatModel(OpenAIClientBuilder openAIClientBuilder, AzureOpenAiChatProperties chatProperties, ToolCallingManager toolCallingManager, @@ -142,8 +144,8 @@ public class AzureOpenAiAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = AzureOpenAiEmbeddingProperties.CONFIG_PREFIX, name = "enabled", - havingValue = "true", matchIfMissing = true) + @ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.AZURE_OPENAI, + matchIfMissing = true) public AzureOpenAiEmbeddingModel azureOpenAiEmbeddingModel(OpenAIClientBuilder openAIClient, AzureOpenAiEmbeddingProperties embeddingProperties, ObjectProvider observationRegistry, ObjectProvider observationConvention) { @@ -160,17 +162,9 @@ public class AzureOpenAiAutoConfiguration { @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, + @ConditionalOnProperty(name = SpringAIModelProperties.IMAGE_MODEL, havingValue = SpringAIModels.AZURE_OPENAI, + matchIfMissing = true) + public AzureOpenAiImageModel azureOpenAiImageModel(OpenAIClientBuilder openAIClientBuilder, AzureOpenAiImageOptionsProperties imageProperties) { return new AzureOpenAiImageModel(openAIClientBuilder.buildClient(), imageProperties.getOptions()); @@ -178,8 +172,8 @@ public class AzureOpenAiAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = AzureOpenAiAudioTranscriptionProperties.CONFIG_PREFIX, name = "enabled", - havingValue = "true", matchIfMissing = true) + @ConditionalOnProperty(name = SpringAIModelProperties.AUDIO_TRANSCRIPTION_MODEL, + havingValue = SpringAIModels.AZURE_OPENAI, matchIfMissing = true) public AzureOpenAiAudioTranscriptionModel azureOpenAiAudioTranscriptionModel(OpenAIClientBuilder openAIClient, AzureOpenAiAudioTranscriptionProperties audioProperties) { return new AzureOpenAiAudioTranscriptionModel(openAIClient.buildClient(), audioProperties.getOptions()); @@ -190,4 +184,12 @@ public class AzureOpenAiAutoConfiguration { customizers.orderedStream().forEach(customizer -> customizer.customize(clientBuilder)); } + @Bean + @ConditionalOnMissingBean + public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) { + DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver(); + manager.setApplicationContext(context); + return manager; + } + } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiModelConfigurationTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiModelConfigurationTests.java new file mode 100644 index 000000000..19a2ce674 --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/java/org/springframework/ai/model/azure/openai/autoconfigure/AzureOpenAiModelConfigurationTests.java @@ -0,0 +1,168 @@ +/* + * 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. + */ + +package org.springframework.ai.model.azure.openai.autoconfigure; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; + +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.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit Tests for {@link AzureOpenAiAutoConfiguration}'s conditional enabling of models. + * + * @author Ilayaperumal Gopinathan + */ +@EnabledIfEnvironmentVariable(named = "OPENAI_API_KEY", matches = ".*") +public class AzureOpenAiModelConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("spring.ai.azure.openai.openai-api-key=" + System.getenv("OPENAI_API_KEY"), + "spring.ai.openai.base-url=TEST_BASE_URL") + .withConfiguration(AutoConfigurations.of(AzureOpenAiAutoConfiguration.class)); + + @Test + void chatModelActivation() { + this.contextRunner.run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.chat=none").run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.chat=azure-openai").run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty(); + }); + + this.contextRunner + .withPropertyValues("spring.ai.model.chat=azure-openai", "spring.ai.model.embedding=none", + "spring.ai.model.image=none", "spring.ai.model.audio.speech=none", + "spring.ai.model.audio.transcription=none", "spring.ai.model.moderation=none") + .run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isEmpty(); + }); + } + + @Test + void embeddingModelActivation() { + this.contextRunner.run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.embedding=none").run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.embedding=azure-openai").run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty(); + }); + + this.contextRunner + .withPropertyValues("spring.ai.model.chat=none", "spring.ai.model.embedding=azure-openai", + "spring.ai.model.image=none", "spring.ai.model.audio.speech=none", + "spring.ai.model.audio.transcription=none", "spring.ai.model.moderation=none") + .run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isEmpty(); + }); + } + + @Test + void imageModelActivation() { + this.contextRunner.run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.image=none").run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiImageOptionsProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.image=azure-openai").run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiImageOptionsProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isNotEmpty(); + }); + + this.contextRunner + .withPropertyValues("spring.ai.model.chat=none", "spring.ai.model.embedding=none", + "spring.ai.model.image=azure-openai", "spring.ai.model.audio.speech=none", + "spring.ai.model.audio.transcription=none", "spring.ai.model.moderation=none") + .run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isEmpty(); + }); + } + + @Test + void audioTranscriptionModelActivation() { + this.contextRunner.run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.audio.transcription=none").run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.audio.transcription=azure-openai").run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty(); + }); + + this.contextRunner + .withPropertyValues("spring.ai.model.chat=none", "spring.ai.model.embedding=none", + "spring.ai.model.image=none", "spring.ai.model.audio.speech=none", + "spring.ai.model.audio.transcription=azure-openai", "spring.ai.model.moderation=none") + .run(context -> { + assertThat(context.getBeansOfType(AzureOpenAiChatModel.class)).isEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiEmbeddingModel.class)).isEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiImageModel.class)).isEmpty(); + assertThat(context.getBeansOfType(AzureOpenAiAudioTranscriptionModel.class)).isNotEmpty(); + }); + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/resources/speech/jfk.flac b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/resources/speech/jfk.flac new file mode 100644 index 000000000..e44b7c138 Binary files /dev/null and b/auto-configurations/models/spring-ai-autoconfigure-model-azure-openai/src/test/resources/speech/jfk.flac differ diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/pom.xml index e90e926d6..7c0112362 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/pom.xml @@ -42,13 +42,6 @@ - - org.springframework.ai - spring-ai-autoconfigure-model-chat-client - ${project.parent.version} - true - - org.springframework.ai spring-ai-autoconfigure-model-tool @@ -65,21 +58,21 @@ org.springframework.ai - spring-ai-autoconfigure-observation-chat + spring-ai-autoconfigure-model-chat-observation ${project.parent.version} true org.springframework.ai - spring-ai-autoconfigure-observation-embedding + spring-ai-autoconfigure-model-embedding-observation ${project.parent.version} true org.springframework.ai - spring-ai-autoconfigure-observation-image + spring-ai-autoconfigure-model-image-observation ${project.parent.version} true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingAutoConfiguration.java index ce60731c0..57765ad55 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingAutoConfiguration.java @@ -20,6 +20,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.regions.providers.AwsRegionProvider; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; import org.springframework.ai.model.bedrock.autoconfigure.BedrockAwsConnectionConfiguration; import org.springframework.ai.model.bedrock.autoconfigure.BedrockAwsConnectionProperties; import org.springframework.ai.bedrock.cohere.BedrockCohereEmbeddingModel; @@ -43,7 +45,8 @@ import org.springframework.context.annotation.Import; @AutoConfiguration @ConditionalOnClass(CohereEmbeddingBedrockApi.class) @EnableConfigurationProperties({ BedrockCohereEmbeddingProperties.class, BedrockAwsConnectionProperties.class }) -@ConditionalOnProperty(prefix = BedrockCohereEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true") +@ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.BEDROCK_COHERE, + matchIfMissing = true) @Import(BedrockAwsConnectionConfiguration.class) public class BedrockCohereEmbeddingAutoConfiguration { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatAutoConfiguration.java index 0a9591ed2..e063bc3af 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatAutoConfiguration.java @@ -22,6 +22,8 @@ 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.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; import org.springframework.ai.model.bedrock.autoconfigure.BedrockAwsConnectionConfiguration; import org.springframework.ai.model.bedrock.autoconfigure.BedrockAwsConnectionProperties; import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration; @@ -53,8 +55,8 @@ import org.springframework.context.annotation.Import; @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) +@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.BEDROCK_CONVERSE, + matchIfMissing = true) @Import(BedrockAwsConnectionConfiguration.class) @ImportAutoConfiguration({ ToolCallingAutoConfiguration.class }) public class BedrockConverseProxyChatAutoConfiguration { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfiguration.java index 1cfea4c07..474ec9030 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/main/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfiguration.java @@ -20,6 +20,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.regions.providers.AwsRegionProvider; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; import org.springframework.ai.model.bedrock.autoconfigure.BedrockAwsConnectionConfiguration; import org.springframework.ai.model.bedrock.autoconfigure.BedrockAwsConnectionProperties; import org.springframework.ai.bedrock.titan.BedrockTitanEmbeddingModel; @@ -43,7 +45,8 @@ import org.springframework.context.annotation.Import; @AutoConfiguration @ConditionalOnClass(TitanEmbeddingBedrockApi.class) @EnableConfigurationProperties({ BedrockTitanEmbeddingProperties.class, BedrockAwsConnectionProperties.class }) -@ConditionalOnProperty(prefix = BedrockTitanEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true") +@ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.BEDROCK_TITAN, + matchIfMissing = true) @Import(BedrockAwsConnectionConfiguration.class) public class BedrockTitanEmbeddingAutoConfiguration { @@ -62,7 +65,6 @@ public class BedrockTitanEmbeddingAutoConfiguration { @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-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingAutoConfigurationIT.java index f612425cf..4e6f0c4eb 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereEmbeddingAutoConfigurationIT.java @@ -43,7 +43,7 @@ import static org.assertj.core.api.Assertions.assertThat; public class BedrockCohereEmbeddingAutoConfigurationIT { private final ApplicationContextRunner contextRunner = BedrockTestUtils.getContextRunner() - .withPropertyValues("spring.ai.bedrock.cohere.embedding.enabled=true", + .withPropertyValues("spring.ai.model.embedding=bedrock-cohere", "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") @@ -109,19 +109,18 @@ public class BedrockCohereEmbeddingAutoConfigurationIT { } @Test - public void embeddingDisabled() { + public void embeddingActivation() { - // 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(); + assertThat(context.getBeansOfType(BedrockCohereEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(BedrockCohereEmbeddingModel.class)).isNotEmpty(); }); // Explicitly enable the embedding auto-configuration. BedrockTestUtils.getContextRunnerWithUserConfiguration() - .withPropertyValues("spring.ai.bedrock.cohere.embedding.enabled=true") + .withPropertyValues("spring.ai.model.embedding=bedrock-cohere") .withConfiguration(AutoConfigurations.of(BedrockCohereEmbeddingAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(BedrockCohereEmbeddingProperties.class)).isNotEmpty(); @@ -130,7 +129,7 @@ public class BedrockCohereEmbeddingAutoConfigurationIT { // Explicitly disable the embedding auto-configuration. BedrockTestUtils.getContextRunnerWithUserConfiguration() - .withPropertyValues("spring.ai.bedrock.cohere.embedding.enabled=false") + .withPropertyValues("spring.ai.model.embedding=none") .withConfiguration(AutoConfigurations.of(BedrockCohereEmbeddingAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(BedrockCohereEmbeddingProperties.class)).isEmpty(); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereModelConfigurationTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereModelConfigurationTests.java new file mode 100644 index 000000000..019261105 --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/cohere/BedrockCohereModelConfigurationTests.java @@ -0,0 +1,60 @@ +/* + * 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. + */ + +package org.springframework.ai.model.bedrock.autoconfigure.cohere; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import org.springframework.ai.bedrock.cohere.BedrockCohereEmbeddingModel; +import org.springframework.ai.bedrock.titan.BedrockTitanEmbeddingModel; +import org.springframework.ai.model.bedrock.autoconfigure.titan.BedrockTitanEmbeddingAutoConfiguration; +import org.springframework.ai.model.bedrock.autoconfigure.titan.BedrockTitanEmbeddingProperties; +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 BedrockCohereEmbeddingAutoConfiguration}'s conditional enabling + * of models. + * + * @author Ilayaperumal Gopinathan + */ +public class BedrockCohereModelConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(BedrockCohereEmbeddingAutoConfiguration.class)) + .withBean(ObjectMapper.class, ObjectMapper::new); + + @Test + void embeddingModelActivation() { + this.contextRunner.run(context -> { + assertThat(context.getBeansOfType(BedrockCohereEmbeddingModel.class)).isNotEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.embedding=none").run(context -> { + assertThat(context.getBeansOfType(BedrockCohereEmbeddingProperties.class)).isEmpty(); + assertThat(context.getBeansOfType(BedrockCohereEmbeddingModel.class)).isEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.embedding=bedrock-cohere").run(context -> { + assertThat(context.getBeansOfType(BedrockCohereEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(BedrockCohereEmbeddingModel.class)).isNotEmpty(); + }); + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseModelConfigurationTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseModelConfigurationTests.java new file mode 100644 index 000000000..0192e85d9 --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseModelConfigurationTests.java @@ -0,0 +1,56 @@ +/* + * 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. + */ + +package org.springframework.ai.model.bedrock.autoconfigure.converse; + +import org.junit.jupiter.api.Test; + +import org.springframework.ai.bedrock.converse.BedrockProxyChatModel; +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 org.springframework.ai.model.bedrock.autoconfigure.converse.BedrockConverseProxyChatAutoConfiguration}'s + * conditional enabling of models. + * + * @author Ilayaperumal Gopinathan + */ +public class BedrockConverseModelConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(BedrockConverseProxyChatAutoConfiguration.class)); + + @Test + void chatModelActivation() { + this.contextRunner.run(context -> { + assertThat(context.getBeansOfType(BedrockProxyChatModel.class)).isNotEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.chat=none").run(context -> { + assertThat(context.getBeansOfType(BedrockConverseProxyChatProperties.class)).isEmpty(); + assertThat(context.getBeansOfType(BedrockProxyChatModel.class)).isEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.chat=bedrock-converse").run(context -> { + assertThat(context.getBeansOfType(BedrockConverseProxyChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(BedrockProxyChatModel.class)).isNotEmpty(); + }); + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatPropertiesTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatPropertiesTests.java index b71900197..65913a7f0 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatPropertiesTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/converse/BedrockConverseProxyChatPropertiesTests.java @@ -18,6 +18,7 @@ package org.springframework.ai.model.bedrock.autoconfigure.converse; import org.junit.jupiter.api.Test; +import org.springframework.ai.bedrock.converse.BedrockProxyChatModel; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -71,14 +72,20 @@ public class BedrockConverseProxyChatPropertiesTests { .run(context -> assertThat(context.getBeansOfType(BedrockConverseProxyChatProperties.class)).isNotEmpty()); // Explicitly enable the chat auto-configuration. - new ApplicationContextRunner().withPropertyValues("spring.ai.bedrock.converse.chat.enabled=true") + new ApplicationContextRunner().withPropertyValues("spring.ai.model.chat=bedrock-converse") .withConfiguration(AutoConfigurations.of(BedrockConverseProxyChatAutoConfiguration.class)) - .run(context -> assertThat(context.getBeansOfType(BedrockConverseProxyChatProperties.class)).isNotEmpty()); + .run(context -> { + assertThat(context.getBeansOfType(BedrockConverseProxyChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(BedrockProxyChatModel.class)).isNotEmpty(); + }); // Explicitly disable the chat auto-configuration. - new ApplicationContextRunner().withPropertyValues("spring.ai.bedrock.converse.chat.enabled=false") + new ApplicationContextRunner().withPropertyValues("spring.ai.model.chat=none") .withConfiguration(AutoConfigurations.of(BedrockConverseProxyChatAutoConfiguration.class)) - .run(context -> assertThat(context.getBeansOfType(BedrockConverseProxyChatProperties.class)).isEmpty()); + .run(context -> { + assertThat(context.getBeansOfType(BedrockConverseProxyChatProperties.class)).isEmpty(); + assertThat(context.getBeansOfType(BedrockProxyChatModel.class)).isEmpty(); + }); } } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfigurationIT.java index 107bb2a6d..8764167dc 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanEmbeddingAutoConfigurationIT.java @@ -108,19 +108,18 @@ public class BedrockTitanEmbeddingAutoConfigurationIT { } @Test - public void embeddingDisabled() { + public void embeddingActivation() { - // 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(); + assertThat(context.getBeansOfType(BedrockTitanEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(BedrockTitanEmbeddingModel.class)).isNotEmpty(); }); // Explicitly enable the embedding auto-configuration. BedrockTestUtils.getContextRunnerWithUserConfiguration() - .withPropertyValues("spring.ai.bedrock.titan.embedding.enabled=true") + .withPropertyValues("spring.ai.model.embedding=bedrock-titan") .withConfiguration(AutoConfigurations.of(BedrockTitanEmbeddingAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(BedrockTitanEmbeddingProperties.class)).isNotEmpty(); @@ -129,7 +128,7 @@ public class BedrockTitanEmbeddingAutoConfigurationIT { // Explicitly disable the embedding auto-configuration. BedrockTestUtils.getContextRunnerWithUserConfiguration() - .withPropertyValues("spring.ai.bedrock.titan.embedding.enabled=false") + .withPropertyValues("spring.ai.model.embedding=none") .withConfiguration(AutoConfigurations.of(BedrockTitanEmbeddingAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(BedrockTitanEmbeddingProperties.class)).isEmpty(); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanModelConfigurationTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanModelConfigurationTests.java new file mode 100644 index 000000000..83d194a9e --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/java/org/springframework/ai/model/bedrock/autoconfigure/titan/BedrockTitanModelConfigurationTests.java @@ -0,0 +1,60 @@ +/* + * 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. + */ + +package org.springframework.ai.model.bedrock.autoconfigure.titan; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import org.springframework.ai.bedrock.converse.BedrockProxyChatModel; +import org.springframework.ai.bedrock.titan.BedrockTitanEmbeddingModel; +import org.springframework.ai.model.bedrock.autoconfigure.converse.BedrockConverseProxyChatAutoConfiguration; +import org.springframework.ai.model.bedrock.autoconfigure.converse.BedrockConverseProxyChatProperties; +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 BedrockTitanEmbeddingAutoConfiguration}'s conditional enabling of + * models. + * + * @author Ilayaperumal Gopinathan + */ +public class BedrockTitanModelConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(BedrockTitanEmbeddingAutoConfiguration.class)) + .withBean(ObjectMapper.class, ObjectMapper::new); + + @Test + void embeddingModelActivation() { + this.contextRunner.run(context -> { + assertThat(context.getBeansOfType(BedrockTitanEmbeddingModel.class)).isNotEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.embedding=none").run(context -> { + assertThat(context.getBeansOfType(BedrockTitanEmbeddingProperties.class)).isEmpty(); + assertThat(context.getBeansOfType(BedrockTitanEmbeddingModel.class)).isEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.embedding=bedrock-titan").run(context -> { + assertThat(context.getBeansOfType(BedrockTitanEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(BedrockTitanEmbeddingModel.class)).isNotEmpty(); + }); + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/resources/spring_framework.png b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/resources/spring_framework.png new file mode 100644 index 000000000..735057a6b Binary files /dev/null and b/auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai/src/test/resources/spring_framework.png differ diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-huggingface/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-huggingface/pom.xml index 9f460fb71..09da01d8f 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-huggingface/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-huggingface/pom.xml @@ -37,14 +37,7 @@ org.springframework.ai - spring-ai-autoconfigure-model-chat-client - ${project.parent.version} - true - - - - org.springframework.ai - spring-ai-autoconfigure-observation-chat + spring-ai-autoconfigure-model-chat-observation ${project.parent.version} true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-huggingface/src/main/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-huggingface/src/main/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatAutoConfiguration.java index a79a6517d..9b937deab 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-huggingface/src/main/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-huggingface/src/main/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceChatAutoConfiguration.java @@ -17,6 +17,8 @@ package org.springframework.ai.model.huggingface.autoconfigure; import org.springframework.ai.huggingface.HuggingfaceChatModel; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -31,7 +33,7 @@ public class HuggingfaceChatAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = HuggingfaceChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + @ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.HUGGINGFACE, matchIfMissing = true) public HuggingfaceChatModel huggingfaceChatModel(HuggingfaceChatProperties huggingfaceChatProperties) { return new HuggingfaceChatModel(huggingfaceChatProperties.getApiKey(), huggingfaceChatProperties.getUrl()); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-huggingface/src/test/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceModelConfigurationTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-huggingface/src/test/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceModelConfigurationTests.java new file mode 100644 index 000000000..835036219 --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-huggingface/src/test/java/org/springframework/ai/model/huggingface/autoconfigure/HuggingfaceModelConfigurationTests.java @@ -0,0 +1,55 @@ +/* + * 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. + */ + +package org.springframework.ai.model.huggingface.autoconfigure; + +import org.junit.jupiter.api.Test; + +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; + +/** + * Unit Tests for {@link HuggingfaceChatAutoConfiguration}'s conditional enabling of + * models. + * + * @author Ilayaperumal Gopinathan + */ +public class HuggingfaceModelConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(HuggingfaceChatAutoConfiguration.class)); + + @Test + void chatModelActivation() { + this.contextRunner.run(context -> { + assertThat(context.getBeansOfType(HuggingfaceChatModel.class)).isNotEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.chat=none").run(context -> { + assertThat(context.getBeansOfType(HuggingfaceChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(HuggingfaceChatModel.class)).isEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.chat=huggingface").run(context -> { + assertThat(context.getBeansOfType(HuggingfaceChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(HuggingfaceChatModel.class)).isNotEmpty(); + }); + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/pom.xml index 4bd9d8ea7..0ead4e2ab 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/pom.xml @@ -35,13 +35,6 @@ - - org.springframework.ai - spring-ai-autoconfigure-model-chat-client - ${project.parent.version} - true - - org.springframework.ai spring-ai-autoconfigure-model-tool @@ -58,14 +51,14 @@ org.springframework.ai - spring-ai-autoconfigure-observation-chat + spring-ai-autoconfigure-model-chat-observation ${project.parent.version} true org.springframework.ai - spring-ai-autoconfigure-observation-embedding + spring-ai-autoconfigure-model-embedding-observation ${project.parent.version} true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfiguration.java index caea39372..f74e3ba21 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/main/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxAutoConfiguration.java @@ -20,6 +20,8 @@ import java.util.List; import io.micrometer.observation.ObservationRegistry; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.chat.observation.ChatModelObservationConvention; import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; @@ -57,7 +59,7 @@ public class MiniMaxAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = MiniMaxChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + @ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.MINIMAX, matchIfMissing = true) public MiniMaxChatModel miniMaxChatModel(MiniMaxConnectionProperties commonProperties, MiniMaxChatProperties chatProperties, ObjectProvider restClientBuilderProvider, @@ -79,7 +81,7 @@ public class MiniMaxAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = MiniMaxEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + @ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.MINIMAX, matchIfMissing = true) public MiniMaxEmbeddingModel miniMaxEmbeddingModel(MiniMaxConnectionProperties commonProperties, MiniMaxEmbeddingProperties embeddingProperties, diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxPropertiesTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxPropertiesTests.java index b0a75f743..962b2e114 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxPropertiesTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MiniMaxPropertiesTests.java @@ -265,7 +265,7 @@ public class MiniMaxPropertiesTests { new ApplicationContextRunner() .withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL", - "spring.ai.minimax.embedding.enabled=false") + "spring.ai.model.embedding=none") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) .run(context -> { @@ -297,7 +297,7 @@ public class MiniMaxPropertiesTests { 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") + "spring.ai.model.chat=none") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) .run(context -> { @@ -316,7 +316,7 @@ public class MiniMaxPropertiesTests { new ApplicationContextRunner() .withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL", - "spring.ai.minimax.chat.enabled=true") + "spring.ai.model.chat=minimax") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, MiniMaxAutoConfiguration.class)) .run(context -> { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MinimaxModelConfigurationTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MinimaxModelConfigurationTests.java new file mode 100644 index 000000000..d1cdd2709 --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-minimax/src/test/java/org/springframework/ai/model/minimax/autoconfigure/MinimaxModelConfigurationTests.java @@ -0,0 +1,83 @@ +/* + * 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. + */ + +package org.springframework.ai.model.minimax.autoconfigure; + +import org.junit.jupiter.api.Test; + +import org.springframework.ai.minimax.MiniMaxChatModel; +import org.springframework.ai.minimax.MiniMaxEmbeddingModel; +import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; +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 MiniMaxAutoConfiguration}'s conditional enabling of models. + * + * @author Ilayaperumal Gopinathan + */ +public class MinimaxModelConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(MiniMaxAutoConfiguration.class, SpringAiRetryAutoConfiguration.class)) + .withPropertyValues("spring.ai.minimax.api-key=API_KEY", "spring.ai.minimax.base-url=TEST_BASE_URL"); + + @Test + void chatModelActivation() { + this.contextRunner.run(context -> { + assertThat(context.getBeansOfType(MiniMaxChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MiniMaxChatModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isNotEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.chat=none", "spring.ai.model.embedding=none") + .run(context -> { + assertThat(context.getBeansOfType(MiniMaxChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MiniMaxChatModel.class)).isEmpty(); + assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.chat=minimax", "spring.ai.model.embedding=none") + .run(context -> { + assertThat(context.getBeansOfType(MiniMaxChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MiniMaxChatModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isEmpty(); + }); + } + + @Test + void embeddingModelActivation() { + this.contextRunner.run(context -> { + assertThat(context.getBeansOfType(MiniMaxChatModel.class)).isNotEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.embedding=none").run(context -> { + assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.embedding=minimax").run(context -> { + assertThat(context.getBeansOfType(MiniMaxEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MiniMaxEmbeddingModel.class)).isNotEmpty(); + }); + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/pom.xml index 0574907d5..e75110f3f 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/pom.xml @@ -35,13 +35,6 @@ - - org.springframework.ai - spring-ai-autoconfigure-model-chat-client - ${project.parent.version} - true - - org.springframework.ai spring-ai-autoconfigure-model-tool @@ -58,21 +51,21 @@ org.springframework.ai - spring-ai-autoconfigure-observation-chat + spring-ai-autoconfigure-model-chat-observation ${project.parent.version} true org.springframework.ai - spring-ai-autoconfigure-observation-embedding + spring-ai-autoconfigure-model-embedding-observation ${project.parent.version} true org.springframework.ai - spring-ai-autoconfigure-observation-image + spring-ai-autoconfigure-model-image-observation ${project.parent.version} true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfiguration.java index b58a75200..77bab0242 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiAutoConfiguration.java @@ -18,6 +18,8 @@ package org.springframework.ai.model.mistralai.autoconfigure; import io.micrometer.observation.ObservationRegistry; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration; import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.chat.observation.ChatModelObservationConvention; @@ -63,7 +65,7 @@ public class MistralAiAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = MistralAiEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + @ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.MISTRAL, matchIfMissing = true) public MistralAiEmbeddingModel mistralAiEmbeddingModel(MistralAiCommonProperties commonProperties, MistralAiEmbeddingProperties embeddingProperties, @@ -86,7 +88,7 @@ public class MistralAiAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = MistralAiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + @ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.MISTRAL, matchIfMissing = true) public MistralAiChatModel mistralAiChatModel(MistralAiCommonProperties commonProperties, MistralAiChatProperties chatProperties, ObjectProvider restClientBuilderProvider, diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiChatProperties.java b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiChatProperties.java index 4d307b5a8..19a782a8e 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiChatProperties.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/main/java/org/springframework/ai/model/mistralai/autoconfigure/MistralAiChatProperties.java @@ -35,7 +35,7 @@ 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(); + public static final String DEFAULT_CHAT_MODEL = MistralAiApi.ChatModel.SMALL.getValue(); private static final Double DEFAULT_TEMPERATURE = 0.7; diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralModelConfigurationTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralModelConfigurationTests.java new file mode 100644 index 000000000..fb4d5013d --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/MistralModelConfigurationTests.java @@ -0,0 +1,82 @@ +/* + * 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. + */ + +package org.springframework.ai.model.mistralai.autoconfigure; + +import org.junit.jupiter.api.Test; + +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; + +/** + * Unit Tests for {@link MistralAiAutoConfiguration}'s conditional enabling of models. + * + * @author Ilayaperumal Gopinathan + */ +public class MistralModelConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(MistralAiAutoConfiguration.class)) + .withPropertyValues("spring.ai.mistralai.apiKey=" + System.getenv("MISTRAL_AI_API_KEY")); + + @Test + void chatModelActivation() { + this.contextRunner.run(context -> { + assertThat(context.getBeansOfType(MistralAiChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MistralAiChatModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MistralAiEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MistralAiEmbeddingModel.class)).isNotEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.chat=none", "spring.ai.model.embedding=none") + .run(context -> { + assertThat(context.getBeansOfType(MistralAiChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MistralAiChatModel.class)).isEmpty(); + assertThat(context.getBeansOfType(MistralAiEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MistralAiEmbeddingModel.class)).isEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.chat=mistral", "spring.ai.model.embedding=none") + .run(context -> { + assertThat(context.getBeansOfType(MistralAiChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MistralAiChatModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MistralAiEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MistralAiEmbeddingModel.class)).isEmpty(); + }); + } + + @Test + void embeddingModelActivation() { + this.contextRunner.run(context -> { + assertThat(context.getBeansOfType(MistralAiEmbeddingModel.class)).isNotEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.embedding=none").run(context -> { + assertThat(context.getBeansOfType(MistralAiEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MistralAiEmbeddingModel.class)).isEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.embedding=mistral").run(context -> { + assertThat(context.getBeansOfType(MistralAiEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(MistralAiEmbeddingModel.class)).isNotEmpty(); + }); + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusBeanOpenAiIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusBeanOpenAiIT.java index 9c4b1c71a..ad66bc99d 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusBeanOpenAiIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai/src/test/java/org/springframework/ai/model/mistralai/autoconfigure/tool/PaymentStatusBeanOpenAiIT.java @@ -26,11 +26,11 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.ai.model.openai.autoconfigure.OpenAiAutoConfiguration; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.mistralai.api.MistralAiApi; +import org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration; import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -60,7 +60,7 @@ class PaymentStatusBeanOpenAiIT { 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)) + .withConfiguration(AutoConfigurations.of(OpenAiChatAutoConfiguration.class)) .withUserConfiguration(Config.class); @Test diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/pom.xml index 60f725ddb..10b41e644 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/pom.xml @@ -35,13 +35,6 @@ - - org.springframework.ai - spring-ai-autoconfigure-model-chat-client - ${project.parent.version} - true - - org.springframework.ai spring-ai-autoconfigure-retry @@ -51,14 +44,14 @@ org.springframework.ai - spring-ai-autoconfigure-observation-chat + spring-ai-autoconfigure-model-chat-observation ${project.parent.version} true org.springframework.ai - spring-ai-autoconfigure-observation-embedding + spring-ai-autoconfigure-model-embedding-observation ${project.parent.version} true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotAutoConfiguration.java index 51674fc76..d1d836c61 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/main/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotAutoConfiguration.java @@ -20,6 +20,8 @@ import java.util.List; import io.micrometer.observation.ObservationRegistry; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.chat.observation.ChatModelObservationConvention; import org.springframework.ai.model.function.DefaultFunctionCallbackResolver; @@ -54,7 +56,10 @@ public class MoonshotAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = MoonshotChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + // @ConditionalOnProperty(prefix = MoonshotChatProperties.CONFIG_PREFIX, name = + // "enabled", havingValue = "true", + // matchIfMissing = true) + @ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.MOONSHOT, matchIfMissing = true) public MoonshotChatModel moonshotChatModel(MoonshotCommonProperties commonProperties, MoonshotChatProperties chatProperties, ObjectProvider restClientBuilderProvider, diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotPropertiesTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotPropertiesTests.java index c97df8b12..119b0fbfe 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotPropertiesTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-moonshot/src/test/java/org/springframework/ai/model/moonshot/autoconfigure/MoonshotPropertiesTests.java @@ -135,7 +135,7 @@ public class MoonshotPropertiesTests { 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") + "spring.ai.model.chat=none") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class)) .run(context -> { @@ -154,7 +154,7 @@ public class MoonshotPropertiesTests { new ApplicationContextRunner() .withPropertyValues("spring.ai.moonshot.api-key=API_KEY", "spring.ai.moonshot.base-url=TEST_BASE_URL", - "spring.ai.moonshot.chat.enabled=true") + "spring.ai.model.chat=moonshot") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, MoonshotAutoConfiguration.class)) .run(context -> { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/pom.xml index d5fdfffcc..4bd179f43 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/pom.xml @@ -35,13 +35,6 @@ - - org.springframework.ai - spring-ai-autoconfigure-model-chat-client - ${project.parent.version} - true - - org.springframework.ai spring-ai-autoconfigure-retry @@ -51,14 +44,14 @@ org.springframework.ai - spring-ai-autoconfigure-observation-chat + spring-ai-autoconfigure-model-chat-observation ${project.parent.version} true org.springframework.ai - spring-ai-autoconfigure-observation-embedding + spring-ai-autoconfigure-model-embedding-observation ${project.parent.version} true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfiguration.java index 84d872137..777c89718 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-oci-genai/src/main/java/org/springframework/ai/model/oci/genai/autoconfigure/OCIGenAiAutoConfiguration.java @@ -31,6 +31,8 @@ import com.oracle.bmc.retrier.RetryConfiguration; import io.micrometer.observation.ObservationRegistry; import org.springframework.ai.chat.observation.ChatModelObservationConvention; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; import org.springframework.ai.oci.OCIEmbeddingModel; import org.springframework.ai.oci.cohere.OCICohereChatModel; import org.springframework.beans.factory.ObjectProvider; @@ -90,7 +92,7 @@ public class OCIGenAiAutoConfiguration { } @Bean - @ConditionalOnProperty(prefix = OCIEmbeddingModelProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + @ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.OCI_GENAI, matchIfMissing = true) public OCIEmbeddingModel ociEmbeddingModel(GenerativeAiInferenceClient generativeAiClient, OCIEmbeddingModelProperties properties) { @@ -98,7 +100,7 @@ public class OCIGenAiAutoConfiguration { } @Bean - @ConditionalOnProperty(prefix = OCICohereChatModelProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + @ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.OCI_GENAI, matchIfMissing = true) public OCICohereChatModel ociChatModel(GenerativeAiInferenceClient generativeAiClient, OCICohereChatModelProperties properties, ObjectProvider observationRegistry, diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/pom.xml index 9f24276ae..cfece9d07 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/pom.xml @@ -34,14 +34,6 @@ - - - org.springframework.ai - spring-ai-autoconfigure-model-chat-client - ${project.parent.version} - true - - org.springframework.ai spring-ai-autoconfigure-model-tool @@ -51,21 +43,21 @@ org.springframework.ai - spring-ai-autoconfigure-observation-chat + spring-ai-autoconfigure-model-chat-observation ${project.parent.version} true org.springframework.ai - spring-ai-autoconfigure-observation-embedding + spring-ai-autoconfigure-model-embedding-observation ${project.parent.version} true org.springframework.ai - spring-ai-autoconfigure-observation-image + spring-ai-autoconfigure-model-image-observation ${project.parent.version} true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaAutoConfiguration.java index 7d7948f8e..0a0af3125 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/main/java/org/springframework/ai/model/ollama/autoconfigure/OllamaAutoConfiguration.java @@ -18,6 +18,8 @@ package org.springframework.ai.model.ollama.autoconfigure; import io.micrometer.observation.ObservationRegistry; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration; import org.springframework.ai.chat.observation.ChatModelObservationConvention; import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; @@ -77,7 +79,7 @@ public class OllamaAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = OllamaChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + @ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.OLLAMA, matchIfMissing = true) public OllamaChatModel ollamaChatModel(OllamaApi ollamaApi, OllamaChatProperties properties, OllamaInitializationProperties initProperties, ToolCallingManager toolCallingManager, @@ -103,7 +105,7 @@ public class OllamaAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = OllamaEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + @ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.OLLAMA, matchIfMissing = true) public OllamaEmbeddingModel ollamaEmbeddingModel(OllamaApi ollamaApi, OllamaEmbeddingProperties properties, OllamaInitializationProperties initProperties, ObjectProvider observationRegistry, diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaModelConfigurationTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaModelConfigurationTests.java new file mode 100644 index 000000000..a02d8a02d --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-ollama/src/test/java/org/springframework/ai/model/ollama/autoconfigure/OllamaModelConfigurationTests.java @@ -0,0 +1,81 @@ +/* + * 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. + */ + +package org.springframework.ai.model.ollama.autoconfigure; + +import org.junit.jupiter.api.Test; + +import org.springframework.ai.ollama.OllamaChatModel; +import org.springframework.ai.ollama.OllamaEmbeddingModel; +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 OllamaAutoConfiguration}'s conditional enabling of models. + * + * @author Ilayaperumal Gopinathan + */ +public class OllamaModelConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(OllamaAutoConfiguration.class)); + + @Test + void chatModelActivation() { + this.contextRunner.run(context -> { + assertThat(context.getBeansOfType(OllamaChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OllamaChatModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isNotEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.chat=none", "spring.ai.model.embedding=none") + .run(context -> { + assertThat(context.getBeansOfType(OllamaChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OllamaChatModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.chat=ollama", "spring.ai.model.embedding=none") + .run(context -> { + assertThat(context.getBeansOfType(OllamaChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OllamaChatModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isEmpty(); + }); + } + + @Test + void embeddingModelActivation() { + this.contextRunner.run(context -> { + assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isNotEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.embedding=none").run(context -> { + assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.embedding=ollama").run(context -> { + assertThat(context.getBeansOfType(OllamaEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OllamaEmbeddingModel.class)).isNotEmpty(); + }); + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAIAutoConfigurationUtil.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAIAutoConfigurationUtil.java new file mode 100644 index 000000000..344e2f7e3 --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAIAutoConfigurationUtil.java @@ -0,0 +1,66 @@ +/* + * 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. + */ + +package org.springframework.ai.model.openai.autoconfigure; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jetbrains.annotations.NotNull; + +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; + +public class OpenAIAutoConfigurationUtil { + + protected 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)); + } + + public record ResolvedConnectionProperties(String baseUrl, String apiKey, MultiValueMap headers) { + + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioSpeechAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioSpeechAutoConfiguration.java new file mode 100644 index 000000000..7c32b0446 --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioSpeechAutoConfiguration.java @@ -0,0 +1,83 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.model.openai.autoconfigure; + +import org.springframework.ai.model.SimpleApiKey; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; +import org.springframework.ai.openai.OpenAiAudioSpeechModel; +import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.openai.api.OpenAiAudioApi; +import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; +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.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; + +import static org.springframework.ai.model.openai.autoconfigure.OpenAIAutoConfigurationUtil.resolveConnectionProperties; + +/** + * {@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 }) +@ConditionalOnClass(OpenAiApi.class) +@ConditionalOnProperty(name = SpringAIModelProperties.AUDIO_SPEECH_MODEL, havingValue = SpringAIModels.OPENAI, + matchIfMissing = true) +@EnableConfigurationProperties({ OpenAiConnectionProperties.class, OpenAiAudioSpeechProperties.class }) +@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, + WebClientAutoConfiguration.class }) +public class OpenAiAudioSpeechAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public OpenAiAudioSpeechModel openAiAudioSpeechModel(OpenAiConnectionProperties commonProperties, + OpenAiAudioSpeechProperties speechProperties, RetryTemplate retryTemplate, + ObjectProvider restClientBuilderProvider, + ObjectProvider webClientBuilderProvider, ResponseErrorHandler responseErrorHandler) { + + OpenAIAutoConfigurationUtil.ResolvedConnectionProperties resolved = resolveConnectionProperties( + commonProperties, speechProperties, "speech"); + + 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()); + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioTranscriptionAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioTranscriptionAutoConfiguration.java new file mode 100644 index 000000000..c01dc0856 --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAudioTranscriptionAutoConfiguration.java @@ -0,0 +1,85 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.model.openai.autoconfigure; + +import org.springframework.ai.model.SimpleApiKey; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; +import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration; +import org.springframework.ai.openai.OpenAiAudioTranscriptionModel; +import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.openai.api.OpenAiAudioApi; +import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; +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.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; + +import static org.springframework.ai.model.openai.autoconfigure.OpenAIAutoConfigurationUtil.resolveConnectionProperties; + +/** + * {@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 }) +@ConditionalOnClass(OpenAiApi.class) +@ConditionalOnProperty(name = SpringAIModelProperties.AUDIO_TRANSCRIPTION_MODEL, havingValue = SpringAIModels.OPENAI, + matchIfMissing = true) +@EnableConfigurationProperties({ OpenAiConnectionProperties.class, OpenAiAudioTranscriptionProperties.class }) +@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, + WebClientAutoConfiguration.class }) +public class OpenAiAudioTranscriptionAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public OpenAiAudioTranscriptionModel openAiAudioTranscriptionModel(OpenAiConnectionProperties commonProperties, + OpenAiAudioTranscriptionProperties transcriptionProperties, RetryTemplate retryTemplate, + ObjectProvider restClientBuilderProvider, + ObjectProvider webClientBuilderProvider, ResponseErrorHandler responseErrorHandler) { + + OpenAIAutoConfigurationUtil.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); + + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAutoConfiguration.java deleted file mode 100644 index 1d6ddc51a..000000000 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAutoConfiguration.java +++ /dev/null @@ -1,310 +0,0 @@ -/* - * Copyright 2023-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.ai.model.openai.autoconfigure; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import io.micrometer.observation.ObservationRegistry; -import org.jetbrains.annotations.NotNull; - -import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration; -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; -import org.springframework.ai.chat.observation.ChatModelObservationConvention; -import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; -import org.springframework.ai.image.observation.ImageModelObservationConvention; -import org.springframework.ai.model.SimpleApiKey; -import org.springframework.ai.model.function.DefaultFunctionCallbackResolver; -import org.springframework.ai.model.function.FunctionCallbackResolver; -import org.springframework.ai.model.tool.ToolCallingManager; -import org.springframework.ai.openai.OpenAiAudioSpeechModel; -import org.springframework.ai.openai.OpenAiAudioTranscriptionModel; -import org.springframework.ai.openai.OpenAiChatModel; -import org.springframework.ai.openai.OpenAiEmbeddingModel; -import org.springframework.ai.openai.OpenAiImageModel; -import org.springframework.ai.openai.OpenAiModerationModel; -import org.springframework.ai.openai.api.OpenAiApi; -import org.springframework.ai.openai.api.OpenAiAudioApi; -import org.springframework.ai.openai.api.OpenAiImageApi; -import org.springframework.ai.openai.api.OpenAiModerationApi; -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.retry.support.RetryTemplate; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; -import org.springframework.util.MultiValueMap; -import org.springframework.util.StringUtils; -import org.springframework.web.client.ResponseErrorHandler; -import org.springframework.web.client.RestClient; -import org.springframework.web.reactive.function.client.WebClient; - -/** - * {@link AutoConfiguration Auto-configuration} for OpenAI. - * - * @author Christian Tzolov - * @author Stefan Vassilev - * @author Thomas Vitale - * @author Ilayaperumal Gopinathan - */ -@AutoConfiguration(after = { RestClientAutoConfiguration.class, WebClientAutoConfiguration.class, - SpringAiRetryAutoConfiguration.class, ToolCallingAutoConfiguration.class }) -@ConditionalOnClass(OpenAiApi.class) -@EnableConfigurationProperties({ OpenAiConnectionProperties.class, OpenAiChatProperties.class, - OpenAiEmbeddingProperties.class, OpenAiImageProperties.class, OpenAiAudioTranscriptionProperties.class, - OpenAiAudioSpeechProperties.class, OpenAiModerationProperties.class }) -@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, - WebClientAutoConfiguration.class, ToolCallingAutoConfiguration.class }) -public class OpenAiAutoConfiguration { - - private static @NotNull ResolvedConnectionProperties resolveConnectionProperties( - OpenAiParentProperties commonProperties, OpenAiParentProperties modelProperties, String modelType) { - - String baseUrl = StringUtils.hasText(modelProperties.getBaseUrl()) ? modelProperties.getBaseUrl() - : commonProperties.getBaseUrl(); - String apiKey = StringUtils.hasText(modelProperties.getApiKey()) ? modelProperties.getApiKey() - : commonProperties.getApiKey(); - String projectId = StringUtils.hasText(modelProperties.getProjectId()) ? modelProperties.getProjectId() - : commonProperties.getProjectId(); - String organizationId = StringUtils.hasText(modelProperties.getOrganizationId()) - ? modelProperties.getOrganizationId() : commonProperties.getOrganizationId(); - - Map> connectionHeaders = new HashMap<>(); - if (StringUtils.hasText(projectId)) { - connectionHeaders.put("OpenAI-Project", List.of(projectId)); - } - if (StringUtils.hasText(organizationId)) { - connectionHeaders.put("OpenAI-Organization", List.of(organizationId)); - } - - Assert.hasText(baseUrl, - "OpenAI base URL must be set. Use the connection property: spring.ai.openai.base-url or spring.ai.openai." - + modelType + ".base-url property."); - Assert.hasText(apiKey, - "OpenAI API key must be set. Use the connection property: spring.ai.openai.api-key or spring.ai.openai." - + modelType + ".api-key property."); - - return new ResolvedConnectionProperties(baseUrl, apiKey, CollectionUtils.toMultiValueMap(connectionHeaders)); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = OpenAiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public OpenAiChatModel openAiChatModel(OpenAiConnectionProperties commonProperties, - OpenAiChatProperties chatProperties, ObjectProvider restClientBuilderProvider, - ObjectProvider webClientBuilderProvider, ToolCallingManager toolCallingManager, - RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, - ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - var openAiApi = openAiApi(chatProperties, commonProperties, - restClientBuilderProvider.getIfAvailable(RestClient::builder), - webClientBuilderProvider.getIfAvailable(WebClient::builder), responseErrorHandler, "chat"); - - var chatModel = OpenAiChatModel.builder() - .openAiApi(openAiApi) - .defaultOptions(chatProperties.getOptions()) - .toolCallingManager(toolCallingManager) - .retryTemplate(retryTemplate) - .observationRegistry(observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)) - .build(); - - observationConvention.ifAvailable(chatModel::setObservationConvention); - - return chatModel; - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = OpenAiEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public OpenAiEmbeddingModel openAiEmbeddingModel(OpenAiConnectionProperties commonProperties, - OpenAiEmbeddingProperties embeddingProperties, ObjectProvider restClientBuilderProvider, - ObjectProvider webClientBuilderProvider, RetryTemplate retryTemplate, - ResponseErrorHandler responseErrorHandler, ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - var openAiApi = openAiApi(embeddingProperties, commonProperties, - restClientBuilderProvider.getIfAvailable(RestClient::builder), - webClientBuilderProvider.getIfAvailable(WebClient::builder), responseErrorHandler, "embedding"); - - var embeddingModel = new OpenAiEmbeddingModel(openAiApi, embeddingProperties.getMetadataMode(), - embeddingProperties.getOptions(), retryTemplate, - observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); - - observationConvention.ifAvailable(embeddingModel::setObservationConvention); - - return embeddingModel; - } - - private OpenAiApi openAiApi(OpenAiChatProperties chatProperties, OpenAiConnectionProperties commonProperties, - RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, - ResponseErrorHandler responseErrorHandler, String modelType) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, chatProperties, - modelType); - - return OpenAiApi.builder() - .baseUrl(resolved.baseUrl()) - .apiKey(new SimpleApiKey(resolved.apiKey())) - .headers(resolved.headers()) - .completionsPath(chatProperties.getCompletionsPath()) - .embeddingsPath(OpenAiEmbeddingProperties.DEFAULT_EMBEDDINGS_PATH) - .restClientBuilder(restClientBuilder) - .webClientBuilder(webClientBuilder) - .responseErrorHandler(responseErrorHandler) - .build(); - } - - private OpenAiApi openAiApi(OpenAiEmbeddingProperties embeddingProperties, - OpenAiConnectionProperties commonProperties, RestClient.Builder restClientBuilder, - WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler, String modelType) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, embeddingProperties, - modelType); - - return OpenAiApi.builder() - .baseUrl(resolved.baseUrl()) - .apiKey(new SimpleApiKey(resolved.apiKey())) - .headers(resolved.headers()) - .completionsPath(OpenAiChatProperties.DEFAULT_COMPLETIONS_PATH) - .embeddingsPath(embeddingProperties.getEmbeddingsPath()) - .restClientBuilder(restClientBuilder) - .webClientBuilder(webClientBuilder) - .responseErrorHandler(responseErrorHandler) - .build(); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = OpenAiImageProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public OpenAiImageModel openAiImageModel(OpenAiConnectionProperties commonProperties, - OpenAiImageProperties imageProperties, ObjectProvider restClientBuilderProvider, - RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, - ObjectProvider observationRegistry, - ObjectProvider observationConvention) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, imageProperties, "image"); - - var openAiImageApi = OpenAiImageApi.builder() - .baseUrl(resolved.baseUrl()) - .apiKey(new SimpleApiKey(resolved.apiKey())) - .headers(resolved.headers()) - .restClientBuilder(restClientBuilderProvider.getIfAvailable(RestClient::builder)) - .responseErrorHandler(responseErrorHandler) - .build(); - var imageModel = new OpenAiImageModel(openAiImageApi, imageProperties.getOptions(), retryTemplate, - observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); - - observationConvention.ifAvailable(imageModel::setObservationConvention); - - return imageModel; - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = OpenAiAudioTranscriptionProperties.CONFIG_PREFIX, name = "enabled", - havingValue = "true", matchIfMissing = true) - public OpenAiAudioTranscriptionModel openAiAudioTranscriptionModel(OpenAiConnectionProperties commonProperties, - OpenAiAudioTranscriptionProperties transcriptionProperties, RetryTemplate retryTemplate, - ObjectProvider restClientBuilderProvider, - ObjectProvider webClientBuilderProvider, ResponseErrorHandler responseErrorHandler) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, transcriptionProperties, - "transcription"); - - var openAiAudioApi = OpenAiAudioApi.builder() - .baseUrl(resolved.baseUrl()) - .apiKey(new SimpleApiKey(resolved.apiKey())) - .headers(resolved.headers()) - .restClientBuilder(restClientBuilderProvider.getIfAvailable(RestClient::builder)) - .webClientBuilder(webClientBuilderProvider.getIfAvailable(WebClient::builder)) - .responseErrorHandler(responseErrorHandler) - .build(); - - return new OpenAiAudioTranscriptionModel(openAiAudioApi, transcriptionProperties.getOptions(), retryTemplate); - - } - - @Bean - @ConditionalOnMissingBean - public OpenAiModerationModel openAiModerationClient(OpenAiConnectionProperties commonProperties, - OpenAiModerationProperties moderationProperties, RetryTemplate retryTemplate, - ObjectProvider restClientBuilderProvider, ResponseErrorHandler responseErrorHandler) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, moderationProperties, - "moderation"); - - var openAiModerationApi = OpenAiModerationApi.builder() - .baseUrl(resolved.baseUrl) - .apiKey(new SimpleApiKey(resolved.apiKey())) - .headers(resolved.headers()) - .restClientBuilder(restClientBuilderProvider.getIfAvailable(RestClient::builder)) - .responseErrorHandler(responseErrorHandler) - .build(); - return new OpenAiModerationModel(openAiModerationApi, retryTemplate) - .withDefaultOptions(moderationProperties.getOptions()); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = OpenAiAudioSpeechProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", - matchIfMissing = true) - public OpenAiAudioSpeechModel openAiAudioSpeechClient(OpenAiConnectionProperties commonProperties, - OpenAiAudioSpeechProperties speechProperties, RetryTemplate retryTemplate, - ObjectProvider restClientBuilderProvider, - ObjectProvider webClientBuilderProvider, ResponseErrorHandler responseErrorHandler) { - - ResolvedConnectionProperties resolved = resolveConnectionProperties(commonProperties, speechProperties, - "speach"); - - var openAiAudioApi = OpenAiAudioApi.builder() - .baseUrl(resolved.baseUrl()) - .apiKey(new SimpleApiKey(resolved.apiKey())) - .headers(resolved.headers()) - .restClientBuilder(restClientBuilderProvider.getIfAvailable(RestClient::builder)) - .webClientBuilder(webClientBuilderProvider.getIfAvailable(WebClient::builder)) - .responseErrorHandler(responseErrorHandler) - .build(); - - return new OpenAiAudioSpeechModel(openAiAudioApi, speechProperties.getOptions()); - } - - @Bean - @ConditionalOnMissingBean - public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) { - DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver(); - manager.setApplicationContext(context); - return manager; - } - - private record ResolvedConnectionProperties(String baseUrl, String apiKey, MultiValueMap headers) { - - } - -} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiChatAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiChatAutoConfiguration.java new file mode 100644 index 000000000..96e297267 --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiChatAutoConfiguration.java @@ -0,0 +1,132 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.model.openai.autoconfigure; + +import io.micrometer.observation.ObservationRegistry; + +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.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; +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.model.tool.autoconfigure.ToolCallingAutoConfiguration; +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.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; +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.MultiValueMap; +import org.springframework.web.client.ResponseErrorHandler; +import org.springframework.web.client.RestClient; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.springframework.ai.model.openai.autoconfigure.OpenAIAutoConfigurationUtil.resolveConnectionProperties; + +/** + * {@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 }) +@ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.OPENAI, + matchIfMissing = true) +@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, + WebClientAutoConfiguration.class, ToolCallingAutoConfiguration.class }) +public class OpenAiChatAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + 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; + } + + private OpenAiApi openAiApi(OpenAiChatProperties chatProperties, OpenAiConnectionProperties commonProperties, + RestClient.Builder restClientBuilder, WebClient.Builder webClientBuilder, + ResponseErrorHandler responseErrorHandler, String modelType) { + + OpenAIAutoConfigurationUtil.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(); + } + + @Bean + @ConditionalOnMissingBean + public FunctionCallbackResolver springAiFunctionManager(ApplicationContext context) { + DefaultFunctionCallbackResolver manager = new DefaultFunctionCallbackResolver(); + manager.setApplicationContext(context); + return manager; + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiEmbeddingAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiEmbeddingAutoConfiguration.java new file mode 100644 index 000000000..3367d0d38 --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiEmbeddingAutoConfiguration.java @@ -0,0 +1,103 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.model.openai.autoconfigure; + +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; +import org.springframework.ai.model.SimpleApiKey; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; +import org.springframework.ai.openai.OpenAiEmbeddingModel; +import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; +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.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; + +import static org.springframework.ai.model.openai.autoconfigure.OpenAIAutoConfigurationUtil.resolveConnectionProperties; + +/** + * {@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 }) +@ConditionalOnClass(OpenAiApi.class) +@ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.OPENAI, + matchIfMissing = true) +@EnableConfigurationProperties({ OpenAiConnectionProperties.class, OpenAiEmbeddingProperties.class }) +@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, + WebClientAutoConfiguration.class }) +public class OpenAiEmbeddingAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + 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(OpenAiEmbeddingProperties embeddingProperties, + OpenAiConnectionProperties commonProperties, RestClient.Builder restClientBuilder, + WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler, String modelType) { + + OpenAIAutoConfigurationUtil.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(); + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiImageAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiImageAutoConfiguration.java new file mode 100644 index 000000000..4a9966072 --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiImageAutoConfiguration.java @@ -0,0 +1,89 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.model.openai.autoconfigure; + +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.ai.image.observation.ImageModelObservationConvention; +import org.springframework.ai.model.SimpleApiKey; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; +import org.springframework.ai.openai.OpenAiImageModel; +import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.openai.api.OpenAiImageApi; +import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; +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.annotation.Bean; +import org.springframework.retry.support.RetryTemplate; +import org.springframework.web.client.ResponseErrorHandler; +import org.springframework.web.client.RestClient; + +import static org.springframework.ai.model.openai.autoconfigure.OpenAIAutoConfigurationUtil.resolveConnectionProperties; + +/** + * {@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 }) +@ConditionalOnClass(OpenAiApi.class) +@ConditionalOnProperty(name = SpringAIModelProperties.IMAGE_MODEL, havingValue = SpringAIModels.OPENAI, + matchIfMissing = true) +@EnableConfigurationProperties({ OpenAiConnectionProperties.class, OpenAiImageProperties.class }) +@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, + WebClientAutoConfiguration.class }) +public class OpenAiImageAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public OpenAiImageModel openAiImageModel(OpenAiConnectionProperties commonProperties, + OpenAiImageProperties imageProperties, ObjectProvider restClientBuilderProvider, + RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler, + ObjectProvider observationRegistry, + ObjectProvider observationConvention) { + + OpenAIAutoConfigurationUtil.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; + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiModerationAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiModerationAutoConfiguration.java new file mode 100644 index 000000000..86b72a103 --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/java/org/springframework/ai/model/openai/autoconfigure/OpenAiModerationAutoConfiguration.java @@ -0,0 +1,98 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.model.openai.autoconfigure; + +import io.micrometer.observation.ObservationRegistry; + +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.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; +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.model.tool.autoconfigure.ToolCallingAutoConfiguration; +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.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; +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; + +import static org.springframework.ai.model.openai.autoconfigure.OpenAIAutoConfigurationUtil.resolveConnectionProperties; + +/** + * {@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 }) +@ConditionalOnClass(OpenAiApi.class) +@ConditionalOnProperty(name = SpringAIModelProperties.MODERATION_MODEL, havingValue = SpringAIModels.OPENAI, + matchIfMissing = true) +@EnableConfigurationProperties({ OpenAiConnectionProperties.class, OpenAiModerationProperties.class }) +@ImportAutoConfiguration(classes = { SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, + WebClientAutoConfiguration.class }) +public class OpenAiModerationAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public OpenAiModerationModel openAiModerationModel(OpenAiConnectionProperties commonProperties, + OpenAiModerationProperties moderationProperties, RetryTemplate retryTemplate, + ObjectProvider restClientBuilderProvider, ResponseErrorHandler responseErrorHandler) { + + OpenAIAutoConfigurationUtil.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()); + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 8edc3fa17..d31ef583d 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -13,4 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. # -org.springframework.ai.model.openai.autoconfigure.OpenAiAutoConfiguration +org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration +org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration +org.springframework.ai.model.openai.autoconfigure.OpenAiImageAutoConfiguration +org.springframework.ai.model.openai.autoconfigure.OpenAiAudioSpeechAutoConfiguration +org.springframework.ai.model.openai.autoconfigure.OpenAiAudioTranscriptionAutoConfiguration +org.springframework.ai.model.openai.autoconfigure.OpenAiModerationAutoConfiguration diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/ChatClientAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/ChatClientAutoConfigurationIT.java index 428ae5ed6..40eeaab33 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/ChatClientAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/ChatClientAutoConfigurationIT.java @@ -23,10 +23,10 @@ import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration; -import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.ChatClientCustomizer; +import org.springframework.ai.model.chat.client.autoconfigure.ChatClientAutoConfiguration; +import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -46,8 +46,9 @@ public class ChatClientAutoConfigurationIT { 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)); + .withConfiguration( + AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, + OpenAiChatAutoConfiguration.class, ChatClientAutoConfiguration.class)); @Test void implicitlyEnabled() { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAutoConfigurationIT.java index e384541c0..a8054a7cc 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAutoConfigurationIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiAutoConfigurationIT.java @@ -52,12 +52,11 @@ 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)); + .withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY")); @Test void chatCall() { - this.contextRunner.run(context -> { + this.contextRunner.withConfiguration(AutoConfigurations.of(OpenAiChatAutoConfiguration.class)).run(context -> { OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); String response = chatModel.call("Hello"); assertThat(response).isNotEmpty(); @@ -73,6 +72,7 @@ public class OpenAiAutoConfigurationIT { "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") + .withConfiguration(AutoConfigurations.of(OpenAiChatAutoConfiguration.class)) .run(context -> { OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); @@ -86,28 +86,30 @@ public class OpenAiAutoConfigurationIT { @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); - }); + this.contextRunner.withConfiguration(AutoConfigurations.of(OpenAiAudioTranscriptionAutoConfiguration.class)) + .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); + this.contextRunner.withConfiguration(AutoConfigurations.of(OpenAiAudioSpeechAutoConfiguration.class)) + .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)); - }); + logger.debug("Response: " + Arrays.toString(response)); + }); } public boolean verifyMp3FrameHeader(byte[] audioResponse) { @@ -123,7 +125,7 @@ public class OpenAiAutoConfigurationIT { @Test void generateStreaming() { - this.contextRunner.run(context -> { + this.contextRunner.withConfiguration(AutoConfigurations.of(OpenAiChatAutoConfiguration.class)).run(context -> { OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); Flux responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello"))); String response = responseFlux.collectList() @@ -139,52 +141,57 @@ public class OpenAiAutoConfigurationIT { @Test void streamingWithTokenUsage() { - this.contextRunner.withPropertyValues("spring.ai.openai.chat.options.stream-usage=true").run(context -> { - OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); + this.contextRunner.withPropertyValues("spring.ai.openai.chat.options.stream-usage=true") + .withConfiguration(AutoConfigurations.of(OpenAiChatAutoConfiguration.class)) + .run(context -> { + OpenAiChatModel chatModel = context.getBean(OpenAiChatModel.class); - Flux responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello"))); + 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()); + 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(streamingTokenUsage[0].getPromptTokens()).isGreaterThan(0); + assertThat(streamingTokenUsage[0].getCompletionTokens()).isGreaterThan(0); + assertThat(streamingTokenUsage[0].getTotalTokens()).isGreaterThan(0); - assertThat(response).isNotEmpty(); - logger.info("Response: " + response); - }); + assertThat(response).isNotEmpty(); + logger.info("Response: " + response); + }); } @Test void embedding() { - this.contextRunner.run(context -> { - OpenAiEmbeddingModel embeddingModel = context.getBean(OpenAiEmbeddingModel.class); + this.contextRunner.withConfiguration(AutoConfigurations.of(OpenAiEmbeddingAutoConfiguration.class)) + .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); + 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); - }); + 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()); - }); + this.contextRunner.withPropertyValues("spring.ai.openai.image.options.size=1024x1024") + .withConfiguration(AutoConfigurations.of(OpenAiImageAutoConfiguration.class)) + .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 @@ -193,6 +200,7 @@ public class OpenAiAutoConfigurationIT { this.contextRunner .withPropertyValues("spring.ai.openai.image.options.model=dall-e-2", "spring.ai.openai.image.options.size=256x256") + .withConfiguration(AutoConfigurations.of(OpenAiImageAutoConfiguration.class)) .run(context -> { OpenAiImageModel imageModel = context.getBean(OpenAiImageModel.class); ImageResponse imageResponse = imageModel.call(new ImagePrompt("forest")); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiModelConfigurationTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiModelConfigurationTests.java new file mode 100644 index 000000000..a7f301514 --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiModelConfigurationTests.java @@ -0,0 +1,308 @@ +/* + * 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. + */ + +package org.springframework.ai.model.openai.autoconfigure; + +import org.junit.jupiter.api.Test; + +import org.springframework.ai.openai.OpenAiAudioSpeechModel; +import org.springframework.ai.openai.OpenAiAudioTranscriptionModel; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiEmbeddingModel; +import org.springframework.ai.openai.OpenAiImageModel; +import org.springframework.ai.openai.OpenAiModerationModel; +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 OpenAI auto configurations' conditional enabling of models. + * + * @author Ilayaperumal Gopinathan + */ +public class OpenAiModelConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.base-url=TEST_BASE_URL"); + + @Test + void chatModelActivation() { + this.contextRunner.withConfiguration(AutoConfigurations.of(OpenAiChatAutoConfiguration.class)).run(context -> { + assertThat(context.getBeansOfType(OpenAiChatModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiImageModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiModerationModel.class)).isEmpty(); + }); + + this.contextRunner.withConfiguration(AutoConfigurations.of(OpenAiChatAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.chat=none") + .run(context -> { + assertThat(context.getBeansOfType(OpenAiChatProperties.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiChatModel.class)).isEmpty(); + }); + + this.contextRunner.withConfiguration(AutoConfigurations.of(OpenAiChatAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.chat=openai") + .run(context -> { + assertThat(context.getBeansOfType(OpenAiChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OpenAiChatModel.class)).isNotEmpty(); + }); + + this.contextRunner + .withConfiguration( + AutoConfigurations.of(OpenAiChatAutoConfiguration.class, OpenAiEmbeddingAutoConfiguration.class, + OpenAiImageAutoConfiguration.class, OpenAiAudioSpeechAutoConfiguration.class, + OpenAiAudioTranscriptionAutoConfiguration.class, OpenAiModerationAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.chat=openai", "spring.ai.model.embedding=none", + "spring.ai.model.image=none", "spring.ai.model.audio.speech=none", + "spring.ai.model.audio.transcription=none", "spring.ai.model.moderation=none") + .run(context -> { + assertThat(context.getBeansOfType(OpenAiChatModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiImageModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiModerationModel.class)).isEmpty(); + }); + } + + @Test + void embeddingModelActivation() { + this.contextRunner.withConfiguration(AutoConfigurations.of(OpenAiEmbeddingAutoConfiguration.class)) + .run(context -> { + assertThat(context.getBeansOfType(OpenAiChatModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OpenAiImageModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiModerationModel.class)).isEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.embedding=none") + .withConfiguration(AutoConfigurations.of(OpenAiEmbeddingAutoConfiguration.class)) + .run(context -> { + assertThat(context.getBeansOfType(OpenAiEmbeddingProperties.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isEmpty(); + }); + + this.contextRunner.withPropertyValues("spring.ai.model.embedding=openai") + .withConfiguration(AutoConfigurations.of(OpenAiEmbeddingAutoConfiguration.class)) + .run(context -> { + assertThat(context.getBeansOfType(OpenAiEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isNotEmpty(); + }); + + this.contextRunner + .withConfiguration( + AutoConfigurations.of(OpenAiChatAutoConfiguration.class, OpenAiEmbeddingAutoConfiguration.class, + OpenAiImageAutoConfiguration.class, OpenAiAudioSpeechAutoConfiguration.class, + OpenAiAudioTranscriptionAutoConfiguration.class, OpenAiModerationAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.chat=none", "spring.ai.model.embedding=openai", + "spring.ai.model.image=none", "spring.ai.model.audio.speech=none", + "spring.ai.model.audio.transcription=none", "spring.ai.model.moderation=none") + .withConfiguration(AutoConfigurations.of(OpenAiEmbeddingAutoConfiguration.class)) + .run(context -> { + assertThat(context.getBeansOfType(OpenAiChatModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OpenAiImageModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiModerationModel.class)).isEmpty(); + }); + } + + @Test + void imageModelActivation() { + this.contextRunner.withConfiguration(AutoConfigurations.of(OpenAiImageAutoConfiguration.class)).run(context -> { + assertThat(context.getBeansOfType(OpenAiChatModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiImageModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiModerationModel.class)).isEmpty(); + }); + + this.contextRunner.withConfiguration(AutoConfigurations.of(OpenAiImageAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.image=none") + .run(context -> { + assertThat(context.getBeansOfType(OpenAiImageProperties.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiImageModel.class)).isEmpty(); + }); + + this.contextRunner.withConfiguration(AutoConfigurations.of(OpenAiImageAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.image=openai") + .run(context -> { + assertThat(context.getBeansOfType(OpenAiImageProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OpenAiImageModel.class)).isNotEmpty(); + }); + + this.contextRunner + .withConfiguration( + AutoConfigurations.of(OpenAiChatAutoConfiguration.class, OpenAiEmbeddingAutoConfiguration.class, + OpenAiImageAutoConfiguration.class, OpenAiAudioSpeechAutoConfiguration.class, + OpenAiAudioTranscriptionAutoConfiguration.class, OpenAiModerationAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.chat=none", "spring.ai.model.embedding=none", + "spring.ai.model.image=openai", "spring.ai.model.audio.speech=none", + "spring.ai.model.audio.transcription=none", "spring.ai.model.moderation=none") + .withConfiguration(AutoConfigurations.of(OpenAiImageAutoConfiguration.class)) + .run(context -> { + assertThat(context.getBeansOfType(OpenAiChatModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiImageModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiModerationModel.class)).isEmpty(); + }); + } + + @Test + void audioSpeechModelActivation() { + this.contextRunner.withConfiguration(AutoConfigurations.of(OpenAiAudioSpeechAutoConfiguration.class)) + .run(context -> { + assertThat(context.getBeansOfType(OpenAiChatModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiImageModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiModerationModel.class)).isEmpty(); + }); + + this.contextRunner.withConfiguration(AutoConfigurations.of(OpenAiAudioSpeechAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.audio.speech=none") + .run(context -> { + assertThat(context.getBeansOfType(OpenAiAudioSpeechProperties.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isEmpty(); + }); + + this.contextRunner.withConfiguration(AutoConfigurations.of(OpenAiAudioSpeechAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.audio.speech=openai") + .run(context -> { + assertThat(context.getBeansOfType(OpenAiAudioSpeechProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isNotEmpty(); + }); + + this.contextRunner + .withConfiguration( + AutoConfigurations.of(OpenAiChatAutoConfiguration.class, OpenAiEmbeddingAutoConfiguration.class, + OpenAiImageAutoConfiguration.class, OpenAiAudioSpeechAutoConfiguration.class, + OpenAiAudioTranscriptionAutoConfiguration.class, OpenAiModerationAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.chat=none", "spring.ai.model.embedding=none", + "spring.ai.model.image=none", "spring.ai.model.audio.speech=openai", + "spring.ai.model.audio.transcription=none", "spring.ai.model.moderation=none") + .run(context -> { + assertThat(context.getBeansOfType(OpenAiChatModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiImageModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiModerationModel.class)).isEmpty(); + }); + } + + @Test + void audioTranscriptionModelActivation() { + this.contextRunner.withConfiguration(AutoConfigurations.of(OpenAiAudioTranscriptionAutoConfiguration.class)) + .run(context -> { + assertThat(context.getBeansOfType(OpenAiChatModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiImageModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OpenAiModerationModel.class)).isEmpty(); + }); + + this.contextRunner.withConfiguration(AutoConfigurations.of(OpenAiAudioTranscriptionAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.audio.transcription=none") + .run(context -> { + assertThat(context.getBeansOfType(OpenAiAudioTranscriptionProperties.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isEmpty(); + }); + + this.contextRunner.withConfiguration(AutoConfigurations.of(OpenAiAudioTranscriptionAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.audio.transcription=openai") + .run(context -> { + assertThat(context.getBeansOfType(OpenAiAudioTranscriptionProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isNotEmpty(); + }); + + this.contextRunner + .withConfiguration( + AutoConfigurations.of(OpenAiChatAutoConfiguration.class, OpenAiEmbeddingAutoConfiguration.class, + OpenAiImageAutoConfiguration.class, OpenAiAudioSpeechAutoConfiguration.class, + OpenAiAudioTranscriptionAutoConfiguration.class, OpenAiModerationAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.chat=none", "spring.ai.model.embedding=none", + "spring.ai.model.image=none", "spring.ai.model.audio.speech=none", + "spring.ai.model.audio.transcription=openai", "spring.ai.model.moderation=none") + .run(context -> { + assertThat(context.getBeansOfType(OpenAiChatModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiImageModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OpenAiModerationModel.class)).isEmpty(); + }); + } + + @Test + void moderationModelActivation() { + this.contextRunner.withConfiguration(AutoConfigurations.of(OpenAiModerationAutoConfiguration.class)) + .run(context -> { + assertThat(context.getBeansOfType(OpenAiChatModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiImageModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiModerationModel.class)).isNotEmpty(); + }); + + this.contextRunner.withConfiguration(AutoConfigurations.of(OpenAiModerationAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.moderation=none") + .run(context -> { + assertThat(context.getBeansOfType(OpenAiModerationProperties.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiModerationModel.class)).isEmpty(); + }); + + this.contextRunner.withConfiguration(AutoConfigurations.of(OpenAiModerationAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.moderation=openai") + .run(context -> { + assertThat(context.getBeansOfType(OpenAiModerationProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OpenAiModerationModel.class)).isNotEmpty(); + }); + + this.contextRunner + .withConfiguration( + AutoConfigurations.of(OpenAiChatAutoConfiguration.class, OpenAiEmbeddingAutoConfiguration.class, + OpenAiImageAutoConfiguration.class, OpenAiAudioSpeechAutoConfiguration.class, + OpenAiAudioTranscriptionAutoConfiguration.class, OpenAiModerationAutoConfiguration.class)) + .withPropertyValues("spring.ai.model.chat=none", "spring.ai.model.embedding=none", + "spring.ai.model.image=none", "spring.ai.model.audio.speech=none", + "spring.ai.model.audio.transcription=none", "spring.ai.model.moderation=openai") + .run(context -> { + assertThat(context.getBeansOfType(OpenAiChatModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiImageModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isEmpty(); + assertThat(context.getBeansOfType(OpenAiModerationModel.class)).isNotEmpty(); + }); + } + +} diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiPropertiesTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiPropertiesTests.java index bbff01e58..1e3098a7f 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiPropertiesTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiPropertiesTests.java @@ -54,7 +54,7 @@ public class OpenAiPropertiesTests { "spring.ai.openai.chat.options.model=MODEL_XYZ", "spring.ai.openai.chat.options.temperature=0.55") // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(OpenAiChatAutoConfiguration.class)) .run(context -> { var chatProperties = context.getBean(OpenAiChatProperties.class); var connectionProperties = context.getBean(OpenAiConnectionProperties.class); @@ -80,7 +80,7 @@ public class OpenAiPropertiesTests { "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)) + .withConfiguration(AutoConfigurations.of(OpenAiAudioTranscriptionAutoConfiguration.class)) .run(context -> { var transcriptionProperties = context.getBean(OpenAiAudioTranscriptionProperties.class); var connectionProperties = context.getBean(OpenAiConnectionProperties.class); @@ -108,7 +108,7 @@ public class OpenAiPropertiesTests { "spring.ai.openai.chat.options.model=MODEL_XYZ", "spring.ai.openai.chat.options.temperature=0.55") // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(OpenAiChatAutoConfiguration.class)) .run(context -> { var chatProperties = context.getBean(OpenAiChatProperties.class); var connectionProperties = context.getBean(OpenAiConnectionProperties.class); @@ -136,7 +136,7 @@ public class OpenAiPropertiesTests { "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)) + .withConfiguration(AutoConfigurations.of(OpenAiAudioTranscriptionAutoConfiguration.class)) .run(context -> { var transcriptionProperties = context.getBean(OpenAiAudioTranscriptionProperties.class); var connectionProperties = context.getBean(OpenAiConnectionProperties.class); @@ -164,7 +164,7 @@ public class OpenAiPropertiesTests { "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)) + .withConfiguration(AutoConfigurations.of(OpenAiAudioSpeechAutoConfiguration.class)) .run(context -> { var speechProperties = context.getBean(OpenAiAudioSpeechProperties.class); var connectionProperties = context.getBean(OpenAiConnectionProperties.class); @@ -195,7 +195,7 @@ public class OpenAiPropertiesTests { "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)) + .withConfiguration(AutoConfigurations.of(OpenAiAudioSpeechAutoConfiguration.class)) .run(context -> { var speechProperties = context.getBean(OpenAiAudioSpeechProperties.class); var connectionProperties = context.getBean(OpenAiConnectionProperties.class); @@ -225,7 +225,7 @@ public class OpenAiPropertiesTests { "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)) + .withConfiguration(AutoConfigurations.of(OpenAiAudioSpeechAutoConfiguration.class)) .run(context -> { var speechProperties = context.getBean(OpenAiAudioSpeechProperties.class); var connectionProperties = context.getBean(OpenAiConnectionProperties.class); @@ -253,7 +253,7 @@ public class OpenAiPropertiesTests { "spring.ai.openai.api-key=abc123", "spring.ai.openai.embedding.options.model=MODEL_XYZ") // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(OpenAiEmbeddingAutoConfiguration.class)) .run(context -> { var embeddingProperties = context.getBean(OpenAiEmbeddingProperties.class); var connectionProperties = context.getBean(OpenAiConnectionProperties.class); @@ -279,7 +279,7 @@ public class OpenAiPropertiesTests { "spring.ai.openai.embedding.api-key=456", "spring.ai.openai.embedding.options.model=MODEL_XYZ") // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(OpenAiEmbeddingAutoConfiguration.class)) .run(context -> { var embeddingProperties = context.getBean(OpenAiEmbeddingProperties.class); var connectionProperties = context.getBean(OpenAiConnectionProperties.class); @@ -303,7 +303,7 @@ public class OpenAiPropertiesTests { "spring.ai.openai.image.options.model=MODEL_XYZ", "spring.ai.openai.image.options.n=3") // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(OpenAiImageAutoConfiguration.class)) .run(context -> { var imageProperties = context.getBean(OpenAiImageProperties.class); var connectionProperties = context.getBean(OpenAiConnectionProperties.class); @@ -330,7 +330,7 @@ public class OpenAiPropertiesTests { "spring.ai.openai.image.options.model=MODEL_XYZ", "spring.ai.openai.image.options.n=3") // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(OpenAiImageAutoConfiguration.class)) .run(context -> { var imageProperties = context.getBean(OpenAiImageProperties.class); var connectionProperties = context.getBean(OpenAiConnectionProperties.class); @@ -397,17 +397,14 @@ public class OpenAiPropertiesTests { "spring.ai.openai.chat.options.user=userXYZ" ) // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(OpenAiChatAutoConfiguration.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); @@ -449,17 +446,14 @@ public class OpenAiPropertiesTests { "spring.ai.openai.audio.transcription.options.temperature=0.55" ) // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(OpenAiAudioTranscriptionAutoConfiguration.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"); @@ -482,7 +476,7 @@ public class OpenAiPropertiesTests { "spring.ai.openai.embedding.options.user=userXYZ" ) // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(OpenAiEmbeddingAutoConfiguration.class)) .run(context -> { var connectionProperties = context.getBean(OpenAiConnectionProperties.class); var embeddingProperties = context.getBean(OpenAiEmbeddingProperties.class); @@ -514,7 +508,7 @@ public class OpenAiPropertiesTests { "spring.ai.openai.image.options.user=userXYZ" ) // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(OpenAiImageAutoConfiguration.class)) .run(context -> { var imageProperties = context.getBean(OpenAiImageProperties.class); var connectionProperties = context.getBean(OpenAiConnectionProperties.class); @@ -539,16 +533,16 @@ public class OpenAiPropertiesTests { 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)) + "spring.ai.model.embedding=none") + .withConfiguration(AutoConfigurations.of(OpenAiEmbeddingAutoConfiguration.class)) .run(context -> { - assertThat(context.getBeansOfType(OpenAiEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OpenAiEmbeddingProperties.class)).isEmpty(); 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)) + .withConfiguration(AutoConfigurations.of(OpenAiEmbeddingAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(OpenAiEmbeddingProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isNotEmpty(); @@ -556,8 +550,8 @@ public class OpenAiPropertiesTests { 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)) + "spring.ai.model.embedding=openai") + .withConfiguration(AutoConfigurations.of(OpenAiEmbeddingAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(OpenAiEmbeddingProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isNotEmpty(); @@ -568,16 +562,16 @@ public class OpenAiPropertiesTests { 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)) + "spring.ai.model.chat=none") + .withConfiguration(AutoConfigurations.of(OpenAiChatAutoConfiguration.class)) .run(context -> { - assertThat(context.getBeansOfType(OpenAiChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OpenAiChatProperties.class)).isEmpty(); 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)) + .withConfiguration(AutoConfigurations.of(OpenAiChatAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(OpenAiChatProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(OpenAiChatModel.class)).isNotEmpty(); @@ -585,8 +579,8 @@ public class OpenAiPropertiesTests { 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)) + "spring.ai.model.chat=openai") + .withConfiguration(AutoConfigurations.of(OpenAiChatAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(OpenAiChatProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(OpenAiChatModel.class)).isNotEmpty(); @@ -598,16 +592,16 @@ public class OpenAiPropertiesTests { 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)) + "spring.ai.model.image=none") + .withConfiguration(AutoConfigurations.of(OpenAiImageAutoConfiguration.class)) .run(context -> { - assertThat(context.getBeansOfType(OpenAiImageProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OpenAiImageProperties.class)).isEmpty(); 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)) + .withConfiguration(AutoConfigurations.of(OpenAiImageAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(OpenAiImageProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(OpenAiImageModel.class)).isNotEmpty(); @@ -615,8 +609,8 @@ public class OpenAiPropertiesTests { 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)) + "spring.ai.model.image=openai") + .withConfiguration(AutoConfigurations.of(OpenAiImageAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(OpenAiImageProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(OpenAiImageModel.class)).isNotEmpty(); @@ -628,16 +622,16 @@ public class OpenAiPropertiesTests { 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)) + "spring.ai.model.audio.speech=none") + .withConfiguration(AutoConfigurations.of(OpenAiAudioSpeechAutoConfiguration.class)) .run(context -> { - assertThat(context.getBeansOfType(OpenAiAudioSpeechProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioSpeechProperties.class)).isEmpty(); 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)) + .withConfiguration(AutoConfigurations.of(OpenAiAudioSpeechAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(OpenAiAudioSpeechProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isNotEmpty(); @@ -645,8 +639,8 @@ public class OpenAiPropertiesTests { 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)) + "spring.ai.model.audio.speech=openai") + .withConfiguration(AutoConfigurations.of(OpenAiAudioSpeechAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(OpenAiAudioSpeechProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isNotEmpty(); @@ -658,16 +652,16 @@ public class OpenAiPropertiesTests { 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)) + "spring.ai.model.audio.transcription=none") + .withConfiguration(AutoConfigurations.of(OpenAiAudioTranscriptionAutoConfiguration.class)) .run(context -> { - assertThat(context.getBeansOfType(OpenAiAudioTranscriptionProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioTranscriptionProperties.class)).isEmpty(); 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)) + .withConfiguration(AutoConfigurations.of(OpenAiAudioTranscriptionAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(OpenAiAudioTranscriptionProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isNotEmpty(); @@ -675,8 +669,8 @@ public class OpenAiPropertiesTests { 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)) + "spring.ai.model.audio.transcription=openai") + .withConfiguration(AutoConfigurations.of(OpenAiAudioTranscriptionAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(OpenAiAudioTranscriptionProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isNotEmpty(); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiResponseFormatPropertiesTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiResponseFormatPropertiesTests.java index a863c88b7..e83a9c717 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiResponseFormatPropertiesTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/OpenAiResponseFormatPropertiesTests.java @@ -63,7 +63,7 @@ public class OpenAiResponseFormatPropertiesTests { "spring.ai.openai.chat.options.response-format.strict=true" ) // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(OpenAiChatAutoConfiguration.class)) .run(context -> { var chatProperties = context.getBean(OpenAiChatProperties.class); var connectionProperties = context.getBean(OpenAiConnectionProperties.class); @@ -82,7 +82,7 @@ public class OpenAiResponseFormatPropertiesTests { .withPropertyValues("spring.ai.openai.api-key=API_KEY", "spring.ai.openai.chat.options.response-format.type=JSON_OBJECT") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(OpenAiChatAutoConfiguration.class)) .run(context -> { var chatProperties = context.getBean(OpenAiChatProperties.class); @@ -96,7 +96,7 @@ public class OpenAiResponseFormatPropertiesTests { new ApplicationContextRunner().withPropertyValues("spring.ai.openai.api-key=API_KEY") - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(OpenAiChatAutoConfiguration.class)) .run(context -> { var chatProperties = context.getBean(OpenAiChatProperties.class); @@ -119,17 +119,13 @@ public class OpenAiResponseFormatPropertiesTests { "spring.ai.openai.audio.transcription.options.temperature=0.55" ) // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(OpenAiAudioTranscriptionAutoConfiguration.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"); @@ -152,7 +148,7 @@ public class OpenAiResponseFormatPropertiesTests { "spring.ai.openai.embedding.options.user=userXYZ" ) // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(OpenAiEmbeddingAutoConfiguration.class)) .run(context -> { var connectionProperties = context.getBean(OpenAiConnectionProperties.class); var embeddingProperties = context.getBean(OpenAiEmbeddingProperties.class); @@ -184,7 +180,7 @@ public class OpenAiResponseFormatPropertiesTests { "spring.ai.openai.image.options.user=userXYZ" ) // @formatter:on - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(OpenAiImageAutoConfiguration.class)) .run(context -> { var imageProperties = context.getBean(OpenAiImageProperties.class); var connectionProperties = context.getBean(OpenAiConnectionProperties.class); @@ -209,16 +205,16 @@ public class OpenAiResponseFormatPropertiesTests { 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)) + "spring.ai.model.embedding=none") + .withConfiguration(AutoConfigurations.of(OpenAiEmbeddingAutoConfiguration.class)) .run(context -> { - assertThat(context.getBeansOfType(OpenAiEmbeddingProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OpenAiEmbeddingProperties.class)).isEmpty(); 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)) + .withConfiguration(AutoConfigurations.of(OpenAiEmbeddingAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(OpenAiEmbeddingProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isNotEmpty(); @@ -226,8 +222,8 @@ public class OpenAiResponseFormatPropertiesTests { 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)) + "spring.ai.model.embedding=openai") + .withConfiguration(AutoConfigurations.of(OpenAiEmbeddingAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(OpenAiEmbeddingProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(OpenAiEmbeddingModel.class)).isNotEmpty(); @@ -238,16 +234,16 @@ public class OpenAiResponseFormatPropertiesTests { 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)) + "spring.ai.model.chat=none") + .withConfiguration(AutoConfigurations.of(OpenAiChatAutoConfiguration.class)) .run(context -> { - assertThat(context.getBeansOfType(OpenAiChatProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OpenAiChatProperties.class)).isEmpty(); 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)) + .withConfiguration(AutoConfigurations.of(OpenAiChatAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(OpenAiChatProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(OpenAiChatModel.class)).isNotEmpty(); @@ -255,8 +251,8 @@ public class OpenAiResponseFormatPropertiesTests { 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)) + "spring.ai.model.chat=openai") + .withConfiguration(AutoConfigurations.of(OpenAiChatAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(OpenAiChatProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(OpenAiChatModel.class)).isNotEmpty(); @@ -268,16 +264,16 @@ public class OpenAiResponseFormatPropertiesTests { 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)) + "spring.ai.model.image=none") + .withConfiguration(AutoConfigurations.of(OpenAiImageAutoConfiguration.class)) .run(context -> { - assertThat(context.getBeansOfType(OpenAiImageProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OpenAiImageProperties.class)).isEmpty(); 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)) + .withConfiguration(AutoConfigurations.of(OpenAiImageAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(OpenAiImageProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(OpenAiImageModel.class)).isNotEmpty(); @@ -285,8 +281,8 @@ public class OpenAiResponseFormatPropertiesTests { 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)) + "spring.ai.model.image=openai") + .withConfiguration(AutoConfigurations.of(OpenAiImageAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(OpenAiImageProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(OpenAiImageModel.class)).isNotEmpty(); @@ -298,16 +294,16 @@ public class OpenAiResponseFormatPropertiesTests { 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)) + "spring.ai.model.audio.speech=none") + .withConfiguration(AutoConfigurations.of(OpenAiAudioSpeechAutoConfiguration.class)) .run(context -> { - assertThat(context.getBeansOfType(OpenAiAudioSpeechProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioSpeechProperties.class)).isEmpty(); 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)) + .withConfiguration(AutoConfigurations.of(OpenAiAudioSpeechAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(OpenAiAudioSpeechProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isNotEmpty(); @@ -315,8 +311,8 @@ public class OpenAiResponseFormatPropertiesTests { 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)) + "spring.ai.model.audio.speech=openai") + .withConfiguration(AutoConfigurations.of(OpenAiAudioSpeechAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(OpenAiAudioSpeechProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(OpenAiAudioSpeechModel.class)).isNotEmpty(); @@ -328,16 +324,16 @@ public class OpenAiResponseFormatPropertiesTests { 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)) + "spring.ai.model.audio.transcription=none") + .withConfiguration(AutoConfigurations.of(OpenAiAudioTranscriptionAutoConfiguration.class)) .run(context -> { - assertThat(context.getBeansOfType(OpenAiAudioTranscriptionProperties.class)).isNotEmpty(); + assertThat(context.getBeansOfType(OpenAiAudioTranscriptionProperties.class)).isEmpty(); 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)) + .withConfiguration(AutoConfigurations.of(OpenAiAudioTranscriptionAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(OpenAiAudioTranscriptionProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isNotEmpty(); @@ -345,8 +341,8 @@ public class OpenAiResponseFormatPropertiesTests { 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)) + "spring.ai.model.audio.transcription=openai") + .withConfiguration(AutoConfigurations.of(OpenAiAudioTranscriptionAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(OpenAiAudioTranscriptionProperties.class)).isNotEmpty(); assertThat(context.getBeansOfType(OpenAiAudioTranscriptionModel.class)).isNotEmpty(); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackInPrompt2IT.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackInPrompt2IT.java index 8e238be52..23acefa0c 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackInPrompt2IT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackInPrompt2IT.java @@ -25,8 +25,8 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.ai.model.openai.autoconfigure.OpenAiAutoConfiguration; import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration; import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.openai.api.OpenAiApi.ChatModel; import org.springframework.ai.tool.function.FunctionToolCallback; @@ -42,7 +42,7 @@ public class FunctionCallbackInPrompt2IT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY")) - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(OpenAiChatAutoConfiguration.class)); @Test void functionCallTest() { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackInPromptIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackInPromptIT.java index 45596ff5e..c3073a56d 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackInPromptIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackInPromptIT.java @@ -25,12 +25,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; -import org.springframework.ai.model.openai.autoconfigure.OpenAiAutoConfiguration; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.Generation; import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration; import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.openai.api.OpenAiApi.ChatModel; @@ -47,7 +47,7 @@ public class FunctionCallbackInPromptIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withPropertyValues("spring.ai.openai.apiKey=" + System.getenv("OPENAI_API_KEY")) - .withConfiguration(AutoConfigurations.of(OpenAiAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(OpenAiChatAutoConfiguration.class)); @Test void functionCallTest() { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java index b1ddf745d..7216afeae 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/FunctionCallbackWithPlainFunctionBeanIT.java @@ -32,7 +32,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; -import org.springframework.ai.model.openai.autoconfigure.OpenAiAutoConfiguration; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.UserMessage; @@ -40,6 +39,7 @@ 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.openai.autoconfigure.OpenAiChatAutoConfiguration; import org.springframework.ai.model.tool.ToolCallingChatOptions; import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.openai.OpenAiChatOptions; @@ -60,7 +60,7 @@ class FunctionCallbackWithPlainFunctionBeanIT { 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)) + .withConfiguration(AutoConfigurations.of(OpenAiChatAutoConfiguration.class)) .withUserConfiguration(Config.class); private static Map feedback = new ConcurrentHashMap<>(); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/OpenAiFunctionCallback2IT.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/OpenAiFunctionCallback2IT.java index 682a8dacb..bb22b7bfd 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/OpenAiFunctionCallback2IT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/OpenAiFunctionCallback2IT.java @@ -23,8 +23,8 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.ai.model.openai.autoconfigure.OpenAiAutoConfiguration; import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration; import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.openai.api.OpenAiApi.ChatModel; import org.springframework.ai.tool.ToolCallback; @@ -44,7 +44,7 @@ public class OpenAiFunctionCallback2IT { 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)) + .withConfiguration(AutoConfigurations.of(OpenAiChatAutoConfiguration.class)) .withUserConfiguration(Config.class); @Test diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/OpenAiFunctionCallbackIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/OpenAiFunctionCallbackIT.java index 6ab701eb2..67d6d8076 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/OpenAiFunctionCallbackIT.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/java/org/springframework/ai/model/openai/autoconfigure/tool/OpenAiFunctionCallbackIT.java @@ -25,12 +25,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; -import org.springframework.ai.model.openai.autoconfigure.OpenAiAutoConfiguration; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.Generation; import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration; import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.openai.api.OpenAiApi.ChatModel; @@ -51,7 +51,7 @@ public class OpenAiFunctionCallbackIT { 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)) + .withConfiguration(AutoConfigurations.of(OpenAiChatAutoConfiguration.class)) .withUserConfiguration(Config.class); @Test diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/resources/speech/jfk.flac b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/resources/speech/jfk.flac new file mode 100644 index 000000000..e44b7c138 Binary files /dev/null and b/auto-configurations/models/spring-ai-autoconfigure-model-openai/src/test/resources/speech/jfk.flac differ diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/pom.xml index 802627a43..7be055456 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/pom.xml @@ -51,7 +51,7 @@ org.springframework.ai - spring-ai-autoconfigure-observation-embedding + spring-ai-autoconfigure-model-embedding-observation ${project.parent.version} true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/src/main/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/src/main/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlAutoConfiguration.java index ae0092ee8..d35ed9c23 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/src/main/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding/src/main/java/org/springframework/ai/model/postgresml/autoconfigure/PostgresMlAutoConfiguration.java @@ -16,6 +16,8 @@ package org.springframework.ai.model.postgresml.autoconfigure; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; import org.springframework.ai.postgresml.PostgresMlEmbeddingModel; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -39,7 +41,7 @@ public class PostgresMlAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = PostgresMlEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + @ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.POSTGRESML, matchIfMissing = true) public PostgresMlEmbeddingModel postgresMlEmbeddingModel(JdbcTemplate jdbcTemplate, PostgresMlEmbeddingProperties embeddingProperties) { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/pom.xml index d4c03036e..abb2d58da 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/pom.xml @@ -35,13 +35,6 @@ - - org.springframework.ai - spring-ai-autoconfigure-model-chat-client - ${project.parent.version} - true - - org.springframework.ai spring-ai-autoconfigure-retry @@ -51,14 +44,14 @@ org.springframework.ai - spring-ai-autoconfigure-observation-chat + spring-ai-autoconfigure-model-chat-observation ${project.parent.version} true org.springframework.ai - spring-ai-autoconfigure-observation-embedding + spring-ai-autoconfigure-model-embedding-observation ${project.parent.version} true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanAutoConfiguration.java index a48870d9d..98d456ef9 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/main/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanAutoConfiguration.java @@ -18,6 +18,8 @@ package org.springframework.ai.model.qianfan.autoconfigure; import io.micrometer.observation.ObservationRegistry; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.chat.observation.ChatModelObservationConvention; import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; @@ -58,7 +60,7 @@ public class QianFanAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = QianFanChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + @ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.QIANFAN, matchIfMissing = true) public QianFanChatModel qianFanChatModel(QianFanConnectionProperties commonProperties, QianFanChatProperties chatProperties, ObjectProvider restClientBuilderProvider, @@ -81,7 +83,7 @@ public class QianFanAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = QianFanEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + @ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.QIANFAN, matchIfMissing = true) public QianFanEmbeddingModel qianFanEmbeddingModel(QianFanConnectionProperties commonProperties, QianFanEmbeddingProperties embeddingProperties, @@ -105,7 +107,7 @@ public class QianFanAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = QianFanImageProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + @ConditionalOnProperty(name = SpringAIModelProperties.IMAGE_MODEL, havingValue = SpringAIModels.QIANFAN, matchIfMissing = true) public QianFanImageModel qianFanImageModel(QianFanConnectionProperties commonProperties, QianFanImageProperties imageProperties, ObjectProvider restClientBuilderProvider, diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/test/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanPropertiesTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/test/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanPropertiesTests.java index 8d976f971..d10e3b704 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/test/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanPropertiesTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-qianfan/src/test/java/org/springframework/ai/model/qianfan/autoconfigure/QianFanPropertiesTests.java @@ -235,7 +235,7 @@ public class QianFanPropertiesTests { 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") + "spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.model.embedding=none") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)) .run(context -> { @@ -255,7 +255,7 @@ public class QianFanPropertiesTests { 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") + "spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.model.chat=qianfan") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)) .run(context -> { @@ -268,7 +268,7 @@ public class QianFanPropertiesTests { 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") + "spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.model.chat=none") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, QianFanAutoConfiguration.class)) .run(context -> { @@ -402,7 +402,7 @@ public class QianFanPropertiesTests { 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") + "spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.model.image=none") .withConfiguration( AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, WebClientAutoConfiguration.class, QianFanAutoConfiguration.class)) @@ -424,7 +424,7 @@ public class QianFanPropertiesTests { 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") + "spring.ai.qianfan.base-url=TEST_BASE_URL", "spring.ai.model.chat=qianfan") .withConfiguration( AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, WebClientAutoConfiguration.class, QianFanAutoConfiguration.class)) diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/pom.xml index 121ea1264..f73cc84ad 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/pom.xml @@ -35,13 +35,6 @@ - - org.springframework.ai - spring-ai-autoconfigure-model-chat-client - ${project.parent.version} - true - - org.springframework.ai spring-ai-autoconfigure-retry @@ -51,7 +44,7 @@ org.springframework.ai - spring-ai-autoconfigure-observation-image + spring-ai-autoconfigure-model-image-observation ${project.parent.version} true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageAutoConfiguration.java index 7c143a941..c6d83e9cd 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageAutoConfiguration.java @@ -16,6 +16,8 @@ package org.springframework.ai.model.stabilityai.autoconfigure; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; import org.springframework.ai.stabilityai.StabilityAiImageModel; import org.springframework.ai.stabilityai.api.StabilityAiApi; import org.springframework.beans.factory.ObjectProvider; @@ -62,7 +64,7 @@ public class StabilityAiImageAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = StabilityAiImageProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + @ConditionalOnProperty(name = SpringAIModelProperties.IMAGE_MODEL, havingValue = SpringAIModels.STABILITY, matchIfMissing = true) public StabilityAiImageModel stabilityAiImageModel(StabilityAiApi stabilityAiApi, StabilityAiImageProperties stabilityAiImageProperties) { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImagePropertiesTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImagePropertiesTests.java index 74e1624f9..9cbc4e9f1 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImagePropertiesTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImagePropertiesTests.java @@ -77,7 +77,7 @@ public class StabilityAiImagePropertiesTests { new ApplicationContextRunner() .withPropertyValues("spring.ai.stabilityai.image.api-key=API_KEY", - "spring.ai.stabilityai.image.base-url=ENDPOINT", "spring.ai.stabilityai.image.enabled=false") + "spring.ai.stabilityai.image.base-url=ENDPOINT", "spring.ai.model.image=none") .withConfiguration(AutoConfigurations.of(StabilityAiImageAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(StabilityAiImageProperties.class)).isNotEmpty(); @@ -97,7 +97,7 @@ public class StabilityAiImagePropertiesTests { new ApplicationContextRunner() .withPropertyValues("spring.ai.stabilityai.image.api-key=API_KEY", - "spring.ai.stabilityai.image.base-url=ENDPOINT", "spring.ai.stabilityai.image.enabled=true") + "spring.ai.stabilityai.image.base-url=ENDPOINT", "spring.ai.model.image=stabilityai") .withConfiguration(AutoConfigurations.of(StabilityAiImageAutoConfiguration.class)) .run(context -> { assertThat(context.getBeansOfType(StabilityAiImageProperties.class)).isNotEmpty(); diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-transformers/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-transformers/pom.xml index ebc45c2d9..248c9968a 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-transformers/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-transformers/pom.xml @@ -37,7 +37,7 @@ org.springframework.ai - spring-ai-autoconfigure-observation-embedding + spring-ai-autoconfigure-model-embedding-observation ${project.parent.version} true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-transformers/src/main/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-transformers/src/main/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelAutoConfiguration.java index 495428766..d4896a80d 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-transformers/src/main/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-transformers/src/main/java/org/springframework/ai/model/transformers/autoconfigure/TransformersEmbeddingModelAutoConfiguration.java @@ -21,6 +21,8 @@ import ai.onnxruntime.OrtSession; import io.micrometer.observation.ObservationRegistry; import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; import org.springframework.ai.transformers.TransformersEmbeddingModel; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -42,8 +44,8 @@ public class TransformersEmbeddingModelAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = TransformersEmbeddingModelProperties.CONFIG_PREFIX, name = "enabled", - havingValue = "true", matchIfMissing = true) + @ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.TRANSFORMERS, + matchIfMissing = true) public TransformersEmbeddingModel embeddingModel(TransformersEmbeddingModelProperties properties, ObjectProvider observationRegistry, ObjectProvider observationConvention) { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/pom.xml index b48b27df6..c4b091a78 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/pom.xml @@ -44,13 +44,6 @@ - - org.springframework.ai - spring-ai-autoconfigure-model-chat-client - ${project.parent.version} - true - - org.springframework.ai spring-ai-autoconfigure-model-tool @@ -67,21 +60,21 @@ org.springframework.ai - spring-ai-autoconfigure-observation-chat + spring-ai-autoconfigure-model-chat-observation ${project.parent.version} true org.springframework.ai - spring-ai-autoconfigure-observation-embedding + spring-ai-autoconfigure-model-embedding-observation ${project.parent.version} true org.springframework.ai - spring-ai-autoconfigure-observation-image + spring-ai-autoconfigure-model-image-observation ${project.parent.version} true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiEmbeddingAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiEmbeddingAutoConfiguration.java index ec4aa79b5..cd67ccf95 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiEmbeddingAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/embedding/VertexAiEmbeddingAutoConfiguration.java @@ -21,6 +21,8 @@ import java.io.IOException; import com.google.cloud.vertexai.VertexAI; import io.micrometer.observation.ObservationRegistry; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; import org.springframework.ai.vertexai.embedding.VertexAiEmbeddingConnectionDetails; @@ -74,8 +76,8 @@ public class VertexAiEmbeddingAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = VertexAiTextEmbeddingProperties.CONFIG_PREFIX, name = "enabled", - havingValue = "true", matchIfMissing = true) + @ConditionalOnProperty(name = SpringAIModelProperties.TEXT_EMBEDDING_MODEL, havingValue = SpringAIModels.VERTEX_AI, + matchIfMissing = true) public VertexAiTextEmbeddingModel textEmbedding(VertexAiEmbeddingConnectionDetails connectionDetails, VertexAiTextEmbeddingProperties textEmbeddingProperties, RetryTemplate retryTemplate, ObjectProvider observationRegistry, @@ -91,8 +93,8 @@ public class VertexAiEmbeddingAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = VertexAiMultimodalEmbeddingProperties.CONFIG_PREFIX, name = "enabled", - havingValue = "true", matchIfMissing = true) + @ConditionalOnProperty(name = SpringAIModelProperties.MULTI_MODAL_EMBEDDING_MODEL, + havingValue = SpringAIModels.VERTEX_AI, matchIfMissing = true) public VertexAiMultimodalEmbeddingModel multimodalEmbedding(VertexAiEmbeddingConnectionDetails connectionDetails, VertexAiMultimodalEmbeddingProperties multimodalEmbeddingProperties) throws IOException { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiAutoConfiguration.java index 4a059efa2..7a4ceec79 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-vertex-ai/src/main/java/org/springframework/ai/model/vertexai/autoconfigure/gemini/VertexAiGeminiAutoConfiguration.java @@ -22,6 +22,8 @@ import com.google.auth.oauth2.GoogleCredentials; import com.google.cloud.vertexai.VertexAI; import io.micrometer.observation.ObservationRegistry; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration; import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.chat.observation.ChatModelObservationConvention; @@ -85,7 +87,7 @@ public class VertexAiGeminiAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = VertexAiGeminiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + @ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.VERTEX_AI, matchIfMissing = true) public VertexAiGeminiChatModel vertexAiGeminiChat(VertexAI vertexAi, VertexAiGeminiChatProperties chatProperties, ToolCallingManager toolCallingManager, ApplicationContext context, RetryTemplate retryTemplate, diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/pom.xml index ef8166b0d..00e937e6a 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/pom.xml @@ -37,21 +37,14 @@ org.springframework.ai - spring-ai-autoconfigure-model-chat-client + spring-ai-autoconfigure-model-chat-observation ${project.parent.version} true org.springframework.ai - spring-ai-autoconfigure-observation-chat - ${project.parent.version} - true - - - - org.springframework.ai - spring-ai-autoconfigure-observation-embedding + spring-ai-autoconfigure-model-embedding-observation ${project.parent.version} true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiAutoConfiguration.java index fbb20c697..2477069c6 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai/src/main/java/org/springframework/ai/model/watsonxai/autoconfigure/WatsonxAiAutoConfiguration.java @@ -16,6 +16,8 @@ package org.springframework.ai.model.watsonxai.autoconfigure; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; import org.springframework.ai.watsonx.WatsonxAiChatModel; import org.springframework.ai.watsonx.WatsonxAiEmbeddingModel; import org.springframework.ai.watsonx.api.WatsonxAiApi; @@ -41,8 +43,6 @@ import org.springframework.web.client.RestClient; @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 @@ -56,7 +56,7 @@ public class WatsonxAiAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = WatsonxAiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + @ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.WATSONX_AI, matchIfMissing = true) public WatsonxAiChatModel watsonxChatModel(WatsonxAiApi watsonxApi, WatsonxAiChatProperties chatProperties) { return new WatsonxAiChatModel(watsonxApi, chatProperties.getOptions()); @@ -64,7 +64,7 @@ public class WatsonxAiAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = WatsonxAiEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + @ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.WATSONX_AI, matchIfMissing = true) public WatsonxAiEmbeddingModel watsonxAiEmbeddingModel(WatsonxAiApi watsonxApi, WatsonxAiEmbeddingProperties properties) { diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/pom.xml index aa367db5e..61719f3a0 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/pom.xml @@ -35,13 +35,6 @@ - - org.springframework.ai - spring-ai-autoconfigure-model-chat-client - ${project.parent.version} - true - - org.springframework.ai spring-ai-autoconfigure-retry @@ -51,21 +44,21 @@ org.springframework.ai - spring-ai-autoconfigure-observation-chat + spring-ai-autoconfigure-model-chat-observation ${project.parent.version} true org.springframework.ai - spring-ai-autoconfigure-observation-embedding + spring-ai-autoconfigure-model-embedding-observation ${project.parent.version} true org.springframework.ai - spring-ai-autoconfigure-observation-image + spring-ai-autoconfigure-model-image-observation ${project.parent.version} true diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfiguration.java index db27df995..4202ce2ef 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/main/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiAutoConfiguration.java @@ -20,6 +20,8 @@ import java.util.List; import io.micrometer.observation.ObservationRegistry; +import org.springframework.ai.model.SpringAIModelProperties; +import org.springframework.ai.model.SpringAIModels; import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.chat.observation.ChatModelObservationConvention; import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; @@ -59,7 +61,7 @@ public class ZhiPuAiAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = ZhiPuAiChatProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + @ConditionalOnProperty(name = SpringAIModelProperties.CHAT_MODEL, havingValue = SpringAIModels.ZHIPUAI, matchIfMissing = true) public ZhiPuAiChatModel zhiPuAiChatModel(ZhiPuAiConnectionProperties commonProperties, ZhiPuAiChatProperties chatProperties, ObjectProvider restClientBuilderProvider, @@ -82,7 +84,7 @@ public class ZhiPuAiAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = ZhiPuAiEmbeddingProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + @ConditionalOnProperty(name = SpringAIModelProperties.EMBEDDING_MODEL, havingValue = SpringAIModels.ZHIPUAI, matchIfMissing = true) public ZhiPuAiEmbeddingModel zhiPuAiEmbeddingModel(ZhiPuAiConnectionProperties commonProperties, ZhiPuAiEmbeddingProperties embeddingProperties, RestClient.Builder restClientBuilder, @@ -116,7 +118,7 @@ public class ZhiPuAiAutoConfiguration { @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = ZhiPuAiImageProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + @ConditionalOnProperty(name = SpringAIModelProperties.IMAGE_MODEL, havingValue = SpringAIModels.ZHIPUAI, matchIfMissing = true) public ZhiPuAiImageModel zhiPuAiImageModel(ZhiPuAiConnectionProperties commonProperties, ZhiPuAiImageProperties imageProperties, RestClient.Builder restClientBuilder, RetryTemplate retryTemplate, diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiPropertiesTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiPropertiesTests.java index ec1b9387a..79b98dfed 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiPropertiesTests.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-zhipuai/src/test/java/org/springframework/ai/model/zhipuai/autoconfigure/ZhiPuAiPropertiesTests.java @@ -335,7 +335,7 @@ public class ZhiPuAiPropertiesTests { new ApplicationContextRunner() .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL", - "spring.ai.zhipuai.embedding.enabled=false") + "spring.ai.model.embedding=none") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) .run(context -> { @@ -354,7 +354,7 @@ public class ZhiPuAiPropertiesTests { new ApplicationContextRunner() .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL", - "spring.ai.zhipuai.embedding.enabled=true") + "spring.ai.model.embedding=zhipuai") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) .run(context -> { @@ -367,7 +367,7 @@ public class ZhiPuAiPropertiesTests { 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") + "spring.ai.model.chat=none") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) .run(context -> { @@ -386,7 +386,7 @@ public class ZhiPuAiPropertiesTests { new ApplicationContextRunner() .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL", - "spring.ai.zhipuai.chat.enabled=true") + "spring.ai.model.chat=zhipuai") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) .run(context -> { @@ -400,7 +400,7 @@ public class ZhiPuAiPropertiesTests { 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") + "spring.ai.model.image=none") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) .run(context -> { @@ -419,7 +419,7 @@ public class ZhiPuAiPropertiesTests { new ApplicationContextRunner() .withPropertyValues("spring.ai.zhipuai.api-key=API_KEY", "spring.ai.zhipuai.base-url=TEST_BASE_URL", - "spring.ai.zhipuai.image.enabled=true") + "spring.ai.model.image=zhipuai") .withConfiguration(AutoConfigurations.of(SpringAiRetryAutoConfiguration.class, RestClientAutoConfiguration.class, ZhiPuAiAutoConfiguration.class)) .run(context -> { diff --git a/auto-configurations/chat/spring-ai-autoconfigure-model-tool/pom.xml b/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/pom.xml similarity index 97% rename from auto-configurations/chat/spring-ai-autoconfigure-model-tool/pom.xml rename to auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/pom.xml index ffdff50b4..6eebc504d 100644 --- a/auto-configurations/chat/spring-ai-autoconfigure-model-tool/pom.xml +++ b/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/pom.xml @@ -7,7 +7,7 @@ org.springframework.ai spring-ai 1.0.0-SNAPSHOT - ../../../pom.xml + ../../../../pom.xml spring-ai-autoconfigure-model-tool jar diff --git a/auto-configurations/chat/spring-ai-autoconfigure-model-tool/src/main/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfiguration.java b/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/main/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfiguration.java similarity index 100% rename from auto-configurations/chat/spring-ai-autoconfigure-model-tool/src/main/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfiguration.java rename to auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/main/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfiguration.java diff --git a/auto-configurations/chat/spring-ai-autoconfigure-model-tool/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports similarity index 100% rename from auto-configurations/chat/spring-ai-autoconfigure-model-tool/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports rename to auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports diff --git a/auto-configurations/chat/spring-ai-autoconfigure-model-tool/src/test/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfigurationTests.java b/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/test/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfigurationTests.java similarity index 100% rename from auto-configurations/chat/spring-ai-autoconfigure-model-tool/src/test/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfigurationTests.java rename to auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/test/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfigurationTests.java diff --git a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-elasticsearch/src/test/java/org/springframework/ai/vectorstore/elasticsearch/autoconfigure/ElasticsearchVectorStoreAutoConfigurationIT.java b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-elasticsearch/src/test/java/org/springframework/ai/vectorstore/elasticsearch/autoconfigure/ElasticsearchVectorStoreAutoConfigurationIT.java index 565eb6f2a..94395dcb8 100644 --- a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-elasticsearch/src/test/java/org/springframework/ai/vectorstore/elasticsearch/autoconfigure/ElasticsearchVectorStoreAutoConfigurationIT.java +++ b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-elasticsearch/src/test/java/org/springframework/ai/vectorstore/elasticsearch/autoconfigure/ElasticsearchVectorStoreAutoConfigurationIT.java @@ -29,7 +29,7 @@ import org.testcontainers.elasticsearch.ElasticsearchContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; -import org.springframework.ai.model.openai.autoconfigure.OpenAiAutoConfiguration; +import org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration; import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.document.Document; import org.springframework.ai.observation.conventions.VectorStoreProvider; @@ -61,7 +61,7 @@ class ElasticsearchVectorStoreAutoConfigurationIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(ElasticsearchRestClientAutoConfiguration.class, ElasticsearchVectorStoreAutoConfiguration.class, RestClientAutoConfiguration.class, - SpringAiRetryAutoConfiguration.class, OpenAiAutoConfiguration.class)) + SpringAiRetryAutoConfiguration.class, OpenAiEmbeddingAutoConfiguration.class)) .withUserConfiguration(Config.class) .withPropertyValues("spring.elasticsearch.uris=" + elasticsearchContainer.getHttpHostAddress(), "spring.ai.vectorstore.elasticsearch.initializeSchema=true", @@ -129,7 +129,7 @@ class ElasticsearchVectorStoreAutoConfigurationIT { new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(ElasticsearchRestClientAutoConfiguration.class, ElasticsearchVectorStoreAutoConfiguration.class, RestClientAutoConfiguration.class, - SpringAiRetryAutoConfiguration.class, OpenAiAutoConfiguration.class)) + SpringAiRetryAutoConfiguration.class, OpenAiEmbeddingAutoConfiguration.class)) .withPropertyValues("spring.elasticsearch.uris=" + elasticsearchContainer.getHttpHostAddress(), "spring.ai.openai.api-key=" + System.getenv("OPENAI_API_KEY"), "spring.ai.vectorstore.elasticsearch.index-name=example", diff --git a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-hanadb/src/test/java/org/springframework/ai/vectorstore/hanadb/autoconfigure/HanaCloudVectorStoreAutoConfigurationIT.java b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-hanadb/src/test/java/org/springframework/ai/vectorstore/hanadb/autoconfigure/HanaCloudVectorStoreAutoConfigurationIT.java index 9699aa6a1..c407fe7cf 100644 --- a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-hanadb/src/test/java/org/springframework/ai/vectorstore/hanadb/autoconfigure/HanaCloudVectorStoreAutoConfigurationIT.java +++ b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-hanadb/src/test/java/org/springframework/ai/vectorstore/hanadb/autoconfigure/HanaCloudVectorStoreAutoConfigurationIT.java @@ -23,7 +23,7 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.springframework.ai.model.openai.autoconfigure.OpenAiAutoConfiguration; +import org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration; import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.document.Document; import org.springframework.ai.vectorstore.VectorStore; @@ -41,8 +41,8 @@ public class HanaCloudVectorStoreAutoConfigurationIT { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(HanaCloudVectorStoreAutoConfiguration.class, - OpenAiAutoConfiguration.class, RestClientAutoConfiguration.class, SpringAiRetryAutoConfiguration.class, - JdbcRepositoriesAutoConfiguration.class)) + OpenAiEmbeddingAutoConfiguration.class, RestClientAutoConfiguration.class, + SpringAiRetryAutoConfiguration.class, JdbcRepositoriesAutoConfiguration.class)) .withPropertyValues("spring.ai.openai.api-key=" + System.getenv("OPENAI_API_KEY"), "spring.ai.openai.embedding.options.model=text-embedding-ada-002", "spring.datasource.url=" + System.getenv("HANA_DATASOURCE_URL"), diff --git a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-mongodb-atlas/src/test/java/org/springframework/ai/vectorstore/mongodb/autoconfigure/MongoDBAtlasVectorStoreAutoConfigurationIT.java b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-mongodb-atlas/src/test/java/org/springframework/ai/vectorstore/mongodb/autoconfigure/MongoDBAtlasVectorStoreAutoConfigurationIT.java index 6ff225d2e..cdbdd8481 100644 --- a/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-mongodb-atlas/src/test/java/org/springframework/ai/vectorstore/mongodb/autoconfigure/MongoDBAtlasVectorStoreAutoConfigurationIT.java +++ b/auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-mongodb-atlas/src/test/java/org/springframework/ai/vectorstore/mongodb/autoconfigure/MongoDBAtlasVectorStoreAutoConfigurationIT.java @@ -27,7 +27,7 @@ import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.mongodb.MongoDBAtlasLocalContainer; -import org.springframework.ai.model.openai.autoconfigure.OpenAiAutoConfiguration; +import org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration; import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration; import org.springframework.ai.document.Document; import org.springframework.ai.observation.conventions.VectorStoreProvider; @@ -64,7 +64,7 @@ class MongoDBAtlasVectorStoreAutoConfigurationIT { .withUserConfiguration(Config.class) .withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class, MongoDataAutoConfiguration.class, MongoDBAtlasVectorStoreAutoConfiguration.class, RestClientAutoConfiguration.class, - SpringAiRetryAutoConfiguration.class, OpenAiAutoConfiguration.class)) + SpringAiRetryAutoConfiguration.class, OpenAiEmbeddingAutoConfiguration.class)) .withPropertyValues("spring.data.mongodb.database=springaisample", "spring.ai.vectorstore.mongodb.initialize-schema=true", "spring.ai.vectorstore.mongodb.collection-name=test_collection", diff --git a/pom.xml b/pom.xml index 1de797edb..6643a452d 100644 --- a/pom.xml +++ b/pom.xml @@ -37,20 +37,26 @@ auto-configurations/common/spring-ai-autoconfigure-retry - auto-configurations/chat/spring-ai-autoconfigure-model-chat-client - auto-configurations/chat/spring-ai-autoconfigure-model-tool - auto-configurations/chat/spring-ai-autoconfigure-model-chat-memory + auto-configurations/models/tool/spring-ai-autoconfigure-model-tool + + auto-configurations/models/chat/client/spring-ai-autoconfigure-model-chat-client + auto-configurations/models/chat/memory/spring-ai-autoconfigure-model-chat-memory + auto-configurations/models/chat/observation/spring-ai-autoconfigure-model-chat-observation + + auto-configurations/models/embedding/observation/spring-ai-autoconfigure-model-embedding-observation + auto-configurations/models/image/observation/spring-ai-autoconfigure-model-image-observation auto-configurations/models/spring-ai-autoconfigure-model-anthropic auto-configurations/models/spring-ai-autoconfigure-model-azure-openai auto-configurations/models/spring-ai-autoconfigure-model-bedrock-ai auto-configurations/models/spring-ai-autoconfigure-model-huggingface + auto-configurations/models/spring-ai-autoconfigure-model-openai auto-configurations/models/spring-ai-autoconfigure-model-minimax auto-configurations/models/spring-ai-autoconfigure-model-mistral-ai auto-configurations/models/spring-ai-autoconfigure-model-moonshot auto-configurations/models/spring-ai-autoconfigure-model-oci-genai auto-configurations/models/spring-ai-autoconfigure-model-ollama - auto-configurations/models/spring-ai-autoconfigure-model-openai + auto-configurations/models/spring-ai-autoconfigure-model-postgresml-embedding auto-configurations/models/spring-ai-autoconfigure-model-qianfan auto-configurations/models/spring-ai-autoconfigure-model-stability-ai @@ -59,9 +65,7 @@ auto-configurations/models/spring-ai-autoconfigure-model-watsonx-ai auto-configurations/models/spring-ai-autoconfigure-model-zhipuai - auto-configurations/observation/chat/spring-ai-autoconfigure-observation-chat - auto-configurations/observation/embedding/spring-ai-autoconfigure-observation-embedding - auto-configurations/observation/image/spring-ai-autoconfigure-observation-image + auto-configurations/mcp/spring-ai-autoconfigure-mcp-client auto-configurations/mcp/spring-ai-autoconfigure-mcp-server diff --git a/spring-ai-core/src/main/java/org/springframework/ai/model/SpringAIModelProperties.java b/spring-ai-core/src/main/java/org/springframework/ai/model/SpringAIModelProperties.java new file mode 100644 index 000000000..c1fb667e9 --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/model/SpringAIModelProperties.java @@ -0,0 +1,39 @@ +/* + * 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. + */ + +package org.springframework.ai.model; + +public class SpringAIModelProperties { + + public static final String MODEL_PREFIX = "spring.ai.model"; + + public static final String CHAT_MODEL = MODEL_PREFIX + ".chat"; + + public static final String EMBEDDING_MODEL = MODEL_PREFIX + ".embedding"; + + public static final String TEXT_EMBEDDING_MODEL = MODEL_PREFIX + ".text.embedding"; + + public static final String MULTI_MODAL_EMBEDDING_MODEL = MODEL_PREFIX + ".multi-modal.embedding"; + + public static final String IMAGE_MODEL = MODEL_PREFIX + ".image"; + + public static final String AUDIO_TRANSCRIPTION_MODEL = MODEL_PREFIX + ".audio.transcription"; + + public static final String AUDIO_SPEECH_MODEL = MODEL_PREFIX + ".audio.speech"; + + public static final String MODERATION_MODEL = MODEL_PREFIX + ".moderation"; + +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/model/SpringAIModels.java b/spring-ai-core/src/main/java/org/springframework/ai/model/SpringAIModels.java new file mode 100644 index 000000000..f937d7671 --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/model/SpringAIModels.java @@ -0,0 +1,59 @@ +/* + * 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. + */ + +package org.springframework.ai.model; + +public class SpringAIModels { + + public static final String ANTHROPIC = "anthropic"; + + public static final String AZURE_OPENAI = "azure-openai"; + + public static final String BEDROCK_COHERE = "bedrock-cohere"; + + public static final String BEDROCK_CONVERSE = "bedrock-converse"; + + public static final String BEDROCK_TITAN = "bedrock-titan"; + + public static final String HUGGINGFACE = "huggingface"; + + public static final String MINIMAX = "minimax"; + + public static final String MISTRAL = "mistral"; + + public static final String MOONSHOT = "moonshot"; + + public static final String OCI_GENAI = "oci-genai"; + + public static final String OLLAMA = "ollama"; + + public static final String OPENAI = "openai"; + + public static final String POSTGRESML = "postgresml"; + + public static final String QIANFAN = "qianfan"; + + public static final String STABILITY = "stabilityai"; + + public static final String TRANSFORMERS = "transformers"; + + public static final String VERTEX_AI = "vertex"; + + public static final String WATSONX_AI = "watsonx"; + + public static final String ZHIPUAI = "zhipuai"; + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/ChatObservationAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/ChatObservationAutoConfiguration.java new file mode 100644 index 000000000..81aee1992 --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/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.model.chat.observation.autoconfigure; + +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/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/ChatObservationProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/ChatObservationProperties.java new file mode 100644 index 000000000..be37d1b26 --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/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.model.chat.observation.autoconfigure; + +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/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/package-info.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/package-info.java new file mode 100644 index 000000000..66bef736b --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/chat/observation/autoconfigure/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.model.chat.observation.autoconfigure; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/embedding/observation/autoconfigure/EmbeddingObservationAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/embedding/observation/autoconfigure/EmbeddingObservationAutoConfiguration.java new file mode 100644 index 000000000..ee0902343 --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/embedding/observation/autoconfigure/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.model.embedding.observation.autoconfigure; + +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/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/embedding/observation/autoconfigure/package-info.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/embedding/observation/autoconfigure/package-info.java new file mode 100644 index 000000000..654b59b6f --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/embedding/observation/autoconfigure/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.model.embedding.observation.autoconfigure; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/ImageObservationAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/ImageObservationAutoConfiguration.java new file mode 100644 index 000000000..f97e12f03 --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/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.model.image.observation.autoconfigure; + +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/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/ImageObservationProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/ImageObservationProperties.java new file mode 100644 index 000000000..7d372d986 --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/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.model.image.observation.autoconfigure; + +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/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/package-info.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/package-info.java new file mode 100644 index 000000000..983cc5a16 --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/model/image/observation/autoconfigure/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.model.image.observation.autoconfigure; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index c31d7de6e..7cb623cc6 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-ai-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -13,8 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # - -org.springframework.ai.model.openai.autoconfigure.OpenAiAutoConfiguration org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiAutoConfiguration org.springframework.ai.model.oci.genai.autoconfigure.OCIGenAiAutoConfiguration org.springframework.ai.model.stabilityai.autoconfigure.StabilityAiImageAutoConfiguration @@ -24,9 +22,9 @@ gemini.org.springframework.ai.model.vertexai.autoconfigure.VertexAiGeminiAutoCon cohere.org.springframework.ai.model.bedrock.autoconfigure.BedrockCohereEmbeddingAutoConfiguration titan.org.springframework.ai.model.bedrock.autoconfigure.BedrockTitanEmbeddingAutoConfiguration converse.org.springframework.ai.model.bedrock.autoconfigure.BedrockConverseProxyChatAutoConfiguration -org.springframework.ai.autoconfigure.chat.observation.ChatObservationAutoConfiguration -org.springframework.ai.autoconfigure.embedding.observation.EmbeddingObservationAutoConfiguration -org.springframework.ai.autoconfigure.image.observation.ImageObservationAutoConfiguration +org.springframework.ai.model.chat.observation.autoconfigure.ChatObservationAutoConfiguration +org.springframework.ai.model.embedding.observation.autoconfigure.EmbeddingObservationAutoConfiguration +org.springframework.ai.model.image.observation.autoconfigure.ImageObservationAutoConfiguration org.springframework.ai.model.ollama.autoconfigure.OllamaAutoConfiguration org.springframework.ai.model.mistralai.autoconfigure.MistralAiAutoConfiguration org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/observation/autoconfigure/ChatObservationAutoConfigurationTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/observation/autoconfigure/ChatObservationAutoConfigurationTests.java new file mode 100644 index 000000000..3ac3f12ca --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/chat/observation/autoconfigure/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.model.chat.observation.autoconfigure; + +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/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/embedding/observation/autoconfigure/EmbeddingObservationAutoConfigurationTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/embedding/observation/autoconfigure/EmbeddingObservationAutoConfigurationTests.java new file mode 100644 index 000000000..10c9feb7c --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/embedding/observation/autoconfigure/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.model.embedding.observation.autoconfigure; + +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/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/image/observation/autoconfigure/ImageObservationAutoConfigurationTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/image/observation/autoconfigure/ImageObservationAutoConfigurationTests.java new file mode 100644 index 000000000..23417b67b --- /dev/null +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/model/image/observation/autoconfigure/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.model.image.observation.autoconfigure; + +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)); + } + +}