Make LLM providers pluggable via auto-configuration#69
Conversation
Register each provider's AgentOnboardingProvider through a Spring Boot @autoConfiguration class listed in the module's AutoConfiguration.imports, mirroring the plugin modules. Providers now self-register from the classpath independently of the app's component scan, instead of relying on @component being reachable under the shared ai.javaclaw package. - anthropic: AnthropticClaudeCodeConfiguration @configuration -> @autoConfiguration; onboarding provider moved to a @bean in AnthropicAgentAutoConfiguration - openai/ollama/google: drop @component, add per-module @autoConfiguration + imports - Remove dead AgentProvider (unused; isConfigured stub returned false and getDefaultChatModel would throw). Model selection is handled centrally by spring.ai.model.chat. - Add per-provider tests asserting the bean registers and the imports file names the auto-config (the only guard for provider discovery, since OnboardingControllerTest mocks AgentOnboardingProviders) - Update AGENTS.md wording
|
Great work. I checked out the branch and verified: full build + all tests green, removing a provider from One change requested before merge (auto-config ordering, see inline), the rest are minor. Follow-up idea: switch the provider deps in |
| import org.springframework.context.annotation.Configuration; | ||
|
|
||
| @Configuration | ||
| @AutoConfiguration |
There was a problem hiding this comment.
This currently beats Spring AI's AnthropicChatAutoConfiguration (same bean name, anthropicChatModel) only because ai.javaclaw… sorts alphabetically before org.springframework…. On main, @Configuration gave a hard ordering guarantee; this is an accident. A package rename would flip the order and boot would fail with BeanDefinitionOverrideException.
Make it explicit:
| @AutoConfiguration | |
| @AutoConfiguration(before = AnthropicChatAutoConfiguration.class) |
There was a problem hiding this comment.
Fixed — AnthropticClaudeCodeConfiguration now declares @AutoConfiguration(before = AnthropicChatAutoConfiguration.class) so the ordering no longer depends on package name sort.
| @Bean | ||
| public AgentOnboardingProvider anthropicAgentOnboardingProvider() { | ||
| return new AnthropicAgentOnboardingProvider(); | ||
| } |
There was a problem hiding this comment.
The plugin auto-configs all use @ConditionalOnMissingBean — same here would allow user overrides and make an accidental re-added @Component harmless. Must be typed to the concrete class though, since the method returns the interface:
| @Bean | |
| public AgentOnboardingProvider anthropicAgentOnboardingProvider() { | |
| return new AnthropicAgentOnboardingProvider(); | |
| } | |
| @Bean | |
| @ConditionalOnMissingBean(AnthropicAgentOnboardingProvider.class) | |
| public AgentOnboardingProvider anthropicAgentOnboardingProvider() { | |
| return new AnthropicAgentOnboardingProvider(); | |
| } |
Same for the google, ollama, and openai modules (lines 10–13 each).
There was a problem hiding this comment.
Fixed in all four modules (anthropic, google, ollama, openai) — each onboarding @Bean now has @ConditionalOnMissingBean typed to the concrete provider class.
| AnthropticClaudeCodeConfiguration.class.getName()); | ||
| } | ||
|
|
||
| static List<String> importedAutoConfigurations() throws IOException { |
There was a problem hiding this comment.
getResources(…) scans all imports files on the classpath, so this asserts the class name appears somewhere, not in this module's own file — it could stay green while pluggability is broken. Reading the module's own resource via getResource(…) (single URL) fixes it. The helper is also copy-pasted in all four test classes (line 36 in the others); worth deduplicating — or replacing entirely with one @SpringBootTest in app asserting all four provider IDs are in AgentOnboardingProviders, which tests the same thing end-to-end.
There was a problem hiding this comment.
Fixed the correctness bug: each test now reads its own module's .imports resource via getResource() (single URL) instead of getResources() scanning the whole classpath, so it can no longer pass on another module's entry.
Left the ~8-line helper duplicated across the four modules rather than deduplicating or replacing with a single app-level test: there's no test-fixtures setup shared across the provider modules today, so deduplicating would mean introducing that infra just to save a few lines. And an app-level @SpringBootTest asserting all four IDs, if built with AutoConfigurations.of(...) explicit classes, wouldn't actually read the .imports files at all — it would bypass the exact mechanism this test guards. Getting a genuine end-to-end version of that (relying on real classpath auto-configuration import scanning) would need booting the full app context, which pulls in the datasource/jobrunr wiring and adds real fragility for what's currently the only regression guard on provider discovery. Happy to revisit if you feel strongly, but the per-module test now does what it's supposed to.
- Make Anthropic's ChatModel auto-config ordering explicit against AnthropicChatAutoConfiguration instead of relying on alphabetical package sort. - Add @ConditionalOnMissingBean, typed to the concrete provider class, to each provider's onboarding auto-config so user overrides win. - Fix the imports-file test helper to read each module's own .imports resource instead of scanning the whole classpath, so it can't pass on another module's entry.
Closes #17
What
Make the
providers/*modules self-register from the classpath the same waythe
plugins/*modules do, instead of depending on the app's component scan.Each provider now contributes its
AgentOnboardingProviderthrough a SpringBoot
@AutoConfigurationclass listed in the module'sMETA-INF/spring/…AutoConfiguration.imports. TheChatModelside was alreadyauto-configured by the Spring AI starters, so no change was needed there.
Changes
AnthropticClaudeCodeConfiguration@Configuration→@AutoConfiguration; onboarding provider moved to a@Beanin a newAnthropicAgentAutoConfiguration(keeps the Claude Code token condition).@Component, add a small per-module@AutoConfiguration+.imports.AgentProvider: it was never injected or called; itsisConfiguredstub returnedfalseandgetDefaultChatModel()would havethrown on an empty list. Active-model selection is handled centrally by
spring.ai.model.chat.auto-config and that the
.importsfile names it — currently the only guardfor provider discovery, since
OnboardingControllerTestmocksAgentOnboardingProviders.AGENTS.mdwording.Notes
auto-configuration.
spring.ai.model.chatstill selects the active model.ChatModelonly loadswhen
spring.ai.model.chat=ollama.are intentionally left out.
Testing
./gradlew compileJava test— all modules green.