recon_cli/script/bindings/ai/
backend.rs1use std::time::Duration;
4use std::collections::HashMap;
5
6use crate::config::AiConfig;
7use super::request::Request;
8
9pub struct BackendCtx<'a> {
12 pub config: &'a AiConfig,
13 pub effective_model: Option<String>,
14 pub effective_timeout: Duration,
15 pub verbose: u8,
16}
17
18#[derive(Debug, Clone)]
20pub struct Response {
21 pub text: String,
22 pub backend: String,
23 pub model: Option<String>,
24 pub duration: Duration,
25 pub exit_code: i32,
26 pub chars_in: usize,
30}
31
32pub trait AiBackend: Send + Sync {
35 fn name(&self) -> &'static str;
36 fn invoke(&self, req: &Request, ctx: &BackendCtx<'_>) -> Result<Response, String>;
37}
38
39pub struct Registry {
43 built_ins: HashMap<&'static str, Box<dyn AiBackend>>,
44}
45
46impl Registry {
47 pub fn empty() -> Self {
50 Self { built_ins: HashMap::new() }
51 }
52
53 pub fn register(&mut self, backend: Box<dyn AiBackend>) {
54 self.built_ins.insert(backend.name(), backend);
55 }
56
57 pub fn get(&self, name: &str) -> Option<&dyn AiBackend> {
58 self.built_ins.get(name).map(|b| b.as_ref())
59 }
60
61 pub fn has(&self, name: &str) -> bool {
62 self.built_ins.contains_key(name)
63 }
64}
65
66pub fn dispatch(
69 backend_name: &str,
70 req: &Request,
71 config: &AiConfig,
72 ctx: &BackendCtx<'_>,
73 registry: &Registry,
74) -> Result<Response, String> {
75 if let Some(b) = registry.get(backend_name) {
76 return b.invoke(req, ctx);
77 }
78 if let Some(bcfg) = config.backends.get(backend_name) {
80 if !bcfg.cmd.is_empty() {
81 return super::backends::cmd::invoke(backend_name, bcfg, req, ctx);
82 }
83 }
84 Err(format!(
85 "ai: backend '{backend_name}' not found (no built-in, and no \
86 `[ai.backends.{backend_name}]` with non-empty `cmd` in ~/.recon/config.toml)"
87 ))
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93
94 struct FakeBackend;
95 impl AiBackend for FakeBackend {
96 fn name(&self) -> &'static str { "fake" }
97 fn invoke(&self, _req: &Request, _ctx: &BackendCtx<'_>) -> Result<Response, String> {
98 Ok(Response {
99 text: "ok".into(),
100 backend: "fake".into(),
101 model: None,
102 duration: Duration::from_millis(1),
103 exit_code: 0,
104 chars_in: 0,
105 })
106 }
107 }
108
109 #[test]
110 fn registry_round_trip() {
111 let mut reg = Registry::empty();
112 reg.register(Box::new(FakeBackend));
113 assert!(reg.has("fake"));
114 assert!(!reg.has("missing"));
115 let b = reg.get("fake").expect("present");
116 assert_eq!(b.name(), "fake");
117 }
118}