1pub mod claude;
2pub mod copilot;
3pub mod gemini;
4
5use anyhow::Result;
6use async_trait::async_trait;
7
8#[derive(Debug, Clone)]
9pub struct AiRequest {
10 pub system_prompt: String,
11 pub user_prompt: String,
12 pub json_schema: Option<String>,
13 pub working_dir: String,
14}
15
16#[derive(Debug, Clone)]
17pub struct AiResponse {
18 pub text: String,
19}
20
21#[async_trait]
22pub trait AiBackend: Send + Sync {
23 fn name(&self) -> &str;
24 async fn is_available(&self) -> bool;
25 async fn request(&self, req: &AiRequest) -> Result<AiResponse>;
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
29pub enum Backend {
30 Claude,
31 Copilot,
32 Gemini,
33}
34
35pub struct BackendConfig {
36 pub backend: Option<Backend>,
37 pub model: Option<String>,
38 pub budget: f64,
39 pub debug: bool,
40}
41
42pub async fn resolve_backend(config: &BackendConfig) -> Result<Box<dyn AiBackend>> {
43 let preferred = config.backend;
44
45 let claude = claude::ClaudeBackend::new(config.model.clone(), config.budget, config.debug);
46 let copilot = copilot::CopilotBackend::new(config.model.clone(), config.debug);
47 let gemini = gemini::GeminiBackend::new(config.model.clone(), config.debug);
48
49 let try_fallbacks = |backends: Vec<Box<dyn AiBackend>>| async move {
51 for backend in backends {
52 if backend.is_available().await {
53 return Ok(backend);
54 }
55 }
56 anyhow::bail!(crate::error::SrAiError::NoBackendAvailable)
57 };
58
59 match preferred {
60 Some(Backend::Claude) => {
61 if claude.is_available().await {
62 return Ok(Box::new(claude));
63 }
64 eprintln!("Warning: claude CLI not found, falling back...");
65 try_fallbacks(vec![Box::new(copilot), Box::new(gemini)]).await
66 }
67 Some(Backend::Copilot) => {
68 if copilot.is_available().await {
69 return Ok(Box::new(copilot));
70 }
71 eprintln!("Warning: gh models not available, falling back...");
72 try_fallbacks(vec![Box::new(claude), Box::new(gemini)]).await
73 }
74 Some(Backend::Gemini) => {
75 if gemini.is_available().await {
76 return Ok(Box::new(gemini));
77 }
78 eprintln!("Warning: gemini CLI not found, falling back...");
79 try_fallbacks(vec![Box::new(claude), Box::new(copilot)]).await
80 }
81 None => try_fallbacks(vec![Box::new(claude), Box::new(copilot), Box::new(gemini)]).await,
82 }
83}