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#[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 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}