Skip to main content

sr_ai/ai/
mod.rs

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    // Helper: try all backends in order, returning the first available one
50    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}