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;
7use tokio::sync::mpsc;
8
9#[derive(Debug, Clone)]
10pub struct AiRequest {
11    pub system_prompt: String,
12    pub user_prompt: String,
13    pub json_schema: Option<String>,
14    pub working_dir: String,
15}
16
17#[derive(Debug, Clone)]
18pub struct AiUsage {
19    pub input_tokens: u64,
20    pub output_tokens: u64,
21    pub cost_usd: Option<f64>,
22}
23
24#[derive(Debug, Clone)]
25pub struct AiResponse {
26    pub text: String,
27    pub usage: Option<AiUsage>,
28}
29
30/// Real-time events emitted during an AI request.
31#[derive(Debug, Clone)]
32pub enum AiEvent {
33    ToolCall { tool: String, input: String },
34}
35
36#[async_trait]
37pub trait AiBackend: Send + Sync {
38    fn name(&self) -> &str;
39    async fn is_available(&self) -> bool;
40    async fn request(
41        &self,
42        req: &AiRequest,
43        events: Option<mpsc::UnboundedSender<AiEvent>>,
44    ) -> Result<AiResponse>;
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
48pub enum Backend {
49    Claude,
50    Copilot,
51    Gemini,
52}
53
54pub struct BackendConfig {
55    pub backend: Option<Backend>,
56    pub model: Option<String>,
57    pub budget: f64,
58    pub debug: bool,
59}
60
61pub async fn resolve_backend(config: &BackendConfig) -> Result<Box<dyn AiBackend>> {
62    let preferred = config.backend;
63
64    let claude = claude::ClaudeBackend::new(config.model.clone(), config.budget, config.debug);
65    let copilot = copilot::CopilotBackend::new(config.model.clone(), config.debug);
66    let gemini = gemini::GeminiBackend::new(config.model.clone(), config.debug);
67
68    // Helper: try all backends in order, returning the first available one
69    let try_fallbacks = |backends: Vec<Box<dyn AiBackend>>| async move {
70        for backend in backends {
71            if backend.is_available().await {
72                return Ok(backend);
73            }
74        }
75        anyhow::bail!(crate::error::SrAiError::NoBackendAvailable)
76    };
77
78    match preferred {
79        Some(Backend::Claude) => {
80            if claude.is_available().await {
81                return Ok(Box::new(claude));
82            }
83            eprintln!("Warning: claude CLI not found, falling back...");
84            try_fallbacks(vec![Box::new(copilot), Box::new(gemini)]).await
85        }
86        Some(Backend::Copilot) => {
87            if copilot.is_available().await {
88                return Ok(Box::new(copilot));
89            }
90            eprintln!("Warning: gh models not available, falling back...");
91            try_fallbacks(vec![Box::new(claude), Box::new(gemini)]).await
92        }
93        Some(Backend::Gemini) => {
94            if gemini.is_available().await {
95                return Ok(Box::new(gemini));
96            }
97            eprintln!("Warning: gemini CLI not found, falling back...");
98            try_fallbacks(vec![Box::new(claude), Box::new(copilot)]).await
99        }
100        None => try_fallbacks(vec![Box::new(claude), Box::new(copilot), Box::new(gemini)]).await,
101    }
102}