33//! This module provides runtime provider selection using enum dispatch,
44//! allowing git-iris to work with any supported provider based on config.
55
6- use anyhow:: { Context , Result } ;
6+ use anyhow:: Result ;
77use rig:: {
88 agent:: { Agent , AgentBuilder , PromptResponse } ,
99 client:: { CompletionClient , ProviderClient } ,
@@ -95,16 +95,16 @@ fn validate_and_warn(key: &str, provider: Provider, source: &str) {
9595/// in config while still supporting env-only setups.
9696pub fn resolve_api_key ( api_key : Option < & str > , provider : Provider ) -> ( Option < String > , ApiKeySource ) {
9797 // If explicit key provided and non-empty, use it
98- if let Some ( key) = api_key {
99- if !key. is_empty ( ) {
100- tracing :: trace! (
101- provider = %provider ,
102- source = "config" ,
103- "Using API key from configuration"
104- ) ;
105- validate_and_warn ( key , provider , "config" ) ;
106- return ( Some ( key. to_string ( ) ) , ApiKeySource :: Config ) ;
107- }
98+ if let Some ( key) = api_key
99+ && !key. is_empty ( )
100+ {
101+ tracing :: trace! (
102+ provider = %provider ,
103+ source = "config" ,
104+ "Using API key from configuration"
105+ ) ;
106+ validate_and_warn ( key, provider , "config" ) ;
107+ return ( Some ( key . to_string ( ) ) , ApiKeySource :: Config ) ;
108108 }
109109
110110 // Fall back to environment variable
@@ -138,11 +138,17 @@ pub fn resolve_api_key(api_key: Option<&str>, provider: Provider) -> (Option<Str
138138///
139139/// # Errors
140140/// Returns an error if client creation fails (invalid credentials or missing env var).
141+ ///
142+ /// # Security
143+ /// Error messages are sanitized to prevent potential API key exposure.
141144pub fn openai_builder ( model : & str , api_key : Option < & str > ) -> Result < OpenAIBuilder > {
142145 let ( resolved_key, _source) = resolve_api_key ( api_key, Provider :: OpenAI ) ;
143146 let client = match resolved_key {
144147 Some ( key) => openai:: Client :: new ( & key)
145- . context ( "Failed to create OpenAI client with provided credentials" ) ?,
148+ // Sanitize error to prevent potential key exposure in error messages
149+ . map_err ( |_| anyhow:: anyhow!(
150+ "Failed to create OpenAI client: authentication or configuration error"
151+ ) ) ?,
146152 None => openai:: Client :: from_env ( ) ,
147153 } ;
148154 Ok ( client. completions_api ( ) . agent ( model) )
@@ -159,11 +165,17 @@ pub fn openai_builder(model: &str, api_key: Option<&str>) -> Result<OpenAIBuilde
159165///
160166/// # Errors
161167/// Returns an error if client creation fails (invalid credentials or missing env var).
168+ ///
169+ /// # Security
170+ /// Error messages are sanitized to prevent potential API key exposure.
162171pub fn anthropic_builder ( model : & str , api_key : Option < & str > ) -> Result < AnthropicBuilder > {
163172 let ( resolved_key, _source) = resolve_api_key ( api_key, Provider :: Anthropic ) ;
164173 let client = match resolved_key {
165174 Some ( key) => anthropic:: Client :: new ( & key)
166- . context ( "Failed to create Anthropic client with provided credentials" ) ?,
175+ // Sanitize error to prevent potential key exposure in error messages
176+ . map_err ( |_| anyhow:: anyhow!(
177+ "Failed to create Anthropic client: authentication or configuration error"
178+ ) ) ?,
167179 None => anthropic:: Client :: from_env ( ) ,
168180 } ;
169181 Ok ( client. agent ( model) )
@@ -180,11 +192,17 @@ pub fn anthropic_builder(model: &str, api_key: Option<&str>) -> Result<Anthropic
180192///
181193/// # Errors
182194/// Returns an error if client creation fails (invalid credentials or missing env var).
195+ ///
196+ /// # Security
197+ /// Error messages are sanitized to prevent potential API key exposure.
183198pub fn gemini_builder ( model : & str , api_key : Option < & str > ) -> Result < GeminiBuilder > {
184199 let ( resolved_key, _source) = resolve_api_key ( api_key, Provider :: Google ) ;
185200 let client = match resolved_key {
186201 Some ( key) => gemini:: Client :: new ( & key)
187- . context ( "Failed to create Gemini client with provided credentials" ) ?,
202+ // Sanitize error to prevent potential key exposure in error messages
203+ . map_err ( |_| anyhow:: anyhow!(
204+ "Failed to create Gemini client: authentication or configuration error"
205+ ) ) ?,
188206 None => gemini:: Client :: from_env ( ) ,
189207 } ;
190208 Ok ( client. agent ( model) )
@@ -242,4 +260,35 @@ mod tests {
242260 assert_eq ! ( ApiKeySource :: ClientDefault , ApiKeySource :: ClientDefault ) ;
243261 assert_ne ! ( ApiKeySource :: Config , ApiKeySource :: Environment ) ;
244262 }
263+
264+ #[ test]
265+ fn test_resolve_api_key_all_providers ( ) {
266+ // Test that resolve_api_key works for all supported providers
267+ for provider in Provider :: ALL {
268+ let ( key, source) =
269+ resolve_api_key ( Some ( "test-key-123456789012345" ) , * provider) ;
270+ assert_eq ! ( key, Some ( "test-key-123456789012345" . to_string( ) ) ) ;
271+ assert_eq ! ( source, ApiKeySource :: Config ) ;
272+ }
273+ }
274+
275+ #[ test]
276+ fn test_resolve_api_key_config_precedence ( ) {
277+ // Even if env var is set, config should take precedence
278+ // We can't easily mock env vars in unit tests, but we can verify
279+ // that a provided config key is always used regardless of env state
280+ let config_key = "sk-from-config-abcdef1234567890" ;
281+ let ( key, source) = resolve_api_key ( Some ( config_key) , Provider :: OpenAI ) ;
282+
283+ assert_eq ! ( key. as_deref( ) , Some ( config_key) ) ;
284+ assert_eq ! ( source, ApiKeySource :: Config ) ;
285+ }
286+
287+ #[ test]
288+ fn test_api_key_source_debug_impl ( ) {
289+ // Verify Debug is implemented for logging purposes
290+ let source = ApiKeySource :: Config ;
291+ let debug_str = format ! ( "{:?}" , source) ;
292+ assert ! ( debug_str. contains( "Config" ) ) ;
293+ }
245294}
0 commit comments