Multiple LLM Providers
The @Qualifier Pattern
Power RAG registers multiple ChatClient beans — one per provider (Claude, Gemini, Ollama). Without disambiguation Spring would throw a NoUniqueBeanDefinitionException. The solution is to annotate each bean with a unique @Qualifier name and annotate the injection point with the same qualifier.
SpringAiConfig.java — Representative excerpt
@Configuration
public class SpringAiConfig {
private static final String SYSTEM_PREAMBLE = """
You are a knowledgeable, helpful, and fair assistant...
""";
@Bean @Primary @Qualifier("claudeSonnet")
public ChatClient claudeSonnetClient(AnthropicChatModel model) {
return ChatClient.builder(model)
.defaultSystem(SYSTEM_PREAMBLE)
.build();
}
@Bean @Qualifier("geminiFlash")
public ChatClient geminiFlashClient(GoogleGenAiChatModel model) {
return ChatClient.builder(model)
.defaultSystem(SYSTEM_PREAMBLE)
.build();
}
/** Input safety only — no default system prompt. */
@Bean @Qualifier("geminiGuard")
public ChatClient geminiGuardClient(GoogleGenAiChatModel model) {
return ChatClient.builder(model).build();
}
@Bean @Qualifier("ollamaQwen")
public ChatClient ollamaQwenClient(OllamaChatModel model) {
return ChatClient.builder(model)
.defaultSystem(SYSTEM_PREAMBLE)
.build();
}
}
Explanation
@Primary
The claudeSonnetClient bean is annotated with @Primary. This means: when a component injects ChatClient without any @Qualifier, Spring automatically selects this bean. It is the application default.
@Qualifier("geminiGuard")
The guardrail ChatClient is built without a default system prompt. That keeps the safety-classification user message separate from the main RAG assistant preamble. The model id for each call is set in GuardrailService via GoogleGenAiChatOptions (default gemini-2.5-flash).
Constructor parameter injection
Each bean method receives a provider-specific model type (AnthropicChatModel, GoogleGenAiChatModel, OllamaChatModel). These are auto-created by the respective provider starters. Spring injects them by type — there is no ambiguity because each type is unique.
@Primary means this bean is injected when no @Qualifier is specified. Always mark exactly one bean as @Primary to avoid NoUniqueBeanDefinitionException in components that inject without a qualifier.Registered Beans in Power RAG
| Qualifier | Provider | Default Model | Purpose |
|---|---|---|---|
claudeSonnet (Primary) |
Anthropic | claude-sonnet-4-6 | Main RAG chat responses |
geminiFlash |
Google GenAI | gemini-2.5-flash | RAG chat; image generation fallback |
geminiGuard |
Google GenAI | powerrag.guardrails.input-model-id (default gemini-2.5-flash) |
Input safety classification only |
ollamaQwen / ollamaDeepSeek |
Ollama | configured chat models | Local chat when user selects Ollama |
Injecting a Specific Bean
To inject the Gemini client specifically in a service:
@Service
public class SomeService {
private final ChatClient geminiClient;
public SomeService(@Qualifier("geminiFlash") ChatClient geminiClient) {
this.geminiClient = geminiClient;
}
}