@@ -4,6 +4,26 @@ use std::collections::HashMap;
44use std:: path:: { Path , PathBuf } ;
55use tracing:: warn;
66
7+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
8+ pub struct ProviderConfig {
9+ #[ serde( default ) ]
10+ pub api_key : Option < String > ,
11+ #[ serde( default ) ]
12+ pub base_url : Option < String > ,
13+ #[ serde( default = "default_true" ) ]
14+ pub enabled : bool ,
15+ }
16+
17+ impl Default for ProviderConfig {
18+ fn default ( ) -> Self {
19+ Self {
20+ api_key : None ,
21+ base_url : None ,
22+ enabled : true ,
23+ }
24+ }
25+ }
26+
727#[ derive( Debug , Clone , Serialize , Deserialize ) ]
828pub struct Config {
929 #[ serde( default = "default_model" ) ]
@@ -175,6 +195,12 @@ pub struct Config {
175195
176196 #[ serde( default ) ]
177197 pub rule_priority : Vec < String > ,
198+
199+ #[ serde( default ) ]
200+ pub providers : HashMap < String , ProviderConfig > ,
201+
202+ #[ serde( default ) ]
203+ pub github_token : Option < String > ,
178204}
179205
180206#[ derive( Debug , Clone , Serialize , Deserialize , Default ) ]
@@ -299,6 +325,8 @@ impl Default for Config {
299325 rules_files : Vec :: new ( ) ,
300326 max_active_rules : default_max_active_rules ( ) ,
301327 rule_priority : Vec :: new ( ) ,
328+ providers : HashMap :: new ( ) ,
329+ github_token : None ,
302330 }
303331 }
304332}
@@ -715,6 +743,70 @@ impl Config {
715743 }
716744 }
717745
746+ /// Resolve which provider to use based on configuration.
747+ ///
748+ /// Returns `(api_key, base_url, adapter)` by checking:
749+ /// 1. If `adapter` is explicitly set and a matching enabled provider exists, use it.
750+ /// 2. If no adapter is set, infer from the model name.
751+ /// 3. Fall back to top-level `api_key`/`base_url`.
752+ pub fn resolve_provider ( & self ) -> ( Option < String > , Option < String > , Option < String > ) {
753+ // If adapter is explicitly set, look for a matching provider
754+ if let Some ( ref adapter) = self . adapter {
755+ let key = adapter. to_lowercase ( ) ;
756+ if let Some ( provider) = self . providers . get ( & key) {
757+ if provider. enabled {
758+ let api_key = provider. api_key . clone ( ) . or_else ( || self . api_key . clone ( ) ) ;
759+ let base_url = provider. base_url . clone ( ) . or_else ( || self . base_url . clone ( ) ) ;
760+ return ( api_key, base_url, Some ( key) ) ;
761+ }
762+ }
763+ // Adapter is set but no matching provider found; fall through to top-level
764+ return ( self . api_key . clone ( ) , self . base_url . clone ( ) , Some ( key) ) ;
765+ }
766+
767+ // No adapter set: try to detect provider from model name
768+ let model_lower = self . model . to_lowercase ( ) ;
769+ let detected = if model_lower. starts_with ( "anthropic/" )
770+ || model_lower. starts_with ( "claude" )
771+ {
772+ Some ( "anthropic" )
773+ } else if model_lower. starts_with ( "openai/" )
774+ || model_lower. starts_with ( "gpt" )
775+ || model_lower. starts_with ( "o1" )
776+ || model_lower. starts_with ( "o3" )
777+ || model_lower. starts_with ( "o4" )
778+ {
779+ Some ( "openai" )
780+ } else if model_lower. starts_with ( "ollama:" ) {
781+ Some ( "ollama" )
782+ } else {
783+ // Default: check if openrouter provider is configured
784+ if self . providers . get ( "openrouter" ) . map_or ( false , |p| p. enabled ) {
785+ Some ( "openrouter" )
786+ } else {
787+ None
788+ }
789+ } ;
790+
791+ if let Some ( provider_key) = detected {
792+ if let Some ( provider) = self . providers . get ( provider_key) {
793+ if provider. enabled {
794+ let api_key = provider. api_key . clone ( ) . or_else ( || self . api_key . clone ( ) ) ;
795+ let base_url = provider. base_url . clone ( ) . or_else ( || self . base_url . clone ( ) ) ;
796+ // Map openrouter to openai adapter (OpenRouter uses OpenAI-compatible API)
797+ let adapter = match provider_key {
798+ "openrouter" => Some ( "openai" . to_string ( ) ) ,
799+ other => Some ( other. to_string ( ) ) ,
800+ } ;
801+ return ( api_key, base_url, adapter) ;
802+ }
803+ }
804+ }
805+
806+ // Fall back to top-level fields
807+ ( self . api_key . clone ( ) , self . base_url . clone ( ) , self . adapter . clone ( ) )
808+ }
809+
718810 /// Try to resolve the API key from Vault if Vault is configured and api_key is not set.
719811 pub async fn resolve_vault_api_key ( & mut self ) -> Result < ( ) > {
720812 if self . api_key . is_some ( ) {
0 commit comments