Skip to content

fix(spring-ai): declare ordering for SpringAIAutoConfiguration so @ConditionalOnBean matches Spring AI model beans - #1502

Merged
copybara-service[bot] merged 1 commit into
google:mainfrom
kongxubihai:fix/spring-ai-autoconfig-ordering
Oct 8, 2026
Merged

copybara-service[bot] merged 1 commit into
google:mainfrom
kongxubihai:fix/spring-ai-autoconfig-ordering

Conversation

@kongxubihai

Copy link
Copy Markdown
Contributor

Fixes #1501

Problem

SpringAIAutoConfiguration (contrib/spring-ai) guards all four of its @Bean methods with
@ConditionalOnBean — three SpringAI variants on ChatModel/StreamingChatModel, and
springAIEmbedding on EmbeddingModel — but declares no ordering relative to the
auto-configurations that register those beans.

The result of @ConditionalOnBean depends on what has been processed so far. With no
declared ordering, evaluation order falls back to the alphabetical class-name sort, and
com.google.adk... sorts before org.springframework.ai... — so ADK's conditions are
evaluated before any Spring AI model bean definition exists. No SpringAI bean is
registered and applications fail to start:

Parameter 0 of method scienceTeacher in ...AdkJavaSpringAiDemoApplication required a bean
of type 'com.google.adk.models.springai.SpringAI' that could not be found.

springAIEmbedding is affected identically — silently: no error, just a missing bean.

Fix

Declare afterName on @AutoConfiguration, listing the Spring AI model auto-configurations
that may provide ChatModel, StreamingChatModel, or EmbeddingModel beans
(7 chat + 9 embedding entries in the commit).

Design notes:

  1. Why afterName (string) and not after (Class): this module compiles only against
    spring-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
    ChatClientAutoConfiguration used for the identical problem.
  2. How the list was derived and verified (against the published Spring AI 2.0.1 jars):
    • Read every provider module's
      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.chat or vertexai.autoconfigure.embedding).
    • javap-verified the bean type of every configuration whose name alone is not
      sufficient: GoogleGenAiChatModel implements ChatModel;
      GoogleGenAiTextEmbeddingModel / VertexAiTextEmbeddingModel extend
      AbstractEmbeddingModel.
  3. Deliberately excluded (each verified, not guessed):
    • *Connection*AutoConfiguration (google-genai, vertex-ai) — register connection-details
      beans, not models;
    • VertexAiMultiModalEmbeddingAutoConfiguration — registers a DocumentEmbeddingModel,
      which does not satisfy @ConditionalOnBean(EmbeddingModel);
    • ElevenLabsAutoConfiguration — registers a text-to-speech model, not chat/embedding;
    • OllamaApiAutoConfiguration and the image/audio/moderation/OCR configurations —
      unrelated interfaces.
  4. Maintenance: new Spring AI provider modules need to be appended to this list — the
    same standing cost Spring AI 1.x accepted for this pattern. Modules introduced after
    2.0.1 are ignored harmlessly until added.

Test

New SpringAIAutoConfigurationOrderingTest places SpringAIAutoConfiguration first in
AutoConfigurations.of(...) alongside the provider configurations and
ToolCallingAutoConfiguration (the latter provides the ToolCallingManager that
OpenAiChatAutoConfiguration requires; a real application imports it automatically, the
context runner must declare it explicitly).

AutoConfigurations.of applies the same ordering rules as production auto-configuration
import (AutoConfigurationSorter#getInPriorityOrder), so:

  • Before this change the tests fail: the alphabetical order processes
    SpringAIAutoConfiguration before the provider configurations, so @ConditionalOnBean
    never matches.
  • After both tests pass, and runtime behavior flips accordingly — in a demo application,
    singleton pre-instantiation logs change from springAIEmbedding being created before
    openAiChatModel (alphabetical order, conditions evaluated too early) to OpenAI beans
    being created first followed by Auto-configuring SpringAI....

Notes

An alternative fix — dropping @ConditionalOnBean in favor of @Bean method parameter
injection, as Spring AI 2.0's own ChatClientAutoConfiguration does — would also remove
the ordering sensitivity, but changes semantics: with no model present, failure moves from
a clear startup report to a less friendly bean-creation error. The afterName declaration
is the smaller, behavior-preserving change.

Google CLA signed.

@google-cla

google-cla Bot commented Sep 11, 2026

Copy link
Copy Markdown

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.

@kongxubihai
kongxubihai force-pushed the fix/spring-ai-autoconfig-ordering branch from 7d52df7 to 4c434a6 Compare September 11, 2026 05:35
@hemasekhar-p hemasekhar-p self-assigned this Sep 11, 2026
@hemasekhar-p

Copy link
Copy Markdown
Contributor

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.

@damianmomotgoogle damianmomotgoogle left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 several ChatModel beans 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. Now springAIWithBothModels fails because it cannot pick a model, and springAIEmbedding fails the same way with two EmbeddingModel beans, even when the app defines its own SpringAI bean.
    • Would @ConditionalOnSingleCandidate on 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 mock ChatModel beans could cover this.
  • Providers missing from afterName still hit #1501. Any chat or embedding auto-configuration that isn't listed and sorts after com.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) alongside afterName help as a fallback? On its own it wouldn't be enough: a config with after = SpringAIAutoConfiguration.class pulls this class ahead of the providers again.

Smaller things:

  • // listed first on purpose can be dropped: AutoConfigurations.of sorts its input.
  • Could the comments in afterName be one line each, without the version note?
  • In the test, could PROPERTIES be inlined and the assertions use hasSingleBean(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.

@damianmomotgoogle damianmomotgoogle added waiting on reporter Waiting for reaction by reporter. Failing that, maintainers will eventually closed it as stale. and removed needs review labels Oct 5, 2026
@kongxubihai
kongxubihai force-pushed the fix/spring-ai-autoconfig-ordering branch 2 times, most recently from 7f19ae1 to c0dfd7c Compare October 7, 2026 01:12
@kongxubihai

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review! Everything is addressed in c0dfd7c (force-pushed as a single
commit, since the commit-count check requires squashing).

Apps with multiple providers now fail at startup — good catch, that was a real regression.
All four bean methods now use @ConditionalOnSingleCandidate instead of @ConditionalOnBean,
so apps with several provider starters back off exactly as before, and a @Primary model is
still picked up. (springAIWithBothModels keeps @ConditionalOnBean(StreamingChatModel.class)
alongside @ConditionalOnSingleCandidate(ChatModel.class), since the annotation accepts a
single type.) Added tests with two mock ChatModel beans, two mock EmbeddingModel beans,
and the @Primary case.

Providers missing from afterName still hit #1501 — I checked before adding
@AutoConfigureOrder(Ordered.LOWEST_PRECEDENCE): AutoConfigureOrder.DEFAULT_ORDER is 0,
so every un-annotated auto-configuration sorts in the 0 bucket, and the annotation does sink
this class below them — an unlisted third-party starter or a provider added in a later release
is now processed first regardless of package name, as long as it declares no explicit order or
before/after constraint. Your caveat still applies: a config with
after = SpringAIAutoConfiguration.class reorders via the before/after graph that
AutoConfigurationSorter applies after the order sort, so the afterName list remains the
reliable path for providers we know about. This is noted in a comment on the annotation.

Smaller things — all done:

  • dropped the // listed first on purpose comment (AutoConfigurations.of sorts its input);
  • one-line comments in afterName, without the version note;
  • inlined the property and switched the assertions to hasSingleBean(SpringAI.class) /
    hasSingleBean(SpringAIEmbedding.class);
  • replaced the starter with spring-ai-autoconfigure-model-openai (it brings
    spring-ai-autoconfigure-model-tool transitively, which covers ToolCallingAutoConfiguration);
  • bumped the new file's copyright year to 2026.

Comment on lines +66 to +86
@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"
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +87 to +89
// 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, using your wording.

@ConditionalOnMissingBean(SpringAI.class)
@ConditionalOnBean({ChatModel.class, StreamingChatModel.class})
@ConditionalOnSingleCandidate(ChatModel.class)
@ConditionalOnBean(StreamingChatModel.class)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: Is @ConditionalOnBean(StreamingChatModel.class) still needed, since every ChatModel is also a StreamingChatModel?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +44 to +46
// ToolCallingAutoConfiguration provides the ToolCallingManager that
// OpenAiChatAutoConfiguration requires. A real application imports it
// automatically; ApplicationContextRunner only processes what is declared.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: Could this comment be one line?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines +57 to +59
private final ApplicationContextRunner runner =
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(SpringAIAutoConfiguration.class));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: The back-off and @Primary tests don't involve ordering; would they fit better in SpringAIAutoConfigurationTest, reusing its contextRunner?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved them to SpringAIAutoConfigurationTest, reusing its contextRunner.

Comment on lines +78 to +83
@Test
void backsOff_whenMultipleEmbeddingModelBeansArePresent() {
runner
.withUserConfiguration(TwoEmbeddingModels.class)
.run(context -> assertThat(context).doesNotHaveBean(SpringAIEmbedding.class));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: Could a back-off test also cover two StreamingChatModel beans, for the streaming-only path?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added testBacksOffWithMultipleStreamingChatModelBeans.

Comment on lines +92 to +93
@Configuration(proxyBeanMethods = false)
static class TwoChatModels {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: Would withBean(...) on the runner be simpler than the nested @Configuration classes for the mocks?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — the @Primary case uses
withBean(..., beanDefinition -> beanDefinition.setPrimary(true)).

Comment thread contrib/spring-ai/pom.xml
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-autoconfigure-model-openai</artifactId>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: The test also imports ToolCallingAutoConfiguration directly, but gets it only transitively; would declaring its module as a test dependency be clearer?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declared spring-ai-autoconfigure-model-tool explicitly.

@kongxubihai
kongxubihai force-pushed the fix/spring-ai-autoconfig-ordering branch 2 times, most recently from 0f294a0 to 2692275 Compare October 8, 2026 01:53
… 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.
@kongxubihai
kongxubihai force-pushed the fix/spring-ai-autoconfig-ordering branch from 2692275 to d342a95 Compare October 8, 2026 01:54
@damianmomotgoogle damianmomotgoogle added ready to pull and removed waiting on reporter Waiting for reaction by reporter. Failing that, maintainers will eventually closed it as stale. labels Oct 8, 2026
@copybara-service
copybara-service Bot merged commit c239870 into google:main Oct 8, 2026
12 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

3 participants