diff --git a/contrib/spring-ai/pom.xml b/contrib/spring-ai/pom.xml index 2a6b1143b..27e0e4ff0 100644 --- a/contrib/spring-ai/pom.xml +++ b/contrib/spring-ai/pom.xml @@ -122,6 +122,16 @@ spring-ai-openai test + + org.springframework.ai + spring-ai-autoconfigure-model-openai + test + + + org.springframework.ai + spring-ai-autoconfigure-model-tool + test + org.springframework.ai spring-ai-anthropic diff --git a/contrib/spring-ai/src/main/java/com/google/adk/models/springai/autoconfigure/SpringAIAutoConfiguration.java b/contrib/spring-ai/src/main/java/com/google/adk/models/springai/autoconfigure/SpringAIAutoConfiguration.java index 7a312ca88..314b8f918 100644 --- a/contrib/spring-ai/src/main/java/com/google/adk/models/springai/autoconfigure/SpringAIAutoConfiguration.java +++ b/contrib/spring-ai/src/main/java/com/google/adk/models/springai/autoconfigure/SpringAIAutoConfiguration.java @@ -58,7 +58,27 @@ * adk.spring-ai.validation.enabled=true * */ -@AutoConfiguration +@AutoConfiguration( + afterName = { + "org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatAutoConfiguration", + "org.springframework.ai.model.bedrock.converse.autoconfigure.BedrockConverseProxyChatAutoConfiguration", + "org.springframework.ai.model.deepseek.autoconfigure.DeepSeekChatAutoConfiguration", + "org.springframework.ai.model.google.genai.autoconfigure.chat.GoogleGenAiChatAutoConfiguration", + "org.springframework.ai.model.mistralai.autoconfigure.MistralAiChatAutoConfiguration", + "org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration", + "org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration", + "org.springframework.ai.model.bedrock.cohere.autoconfigure.BedrockCohereEmbeddingAutoConfiguration", + "org.springframework.ai.model.bedrock.titan.autoconfigure.BedrockTitanEmbeddingAutoConfiguration", + "org.springframework.ai.model.google.genai.autoconfigure.embedding.GoogleGenAiTextEmbeddingAutoConfiguration", + "org.springframework.ai.model.mistralai.autoconfigure.MistralAiEmbeddingAutoConfiguration", + "org.springframework.ai.model.ollama.autoconfigure.OllamaEmbeddingAutoConfiguration", + "org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration", + "org.springframework.ai.model.postgresml.autoconfigure.PostgresMlEmbeddingAutoConfiguration", + "org.springframework.ai.model.transformers.autoconfigure.TransformersEmbeddingModelAutoConfiguration", + "org.springframework.ai.model.vertexai.autoconfigure.embedding.VertexAiEmbeddingConnectionAutoConfiguration", + "org.springframework.ai.model.vertexai.autoconfigure.embedding.VertexAiTextEmbeddingAutoConfiguration", + "org.springframework.ai.model.vertexai.autoconfigure.embedding.VertexAiMultiModalEmbeddingAutoConfiguration", + }) @ConditionalOnClass({SpringAI.class, ChatModel.class}) @ConditionalOnProperty( prefix = "adk.spring-ai.auto-configuration", diff --git a/contrib/spring-ai/src/test/java/com/google/adk/models/springai/autoconfigure/SpringAIAutoConfigurationOrderingTest.java b/contrib/spring-ai/src/test/java/com/google/adk/models/springai/autoconfigure/SpringAIAutoConfigurationOrderingTest.java new file mode 100644 index 000000000..d1ff29267 --- /dev/null +++ b/contrib/spring-ai/src/test/java/com/google/adk/models/springai/autoconfigure/SpringAIAutoConfigurationOrderingTest.java @@ -0,0 +1,75 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.adk.models.springai.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.adk.models.springai.SpringAI; +import com.google.adk.models.springai.SpringAIEmbedding; +import org.junit.jupiter.api.Test; +import org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration; +import org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration; +import org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +/** + * Verifies that {@link SpringAIAutoConfiguration} is ordered after the Spring AI model + * auto-configurations. + * + *

{@link org.springframework.boot.autoconfigure.condition.ConditionalOnBean} conditions are + * evaluated when the configuration is processed, so {@code @ConditionalOnBean(ChatModel)} on this + * auto-configuration only sees the model beans if the provider auto-configurations ran first. In a + * real application the configurations are otherwise sorted alphabetically, which puts {@code + * com.google.adk...} before {@code org.springframework.ai...} and makes every SpringAI bean + * silently disappear (see issue #1501). + */ +class SpringAIAutoConfigurationOrderingTest { + + private final ApplicationContextRunner contextRunner = + new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of( + SpringAIAutoConfiguration.class, + OpenAiChatAutoConfiguration.class, + OpenAiEmbeddingAutoConfiguration.class, + ToolCallingAutoConfiguration.class)); + + @Test + void springAIBeansAreCreatedWhenModelsComeFromSpringAIModelAutoConfigurations() { + contextRunner + .withPropertyValues( + "spring.ai.openai.api-key=dummy-key", "adk.spring-ai.validation.enabled=false") + .run( + context -> { + assertThat(context).hasSingleBean(SpringAI.class); + assertThat(context).hasSingleBean(SpringAIEmbedding.class); + }); + } + + @Test + void disabledAutoConfigurationStillBacksOff() { + contextRunner + .withPropertyValues( + "spring.ai.openai.api-key=dummy-key", "adk.spring-ai.auto-configuration.enabled=false") + .run( + context -> { + assertThat(context).doesNotHaveBean(SpringAI.class); + assertThat(context).doesNotHaveBean(SpringAIEmbedding.class); + assertThat(context).hasSingleBean(org.springframework.ai.chat.model.ChatModel.class); + }); + } +}