Repository navigation
fix(spring-ai): declare ordering for SpringAIAutoConfiguration so @ConditionalOnBean matches Spring AI model beans - #1502
Conversation
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
7d52df7 to
4c434a6
Compare
|
Hi @kongxubihai, thank you for your contribution. We appreciate you taking the time to submit this pull request. Currently this PR is under review by our team and we will keep you posted if any additional information is required. thank you. |
There was a problem hiding this comment.
The diagnosis looks right and this fixes #1501 for apps with a single provider, though I have two concerns before merging:
- Apps with multiple providers now fail at startup.
@ConditionalOnBean(ChatModel.class)also matches when severalChatModelbeans are present. Previously, the guards ran before the provider auto-configurations, so an app with two provider starters (e.g. OpenAI and Ollama) started without ADK's beans. NowspringAIWithBothModelsfails because it cannot pick a model, andspringAIEmbeddingfails the same way with twoEmbeddingModelbeans, even when the app defines its ownSpringAIbean.- Would
@ConditionalOnSingleCandidateon the four bean methods make this safer? It matches only when there is a single candidate (or one@Primary), so these apps would back off as before. A test with two mockChatModelbeans could cover this.
- Would
- Providers missing from
afterNamestill hit #1501. Any chat or embedding auto-configuration that isn't listed and sorts aftercom.google.adk...is still processed after this class (e.g. a third-party starter or a provider added in a later Spring AI release).- Would adding
@AutoConfigureOrder(Ordered.LOWEST_PRECEDENCE)alongsideafterNamehelp as a fallback? On its own it wouldn't be enough: a config withafter = SpringAIAutoConfiguration.classpulls this class ahead of the providers again.
- Would adding
Smaller things:
// listed first on purposecan be dropped:AutoConfigurations.ofsorts its input.- Could the comments in
afterNamebe one line each, without the version note? - In the test, could
PROPERTIESbe inlined and the assertions usehasSingleBean(SpringAI.class)/hasSingleBean(SpringAIEmbedding.class)instead of bean method names? - Would the OpenAI auto-configuration module be enough as the test dependency, instead of the full starter?
- The new file's copyright year should be 2026.
7f19ae1 to
c0dfd7c
Compare
|
Thanks for the thorough review! Everything is addressed in c0dfd7c (force-pushed as a single Apps with multiple providers now fail at startup — good catch, that was a real regression. Providers missing from afterName still hit #1501 — I checked before adding Smaller things — all done:
|
| @AutoConfiguration( | ||
| afterName = { | ||
| // Spring AI chat model auto-configurations | ||
| "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", | ||
| // Spring AI embedding model auto-configurations (required for springAIEmbedding) | ||
| "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.VertexAiTextEmbeddingAutoConfiguration" | ||
| }) |
There was a problem hiding this comment.
Could we drop the afterName list now? I suggested keeping it earlier, but after trying it, @AutoConfigureOrder(Ordered.LOWEST_PRECEDENCE) alone is enough: Spring AI's model auto-configurations don't set an order, so this class sorts after all of them, listed or not. Without the list, the existing OpenAI tests still pass, two providers back off, and an unlisted provider gets the bean. The list only matters if another auto-configuration declares after = SpringAIAutoConfiguration.class without an order, pulling this class ahead of the providers; such a config can use LOWEST_PRECEDENCE too. Dropping the list also removes 16 class names that need updating as Spring AI adds providers, and the existing tests then cover the ordering annotation.
If you'd rather keep the list, could a test cover the fallback? All current tests pass with @AutoConfigureOrder removed. A provider auto-configuration nested in SpringAIAutoConfigurationOrderingTest (so its name sorts after this class) and omitted from afterName would catch that.
There was a problem hiding this comment.
Dropped — thanks for testing it, that matches what AutoConfigurationSorter implies:
un-annotated configs sort in the DEFAULT_ORDER=0 bucket, so LOWEST_PRECEDENCE alone keeps
this class after all providers, listed or not. With the list gone, the existing OpenAI ordering
tests now exercise the annotation directly.
| // Fallback for provider auto-configurations that are not listed in afterName: un-annotated | ||
| // auto-configurations sort in the DEFAULT_ORDER=0 bucket, so LOWEST_PRECEDENCE keeps this class | ||
| // after them regardless of package name. |
There was a problem hiding this comment.
Would a one-line comment work here, e.g. // Sorts after provider auto-configurations; configs ordered after this class need LOWEST_PRECEDENCE too.? The current comment says the class sorts after unlisted providers "regardless of package name", which doesn't hold when a config is ordered after it.
There was a problem hiding this comment.
Done, using your wording.
| @ConditionalOnMissingBean(SpringAI.class) | ||
| @ConditionalOnBean({ChatModel.class, StreamingChatModel.class}) | ||
| @ConditionalOnSingleCandidate(ChatModel.class) | ||
| @ConditionalOnBean(StreamingChatModel.class) |
There was a problem hiding this comment.
Optional: Is @ConditionalOnBean(StreamingChatModel.class) still needed, since every ChatModel is also a StreamingChatModel?
There was a problem hiding this comment.
Dropped. Verified in Spring AI 2.0.1 that ChatModel extends StreamingChatModel, so the
condition was always true whenever @ConditionalOnSingleCandidate(ChatModel.class) matched.
One consequence worth flagging: springAIWithChatModel is now unreachable — its guards are
identical to springAIWithBothModels, which is declared first, and the single ChatModel
candidate always satisfies the StreamingChatModel parameter. I left it in place since removing
it felt beyond the scope of the question, but happy to delete it if you'd prefer.
| // ToolCallingAutoConfiguration provides the ToolCallingManager that | ||
| // OpenAiChatAutoConfiguration requires. A real application imports it | ||
| // automatically; ApplicationContextRunner only processes what is declared. |
There was a problem hiding this comment.
Optional: Could this comment be one line?
| private final ApplicationContextRunner runner = | ||
| new ApplicationContextRunner() | ||
| .withConfiguration(AutoConfigurations.of(SpringAIAutoConfiguration.class)); |
There was a problem hiding this comment.
Optional: The back-off and @Primary tests don't involve ordering; would they fit better in SpringAIAutoConfigurationTest, reusing its contextRunner?
There was a problem hiding this comment.
Moved them to SpringAIAutoConfigurationTest, reusing its contextRunner.
| @Test | ||
| void backsOff_whenMultipleEmbeddingModelBeansArePresent() { | ||
| runner | ||
| .withUserConfiguration(TwoEmbeddingModels.class) | ||
| .run(context -> assertThat(context).doesNotHaveBean(SpringAIEmbedding.class)); | ||
| } |
There was a problem hiding this comment.
Optional: Could a back-off test also cover two StreamingChatModel beans, for the streaming-only path?
There was a problem hiding this comment.
Added testBacksOffWithMultipleStreamingChatModelBeans.
| @Configuration(proxyBeanMethods = false) | ||
| static class TwoChatModels { |
There was a problem hiding this comment.
Optional: Would withBean(...) on the runner be simpler than the nested @Configuration classes for the mocks?
There was a problem hiding this comment.
Done — the @Primary case uses
withBean(..., beanDefinition -> beanDefinition.setPrimary(true)).
| </dependency> | ||
| <dependency> | ||
| <groupId>org.springframework.ai</groupId> | ||
| <artifactId>spring-ai-autoconfigure-model-openai</artifactId> |
There was a problem hiding this comment.
Optional: The test also imports ToolCallingAutoConfiguration directly, but gets it only transitively; would declaring its module as a test dependency be clearer?
There was a problem hiding this comment.
Declared spring-ai-autoconfigure-model-tool explicitly.
0f294a0 to
2692275
Compare
… auto-configurations Add @AutoConfigureOrder(Ordered.LOWEST_PRECEDENCE) so this class sorts after the Spring AI model auto-configurations (un-annotated auto-configurations sort in the DEFAULT_ORDER=0 bucket) and its conditional guards can see the provider beans. Auto-configurations that must sort after this class need LOWEST_PRECEDENCE too. Guard the four bean methods with @ConditionalOnSingleCandidate instead of @ConditionalOnBean so apps with several provider starters keep backing off instead of failing to pick a model at startup; a @primary model is still used.
2692275 to
d342a95
Compare
Fixes #1501
Problem
SpringAIAutoConfiguration(contrib/spring-ai) guards all four of its@Beanmethods with@ConditionalOnBean— threeSpringAIvariants onChatModel/StreamingChatModel, andspringAIEmbeddingonEmbeddingModel— but declares no ordering relative to theauto-configurations that register those beans.
The result of
@ConditionalOnBeandepends on what has been processed so far. With nodeclared ordering, evaluation order falls back to the alphabetical class-name sort, and
com.google.adk...sorts beforeorg.springframework.ai...— so ADK's conditions areevaluated before any Spring AI model bean definition exists. No
SpringAIbean isregistered and applications fail to start:
springAIEmbeddingis affected identically — silently: no error, just a missing bean.Fix
Declare
afterNameon@AutoConfiguration, listing the Spring AI model auto-configurationsthat may provide
ChatModel,StreamingChatModel, orEmbeddingModelbeans(7 chat + 9 embedding entries in the commit).
Design notes:
afterName(string) and notafter(Class): this module compiles only againstspring-ai-model; provider auto-configuration classes are not on the compile classpath.Names that do not exist on the classpath are ignored by the sorter, which makes string
form the intended mechanism here — the same pattern Spring AI 1.x's own
ChatClientAutoConfigurationused for the identical problem.META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(the authoritative registration file — class-path/name-pattern guessing misses
nested packages such as
google.genai.autoconfigure.chatorvertexai.autoconfigure.embedding).javap-verified the bean type of every configuration whose name alone is notsufficient:
GoogleGenAiChatModel implements ChatModel;GoogleGenAiTextEmbeddingModel/VertexAiTextEmbeddingModelextendAbstractEmbeddingModel.*Connection*AutoConfiguration(google-genai, vertex-ai) — register connection-detailsbeans, not models;
VertexAiMultiModalEmbeddingAutoConfiguration— registers aDocumentEmbeddingModel,which does not satisfy
@ConditionalOnBean(EmbeddingModel);ElevenLabsAutoConfiguration— registers a text-to-speech model, not chat/embedding;OllamaApiAutoConfigurationand the image/audio/moderation/OCR configurations —unrelated interfaces.
same standing cost Spring AI 1.x accepted for this pattern. Modules introduced after
2.0.1 are ignored harmlessly until added.
Test
New
SpringAIAutoConfigurationOrderingTestplacesSpringAIAutoConfigurationfirst inAutoConfigurations.of(...)alongside the provider configurations andToolCallingAutoConfiguration(the latter provides theToolCallingManagerthatOpenAiChatAutoConfigurationrequires; a real application imports it automatically, thecontext runner must declare it explicitly).
AutoConfigurations.ofapplies the same ordering rules as production auto-configurationimport (
AutoConfigurationSorter#getInPriorityOrder), so:SpringAIAutoConfigurationbefore the provider configurations, so@ConditionalOnBeannever matches.
singleton pre-instantiation logs change from
springAIEmbeddingbeing created beforeopenAiChatModel(alphabetical order, conditions evaluated too early) to OpenAI beansbeing created first followed by
Auto-configuring SpringAI....Notes
An alternative fix — dropping
@ConditionalOnBeanin favor of@Beanmethod parameterinjection, as Spring AI 2.0's own
ChatClientAutoConfigurationdoes — would also removethe ordering sensitivity, but changes semantics: with no model present, failure moves from
a clear startup report to a less friendly bean-creation error. The
afterNamedeclarationis the smaller, behavior-preserving change.
Google CLA signed.