syncable_cli/agent/session/
providers.rs1use crate::agent::{AgentError, AgentResult, ProviderType};
11use crate::config::{load_agent_config, save_agent_config};
12use colored::Colorize;
13use std::io::{self, Write};
14
15pub 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 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
61pub fn has_api_key(provider: ProviderType) -> bool {
63 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 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 let agent_config = load_agent_config();
87
88 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 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 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
173pub fn load_api_key_to_env(provider: ProviderType) {
175 let agent_config = load_agent_config();
176
177 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 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 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 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 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 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 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 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
269pub 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
281pub(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 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 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 unsafe {
359 std::env::set_var("AWS_PROFILE", profile);
360 }
361 println!("{}", format!("Using profile: {}", profile).green());
362 }
363 "2" => {
364 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 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 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 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 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 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
529pub fn prompt_api_key(provider: ProviderType) -> AgentResult<String> {
531 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!(), };
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 unsafe {
563 std::env::set_var(env_var, &key);
564 }
565
566 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!(), }
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}