Refactor autoconfigurations for models

- Conditionally enable invidividual models within the provider
     - Introduce top level properties to support conditional logic
        - The properties will have the format like "spring.ai.model.<chat/embedding/image etc.,>=<provider>" to enable specific chat/embedding/image/audio/moderation models by the provider. By default, these will be enabled when no specific properties are set. To disable, set any value other than the provider name for example, "none"

    - For the auto configurations where the provider has multiple models, split the autoconfiguration into per model auto-configuration classes. This will enable the isolated auto-configurations for each provider and its model.
      - This PR addresses this for OpenAI and others will follow in subsequent PRs

Signed-off-by: Ilayaperumal Gopinathan <ilayaperumal.gopinathan@broadcom.com>
This commit is contained in:
Ilayaperumal Gopinathan
2025-03-10 07:21:23 +00:00
committed by Soby Chacko
parent 34c19796d6
commit 431a7eda49
132 changed files with 3692 additions and 733 deletions

View File

@@ -9,7 +9,7 @@
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<artifactId>spring-ai-mcp-client-spring-boot-autoconfigure</artifactId>
<artifactId>spring-ai-autoconfigure-mcp-client</artifactId>
<packaging>jar</packaging>
<name>Spring AI MCP Client Auto Configuration</name>
<description>Spring AI MCP Client Auto Configuration</description>

View File

@@ -9,7 +9,7 @@
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<artifactId>spring-ai-mcp-server-spring-boot-autoconfigure</artifactId>
<artifactId>spring-ai-autoconfigure-mcp-server</artifactId>
<packaging>jar</packaging>
<name>Spring AI MCP Server Auto Configuration</name>
<description>Spring AI MCP Server Auto Configuration</description>

View File

@@ -7,7 +7,7 @@
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
<relativePath>../../../../../pom.xml</relativePath>
</parent>
<artifactId>spring-ai-autoconfigure-model-chat-client</artifactId>
<packaging>jar</packaging>

View File

@@ -7,7 +7,7 @@
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath>
<relativePath>../../../../../pom.xml</relativePath>
</parent>
<artifactId>spring-ai-autoconfigure-model-chat-memory</artifactId>
<packaging>jar</packaging>

View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../../../../pom.xml</relativePath>
</parent>
<artifactId>spring-ai-autoconfigure-model-chat-observation</artifactId>
<packaging>jar</packaging>
<name>Spring AI Chat Observation Auto Configuration</name>
<description>Spring AI Chat Observation Auto Configuration</description>
<url>https://github.com/spring-projects/spring-ai</url>
<scm>
<url>https://github.com/spring-projects/spring-ai</url>
<connection>git://github.com/spring-projects/spring-ai.git</connection>
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
</scm>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-core</artifactId>
<version>${parent.version}</version>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
<optional>true</optional>
</dependency>
<!-- Boot dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-test</artifactId>
<version>${project.parent.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -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> 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));
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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));
}
}

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../../../../pom.xml</relativePath>
</parent>
<artifactId>spring-ai-autoconfigure-model-embedding-observation</artifactId>
<packaging>jar</packaging>
<name>Spring AI Embedding Observation Auto Configuration</name>
<description>Spring AI Embedding Observation Auto Configuration</description>
<url>https://github.com/spring-projects/spring-ai</url>
<scm>
<url>https://github.com/spring-projects/spring-ai</url>
<connection>git://github.com/spring-projects/spring-ai.git</connection>
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
</scm>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-core</artifactId>
<version>${parent.version}</version>
</dependency>
<!-- Boot dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-test</artifactId>
<version>${project.parent.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -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> meterRegistry) {
return new EmbeddingModelMeterObservationHandler(meterRegistry.getObject());
}
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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));
}
}

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../../../../../pom.xml</relativePath>
</parent>
<artifactId>spring-ai-autoconfigure-model-image-observation</artifactId>
<packaging>jar</packaging>
<name>Spring AI Image Observation Auto Configuration</name>
<description>Spring AI Image Observation Auto Configuration</description>
<url>https://github.com/spring-projects/spring-ai</url>
<scm>
<url>https://github.com/spring-projects/spring-ai</url>
<connection>git://github.com/spring-projects/spring-ai.git</connection>
<developerConnection>git@github.com:spring-projects/spring-ai.git</developerConnection>
</scm>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-core</artifactId>
<version>${parent.version}</version>
</dependency>
<!-- Boot dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-test</artifactId>
<version>${project.parent.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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));
}
}

View File

@@ -35,13 +35,6 @@
<!-- Spring AI auto configurations -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-client</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-tool</artifactId>
@@ -58,21 +51,7 @@
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-observation-chat</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-observation-embedding</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-observation-image</artifactId>
<artifactId>spring-ai-autoconfigure-model-chat-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>

View File

@@ -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> observationRegistry,

View File

@@ -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();
});
}
}

View File

@@ -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();
});
}

View File

@@ -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);

View File

@@ -35,13 +35,6 @@
<!-- Spring AI auto configurations -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-client</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-tool</artifactId>
@@ -58,21 +51,21 @@
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-observation-chat</artifactId>
<artifactId>spring-ai-autoconfigure-model-chat-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-observation-embedding</artifactId>
<artifactId>spring-ai-autoconfigure-model-embedding-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-observation-image</artifactId>
<artifactId>spring-ai-autoconfigure-model-image-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>

View File

@@ -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> observationRegistry,
ObjectProvider<EmbeddingModelObservationConvention> 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;
}
}

View File

@@ -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();
});
}
}

View File

@@ -42,13 +42,6 @@
<!-- Spring AI auto configurations -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-client</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-tool</artifactId>
@@ -65,21 +58,21 @@
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-observation-chat</artifactId>
<artifactId>spring-ai-autoconfigure-model-chat-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-observation-embedding</artifactId>
<artifactId>spring-ai-autoconfigure-model-embedding-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-observation-image</artifactId>
<artifactId>spring-ai-autoconfigure-model-image-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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());
}

View File

@@ -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();

View File

@@ -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();
});
}
}

View File

@@ -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();
});
}
}

View File

@@ -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();
});
}
}

View File

@@ -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();

View File

@@ -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();
});
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -37,14 +37,7 @@
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-client</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-observation-chat</artifactId>
<artifactId>spring-ai-autoconfigure-model-chat-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>

View File

@@ -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());

View File

@@ -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();
});
}
}

View File

@@ -35,13 +35,6 @@
<!-- Spring AI auto configurations -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-client</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-tool</artifactId>
@@ -58,14 +51,14 @@
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-observation-chat</artifactId>
<artifactId>spring-ai-autoconfigure-model-chat-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-observation-embedding</artifactId>
<artifactId>spring-ai-autoconfigure-model-embedding-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>

View File

@@ -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<RestClient.Builder> 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,

View File

@@ -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 -> {

View File

@@ -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();
});
}
}

View File

@@ -35,13 +35,6 @@
<!-- Spring AI auto configurations -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-client</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-tool</artifactId>
@@ -58,21 +51,21 @@
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-observation-chat</artifactId>
<artifactId>spring-ai-autoconfigure-model-chat-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-observation-embedding</artifactId>
<artifactId>spring-ai-autoconfigure-model-embedding-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-observation-image</artifactId>
<artifactId>spring-ai-autoconfigure-model-image-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>

View File

@@ -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<RestClient.Builder> restClientBuilderProvider,

View File

@@ -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;

View File

@@ -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();
});
}
}

View File

@@ -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

View File

@@ -35,13 +35,6 @@
<!-- Spring AI auto configurations -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-client</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-retry</artifactId>
@@ -51,14 +44,14 @@
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-observation-chat</artifactId>
<artifactId>spring-ai-autoconfigure-model-chat-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-observation-embedding</artifactId>
<artifactId>spring-ai-autoconfigure-model-embedding-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>

View File

@@ -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<RestClient.Builder> restClientBuilderProvider,

View File

@@ -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 -> {

View File

@@ -35,13 +35,6 @@
<!-- Spring AI auto configurations -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-client</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-retry</artifactId>
@@ -51,14 +44,14 @@
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-observation-chat</artifactId>
<artifactId>spring-ai-autoconfigure-model-chat-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-observation-embedding</artifactId>
<artifactId>spring-ai-autoconfigure-model-embedding-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>

View File

@@ -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> observationRegistry,

View File

@@ -34,14 +34,6 @@
</dependency>
<!-- Spring AI auto configurations -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-client</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-tool</artifactId>
@@ -51,21 +43,21 @@
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-observation-chat</artifactId>
<artifactId>spring-ai-autoconfigure-model-chat-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-observation-embedding</artifactId>
<artifactId>spring-ai-autoconfigure-model-embedding-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-observation-image</artifactId>
<artifactId>spring-ai-autoconfigure-model-image-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>

View File

@@ -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> observationRegistry,

View File

@@ -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();
});
}
}

View File

@@ -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<String, List<String>> 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<String, String> headers) {
}
}

View File

@@ -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<RestClient.Builder> restClientBuilderProvider,
ObjectProvider<WebClient.Builder> 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());
}
}

View File

@@ -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<RestClient.Builder> restClientBuilderProvider,
ObjectProvider<WebClient.Builder> 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);
}
}

View File

@@ -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<String, List<String>> 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<RestClient.Builder> restClientBuilderProvider,
ObjectProvider<WebClient.Builder> webClientBuilderProvider, ToolCallingManager toolCallingManager,
RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler,
ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<ChatModelObservationConvention> 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<RestClient.Builder> restClientBuilderProvider,
ObjectProvider<WebClient.Builder> webClientBuilderProvider, RetryTemplate retryTemplate,
ResponseErrorHandler responseErrorHandler, ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<EmbeddingModelObservationConvention> 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<RestClient.Builder> restClientBuilderProvider,
RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler,
ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<ImageModelObservationConvention> 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<RestClient.Builder> restClientBuilderProvider,
ObjectProvider<WebClient.Builder> 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<RestClient.Builder> 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<RestClient.Builder> restClientBuilderProvider,
ObjectProvider<WebClient.Builder> 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<String, String> headers) {
}
}

View File

@@ -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<RestClient.Builder> restClientBuilderProvider,
ObjectProvider<WebClient.Builder> webClientBuilderProvider, ToolCallingManager toolCallingManager,
RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler,
ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<ChatModelObservationConvention> 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;
}
}

View File

@@ -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<RestClient.Builder> restClientBuilderProvider,
ObjectProvider<WebClient.Builder> webClientBuilderProvider, RetryTemplate retryTemplate,
ResponseErrorHandler responseErrorHandler, ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<EmbeddingModelObservationConvention> 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();
}
}

View File

@@ -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<RestClient.Builder> restClientBuilderProvider,
RetryTemplate retryTemplate, ResponseErrorHandler responseErrorHandler,
ObjectProvider<ObservationRegistry> observationRegistry,
ObjectProvider<ImageModelObservationConvention> 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;
}
}

View File

@@ -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<RestClient.Builder> 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());
}
}

View File

@@ -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

View File

@@ -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() {

View File

@@ -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<ChatResponse> 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<ChatResponse> responseFlux = chatModel.stream(new Prompt(new UserMessage("Hello")));
Flux<ChatResponse> 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"));

View File

@@ -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();
});
}
}

View File

@@ -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();

View File

@@ -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();

View File

@@ -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() {

View File

@@ -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() {

View File

@@ -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<String, Object> feedback = new ConcurrentHashMap<>();

View File

@@ -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

View File

@@ -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

View File

@@ -51,7 +51,7 @@
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-observation-embedding</artifactId>
<artifactId>spring-ai-autoconfigure-model-embedding-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>

View File

@@ -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) {

View File

@@ -35,13 +35,6 @@
<!-- Spring AI auto configurations -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-client</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-retry</artifactId>
@@ -51,14 +44,14 @@
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-observation-chat</artifactId>
<artifactId>spring-ai-autoconfigure-model-chat-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-observation-embedding</artifactId>
<artifactId>spring-ai-autoconfigure-model-embedding-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>

View File

@@ -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<RestClient.Builder> 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<RestClient.Builder> restClientBuilderProvider,

View File

@@ -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))

View File

@@ -35,13 +35,6 @@
<!-- Spring AI auto configurations -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-chat-client</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-retry</artifactId>
@@ -51,7 +44,7 @@
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-observation-image</artifactId>
<artifactId>spring-ai-autoconfigure-model-image-observation</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>

View File

@@ -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) {

View File

@@ -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();

Some files were not shown because too many files have changed in this diff Show More