syncable_cli/agent/session/
providers.rs

1//! Provider-related logic for API key management, model selection, and credential handling.
2//!
3//! This module contains:
4//! - `get_available_models` - Returns available models per provider
5//! - `has_api_key` - Checks if API key is configured for a provider
6//! - `load_api_key_to_env` - Loads API key from config and sets in environment
7//! - `get_configured_providers` - Returns list of providers with valid credentials
8//! - `prompt_api_key` - Prompts user for API key interactively
9
10use crate::agent::{AgentError, AgentResult, ProviderType};
11use crate::config::{load_agent_config, save_agent_config};
12use colored::Colorize;
13use std::io::{self, Write};
14
15/// Available models per provider
16pub fn get_available_models(provider: ProviderType) -> Vec<(&'static str, &'static str)> {
17    match provider {
18        ProviderType::OpenAI => vec![
19            ("gpt-5.2", "GPT-5.2 - Latest reasoning model (Dec 2025)"),
20            ("gpt-5.2-mini", "GPT-5.2 Mini - Fast and affordable"),
21            ("gpt-4o", "GPT-4o - Multimodal workhorse"),
22            ("o1-preview", "o1-preview - Advanced reasoning"),
23        ],
24        ProviderType::Anthropic => vec![
25            (
26                "claude-opus-4-5-20251101",
27                "Claude Opus 4.5 - Most capable (Nov 2025)",
28            ),
29            (
30                "claude-sonnet-4-5-20250929",
31                "Claude Sonnet 4.5 - Balanced (Sep 2025)",
32            ),
33            (
34                "claude-haiku-4-5-20251001",
35                "Claude Haiku 4.5 - Fast (Oct 2025)",
36            ),
37            ("claude-sonnet-4-20250514", "Claude Sonnet 4 - Previous gen"),
38        ],
39        // Bedrock models - use cross-region inference profile format (global. prefix)
40        ProviderType::Bedrock => vec![
41            (
42                "global.anthropic.claude-opus-4-5-20251101-v1:0",
43                "Claude Opus 4.5 - Most capable (Nov 2025)",
44            ),
45            (
46                "global.anthropic.claude-sonnet-4-5-20250929-v1:0",
47                "Claude Sonnet 4.5 - Balanced (Sep 2025)",
48            ),
49            (
50                "global.anthropic.claude-haiku-4-5-20251001-v1:0",
51                "Claude Haiku 4.5 - Fast (Oct 2025)",
52            ),
53            (
54                "global.anthropic.claude-sonnet-4-20250514-v1:0",
55                "Claude Sonnet 4 - Previous gen",
56            ),
57        ],
58    }
59}
60
61/// Check if API key is configured for a provider (env var OR config file)
62pub fn has_api_key(provider: ProviderType) -> bool {
63    // Check environment variable first
64    let env_key = match provider {
65        ProviderType::OpenAI => std::env::var("OPENAI_API_KEY").ok(),
66        ProviderType::Anthropic => std::env::var("ANTHROPIC_API_KEY").ok(),
67        ProviderType::Bedrock => {
68            // Check for AWS credentials from env vars
69            if std::env::var("AWS_ACCESS_KEY_ID").is_ok()
70                && std::env::var("AWS_SECRET_ACCESS_KEY").is_ok()
71            {
72                return true;
73            }
74            if std::env::var("AWS_PROFILE").is_ok() {
75                return true;
76            }
77            None
78        }
79    };
80
81    if env_key.is_some() {
82        return true;
83    }
84
85    // Check config file - first try active global profile
86    let agent_config = load_agent_config();
87
88    // Check active global profile first
89    if let Some(profile_name) = &agent_config.active_profile
90        && let Some(profile) = agent_config.profiles.get(profile_name)
91    {
92        match provider {
93            ProviderType::OpenAI => {
94                if profile
95                    .openai
96                    .as_ref()
97                    .map(|o| !o.api_key.is_empty())
98                    .unwrap_or(false)
99                {
100                    return true;
101                }
102            }
103            ProviderType::Anthropic => {
104                if profile
105                    .anthropic
106                    .as_ref()
107                    .map(|a| !a.api_key.is_empty())
108                    .unwrap_or(false)
109                {
110                    return true;
111                }
112            }
113            ProviderType::Bedrock => {
114                if let Some(bedrock) = &profile.bedrock
115                    && (bedrock.profile.is_some()
116                        || (bedrock.access_key_id.is_some() && bedrock.secret_access_key.is_some()))
117                {
118                    return true;
119                }
120            }
121        }
122    }
123
124    // Check any profile that has this provider configured
125    for profile in agent_config.profiles.values() {
126        match provider {
127            ProviderType::OpenAI => {
128                if profile
129                    .openai
130                    .as_ref()
131                    .map(|o| !o.api_key.is_empty())
132                    .unwrap_or(false)
133                {
134                    return true;
135                }
136            }
137            ProviderType::Anthropic => {
138                if profile
139                    .anthropic
140                    .as_ref()
141                    .map(|a| !a.api_key.is_empty())
142                    .unwrap_or(false)
143                {
144                    return true;
145                }
146            }
147            ProviderType::Bedrock => {
148                if let Some(bedrock) = &profile.bedrock
149                    && (bedrock.profile.is_some()
150                        || (bedrock.access_key_id.is_some() && bedrock.secret_access_key.is_some()))
151                {
152                    return true;
153                }
154            }
155        }
156    }
157
158    // Fall back to legacy config
159    match provider {
160        ProviderType::OpenAI => agent_config.openai_api_key.is_some(),
161        ProviderType::Anthropic => agent_config.anthropic_api_key.is_some(),
162        ProviderType::Bedrock => {
163            if let Some(bedrock) = &agent_config.bedrock {
164                bedrock.profile.is_some()
165                    || (bedrock.access_key_id.is_some() && bedrock.secret_access_key.is_some())
166            } else {
167                agent_config.bedrock_configured.unwrap_or(false)
168            }
169        }
170    }
171}
172
173/// Load API key from config if not in env, and set it in env for use
174pub fn load_api_key_to_env(provider: ProviderType) {
175    let agent_config = load_agent_config();
176
177    // Try to get credentials from active global profile first
178    let active_profile = agent_config
179        .active_profile
180        .as_ref()
181        .and_then(|name| agent_config.profiles.get(name));
182
183    match provider {
184        ProviderType::OpenAI => {
185            if std::env::var("OPENAI_API_KEY").is_ok() {
186                return;
187            }
188            // Check active global profile
189            if let Some(key) = active_profile
190                .and_then(|p| p.openai.as_ref())
191                .map(|o| o.api_key.clone())
192                .filter(|k| !k.is_empty())
193            {
194                unsafe {
195                    std::env::set_var("OPENAI_API_KEY", &key);
196                }
197                return;
198            }
199            // Fall back to legacy key
200            if let Some(key) = &agent_config.openai_api_key {
201                unsafe {
202                    std::env::set_var("OPENAI_API_KEY", key);
203                }
204            }
205        }
206        ProviderType::Anthropic => {
207            if std::env::var("ANTHROPIC_API_KEY").is_ok() {
208                return;
209            }
210            // Check active global profile
211            if let Some(key) = active_profile
212                .and_then(|p| p.anthropic.as_ref())
213                .map(|a| a.api_key.clone())
214                .filter(|k| !k.is_empty())
215            {
216                unsafe {
217                    std::env::set_var("ANTHROPIC_API_KEY", &key);
218                }
219                return;
220            }
221            // Fall back to legacy key
222            if let Some(key) = &agent_config.anthropic_api_key {
223                unsafe {
224                    std::env::set_var("ANTHROPIC_API_KEY", key);
225                }
226            }
227        }
228        ProviderType::Bedrock => {
229            // Check active global profile first
230            let bedrock_config = active_profile
231                .and_then(|p| p.bedrock.as_ref())
232                .or(agent_config.bedrock.as_ref());
233
234            if let Some(bedrock) = bedrock_config {
235                // Load region
236                if std::env::var("AWS_REGION").is_err()
237                    && let Some(region) = &bedrock.region
238                {
239                    unsafe {
240                        std::env::set_var("AWS_REGION", region);
241                    }
242                }
243                // Load profile OR access keys (profile takes precedence)
244                if let Some(profile) = &bedrock.profile
245                    && std::env::var("AWS_PROFILE").is_err()
246                {
247                    unsafe {
248                        std::env::set_var("AWS_PROFILE", profile);
249                    }
250                } else if let (Some(key_id), Some(secret)) =
251                    (&bedrock.access_key_id, &bedrock.secret_access_key)
252                {
253                    if std::env::var("AWS_ACCESS_KEY_ID").is_err() {
254                        unsafe {
255                            std::env::set_var("AWS_ACCESS_KEY_ID", key_id);
256                        }
257                    }
258                    if std::env::var("AWS_SECRET_ACCESS_KEY").is_err() {
259                        unsafe {
260                            std::env::set_var("AWS_SECRET_ACCESS_KEY", secret);
261                        }
262                    }
263                }
264            }
265        }
266    }
267}
268
269/// Get configured providers (those with API keys)
270pub fn get_configured_providers() -> Vec<ProviderType> {
271    let mut providers = Vec::new();
272    if has_api_key(ProviderType::OpenAI) {
273        providers.push(ProviderType::OpenAI);
274    }
275    if has_api_key(ProviderType::Anthropic) {
276        providers.push(ProviderType::Anthropic);
277    }
278    providers
279}
280
281/// Interactive wizard to set up AWS Bedrock credentials
282pub(crate) fn run_bedrock_setup_wizard() -> AgentResult<String> {
283    use crate::config::types::BedrockConfig as BedrockConfigType;
284
285    println!();
286    println!(
287        "{}",
288        "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".cyan()
289    );
290    println!("{}", "  AWS Bedrock Setup Wizard".cyan().bold());
291    println!(
292        "{}",
293        "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".cyan()
294    );
295    println!();
296    println!("AWS Bedrock provides access to Claude models via AWS.");
297    println!("You'll need an AWS account with Bedrock access enabled.");
298    println!();
299
300    // Step 1: Choose authentication method
301    println!("{}", "Step 1: Choose authentication method".white().bold());
302    println!();
303    println!(
304        "  {} Use AWS Profile (from ~/.aws/credentials)",
305        "[1]".cyan()
306    );
307    println!(
308        "      {}",
309        "Best for: AWS CLI users, SSO, multiple accounts".dimmed()
310    );
311    println!();
312    println!("  {} Enter Access Keys directly", "[2]".cyan());
313    println!(
314        "      {}",
315        "Best for: Quick setup, CI/CD environments".dimmed()
316    );
317    println!();
318    println!("  {} Use existing environment variables", "[3]".cyan());
319    println!(
320        "      {}",
321        "Best for: Already configured AWS_* env vars".dimmed()
322    );
323    println!();
324    print!("Enter choice [1-3]: ");
325    io::stdout().flush().unwrap();
326
327    let mut choice = String::new();
328    io::stdin()
329        .read_line(&mut choice)
330        .map_err(|e| AgentError::ToolError(e.to_string()))?;
331    let choice = choice.trim();
332
333    let mut bedrock_config = BedrockConfigType::default();
334
335    match choice {
336        "1" => {
337            // AWS Profile
338            println!();
339            println!("{}", "Step 2: Enter AWS Profile".white().bold());
340            println!("{}", "Press Enter for 'default' profile".dimmed());
341            print!("Profile name: ");
342            io::stdout().flush().unwrap();
343
344            let mut profile = String::new();
345            io::stdin()
346                .read_line(&mut profile)
347                .map_err(|e| AgentError::ToolError(e.to_string()))?;
348            let profile = profile.trim();
349            let profile = if profile.is_empty() {
350                "default"
351            } else {
352                profile
353            };
354
355            bedrock_config.profile = Some(profile.to_string());
356
357            // Set in env for current session
358            unsafe {
359                std::env::set_var("AWS_PROFILE", profile);
360            }
361            println!("{}", format!("Using profile: {}", profile).green());
362        }
363        "2" => {
364            // Access Keys
365            println!();
366            println!("{}", "Step 2: Enter AWS Access Keys".white().bold());
367            println!(
368                "{}",
369                "Get these from AWS Console -> IAM -> Security credentials".dimmed()
370            );
371            println!();
372
373            print!("AWS Access Key ID: ");
374            io::stdout().flush().unwrap();
375            let mut access_key = String::new();
376            io::stdin()
377                .read_line(&mut access_key)
378                .map_err(|e| AgentError::ToolError(e.to_string()))?;
379            let access_key = access_key.trim().to_string();
380
381            if access_key.is_empty() {
382                return Err(AgentError::MissingApiKey("AWS_ACCESS_KEY_ID".to_string()));
383            }
384
385            print!("AWS Secret Access Key: ");
386            io::stdout().flush().unwrap();
387            let mut secret_key = String::new();
388            io::stdin()
389                .read_line(&mut secret_key)
390                .map_err(|e| AgentError::ToolError(e.to_string()))?;
391            let secret_key = secret_key.trim().to_string();
392
393            if secret_key.is_empty() {
394                return Err(AgentError::MissingApiKey(
395                    "AWS_SECRET_ACCESS_KEY".to_string(),
396                ));
397            }
398
399            bedrock_config.access_key_id = Some(access_key.clone());
400            bedrock_config.secret_access_key = Some(secret_key.clone());
401
402            // Set in env for current session
403            unsafe {
404                std::env::set_var("AWS_ACCESS_KEY_ID", &access_key);
405                std::env::set_var("AWS_SECRET_ACCESS_KEY", &secret_key);
406            }
407            println!("{}", "Access keys configured".green());
408        }
409        "3" => {
410            // Use existing env vars
411            if std::env::var("AWS_ACCESS_KEY_ID").is_err() && std::env::var("AWS_PROFILE").is_err()
412            {
413                println!("{}", "No AWS credentials found in environment!".yellow());
414                println!("Set AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY or AWS_PROFILE");
415                return Err(AgentError::MissingApiKey("AWS credentials".to_string()));
416            }
417            println!("{}", "Using existing environment variables".green());
418        }
419        _ => {
420            println!("{}", "Invalid choice, using environment variables".yellow());
421        }
422    }
423
424    // Step 2: Region selection
425    if bedrock_config.region.is_none() {
426        println!();
427        println!("{}", "Step 2: Select AWS Region".white().bold());
428        println!(
429            "{}",
430            "Bedrock is available in select regions. Common choices:".dimmed()
431        );
432        println!();
433        println!(
434            "  {} us-east-1     (N. Virginia) - Most models",
435            "[1]".cyan()
436        );
437        println!("  {} us-west-2     (Oregon)", "[2]".cyan());
438        println!("  {} eu-west-1     (Ireland)", "[3]".cyan());
439        println!("  {} ap-northeast-1 (Tokyo)", "[4]".cyan());
440        println!();
441        print!("Enter choice [1-4] or region name: ");
442        io::stdout().flush().unwrap();
443
444        let mut region_choice = String::new();
445        io::stdin()
446            .read_line(&mut region_choice)
447            .map_err(|e| AgentError::ToolError(e.to_string()))?;
448        let region = match region_choice.trim() {
449            "1" | "" => "us-east-1",
450            "2" => "us-west-2",
451            "3" => "eu-west-1",
452            "4" => "ap-northeast-1",
453            other => other,
454        };
455
456        bedrock_config.region = Some(region.to_string());
457        unsafe {
458            std::env::set_var("AWS_REGION", region);
459        }
460        println!("{}", format!("Region: {}", region).green());
461    }
462
463    // Step 3: Model selection
464    println!();
465    println!("{}", "Step 3: Select Default Model".white().bold());
466    println!();
467    let models = get_available_models(ProviderType::Bedrock);
468    for (i, (id, desc)) in models.iter().enumerate() {
469        let marker = if i == 0 { "-> " } else { "  " };
470        println!("  {} {} {}", marker, format!("[{}]", i + 1).cyan(), desc);
471        println!("      {}", id.dimmed());
472    }
473    println!();
474    print!("Enter choice [1-{}] (default: 1): ", models.len());
475    io::stdout().flush().unwrap();
476
477    let mut model_choice = String::new();
478    io::stdin()
479        .read_line(&mut model_choice)
480        .map_err(|e| AgentError::ToolError(e.to_string()))?;
481    let model_idx: usize = model_choice.trim().parse().unwrap_or(1);
482    let model_idx = model_idx.saturating_sub(1).min(models.len() - 1);
483    let selected_model = models[model_idx].0.to_string();
484
485    bedrock_config.default_model = Some(selected_model.clone());
486    println!(
487        "{}",
488        format!(
489            "Default model: {}",
490            models[model_idx]
491                .1
492                .split(" - ")
493                .next()
494                .unwrap_or(&selected_model)
495        )
496        .green()
497    );
498
499    // Save configuration
500    let mut agent_config = load_agent_config();
501    agent_config.bedrock = Some(bedrock_config);
502    agent_config.bedrock_configured = Some(true);
503
504    if let Err(e) = save_agent_config(&agent_config) {
505        eprintln!(
506            "{}",
507            format!("Warning: Could not save config: {}", e).yellow()
508        );
509    } else {
510        println!();
511        println!("{}", "Configuration saved to ~/.syncable.toml".green());
512    }
513
514    println!();
515    println!(
516        "{}",
517        "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".cyan()
518    );
519    println!("{}", "  AWS Bedrock setup complete!".green().bold());
520    println!(
521        "{}",
522        "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".cyan()
523    );
524    println!();
525
526    Ok(selected_model)
527}
528
529/// Prompt user to enter API key for a provider
530pub fn prompt_api_key(provider: ProviderType) -> AgentResult<String> {
531    // Bedrock uses AWS credential chain - run setup wizard
532    if matches!(provider, ProviderType::Bedrock) {
533        return run_bedrock_setup_wizard();
534    }
535
536    let env_var = match provider {
537        ProviderType::OpenAI => "OPENAI_API_KEY",
538        ProviderType::Anthropic => "ANTHROPIC_API_KEY",
539        ProviderType::Bedrock => unreachable!(), // Handled above
540    };
541
542    println!(
543        "\n{}",
544        format!("No API key found for {}", provider).yellow()
545    );
546    println!("Please enter your {} API key:", provider);
547    print!("> ");
548    io::stdout().flush().unwrap();
549
550    let mut key = String::new();
551    io::stdin()
552        .read_line(&mut key)
553        .map_err(|e| AgentError::ToolError(e.to_string()))?;
554    let key = key.trim().to_string();
555
556    if key.is_empty() {
557        return Err(AgentError::MissingApiKey(env_var.to_string()));
558    }
559
560    // Set for current session
561    // SAFETY: We're in a single-threaded CLI context during initialization
562    unsafe {
563        std::env::set_var(env_var, &key);
564    }
565
566    // Save to config file for persistence
567    let mut agent_config = load_agent_config();
568    match provider {
569        ProviderType::OpenAI => agent_config.openai_api_key = Some(key.clone()),
570        ProviderType::Anthropic => agent_config.anthropic_api_key = Some(key.clone()),
571        ProviderType::Bedrock => unreachable!(), // Handled above
572    }
573
574    if let Err(e) = save_agent_config(&agent_config) {
575        eprintln!(
576            "{}",
577            format!("Warning: Could not save config: {}", e).yellow()
578        );
579    } else {
580        println!("{}", "API key saved to ~/.syncable.toml".green());
581    }
582
583    Ok(key)
584}