Skip to content

Make LLM providers pluggable via auto-configuration#69

Open
MarcosLM11 wants to merge 2 commits into
ClawRunr:mainfrom
MarcosLM11:feat/pluggable-providers
Open

Make LLM providers pluggable via auto-configuration#69
MarcosLM11 wants to merge 2 commits into
ClawRunr:mainfrom
MarcosLM11:feat/pluggable-providers

Conversation

@MarcosLM11

Copy link
Copy Markdown

Closes #17

What

Make the providers/* modules self-register from the classpath the same way
the plugins/* modules do, instead of depending on the app's component scan.

Each provider now contributes its AgentOnboardingProvider through a Spring
Boot @AutoConfiguration class listed in the module's
META-INF/spring/…AutoConfiguration.imports. The ChatModel side was already
auto-configured by the Spring AI starters, so no change was needed there.

Changes

  • anthropic: AnthropticClaudeCodeConfiguration @Configuration
    @AutoConfiguration; onboarding provider moved to a @Bean in a new
    AnthropicAgentAutoConfiguration (keeps the Claude Code token condition).
  • openai / ollama / google: drop @Component, add a small per-module
    @AutoConfiguration + .imports.
  • Remove dead AgentProvider: it was never injected or called; its
    isConfigured stub returned false and getDefaultChatModel() would have
    thrown on an empty list. Active-model selection is handled centrally by
    spring.ai.model.chat.
  • Tests: one test per provider asserting the bean registers via the
    auto-config and that the .imports file names it — currently the only guard
    for provider discovery, since OnboardingControllerTest mocks
    AgentOnboardingProviders.
  • Docs: update AGENTS.md wording.

Notes

  • No behavior change: providers are discovered exactly as before, just wired via
    auto-configuration. spring.ai.model.chat still selects the active model.
  • Ollama needs no api-key gating — like the others, its ChatModel only loads
    when spring.ai.model.chat=ollama.
  • Scope is classpath pluggability (parity with plugins). Runtime provider jars
    are intentionally left out.

Testing

./gradlew compileJava test — all modules green.

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

arefbehboudi commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Great work.

I checked out the branch and verified: full build + all tests green, removing a provider from app/build.gradle still compiles and passes tests (true drop-in pluggability), all four providers register at runtime, and the Claude Code path still produces exactly one AnthropicChatModel.

One change requested before merge (auto-config ordering, see inline), the rest are minor.

Follow-up idea: switch the provider deps in app/build.gradle from implementation to runtimeOnly, so the compiler enforces that app never references provider classes directly.

import org.springframework.context.annotation.Configuration;

@Configuration
@AutoConfiguration

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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:

Suggested change
@AutoConfiguration
@AutoConfiguration(before = AnthropicChatAutoConfiguration.class)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed — AnthropticClaudeCodeConfiguration now declares @AutoConfiguration(before = AnthropicChatAutoConfiguration.class) so the ordering no longer depends on package name sort.

Comment on lines +10 to +13
@Bean
public AgentOnboardingProvider anthropicAgentOnboardingProvider() {
return new AnthropicAgentOnboardingProvider();
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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:

Suggested change
@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).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.
@MarcosLM11 MarcosLM11 requested a review from arefbehboudi July 13, 2026 10:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature - refactor so providers are pluggable

2 participants