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}
27
28pub trait AiBackend: Send + Sync {
31 fn name(&self) -> &'static str;
32 fn invoke(&self, req: &Request, ctx: &BackendCtx<'_>) -> Result<Response, String>;
33}
34
35pub struct Registry {
39 built_ins: HashMap<&'static str, Box<dyn AiBackend>>,
40}
41
42impl Registry {
43 pub fn empty() -> Self {
46 Self { built_ins: HashMap::new() }
47 }
48
49 pub fn register(&mut self, backend: Box<dyn AiBackend>) {
50 self.built_ins.insert(backend.name(), backend);
51 }
52
53 pub fn get(&self, name: &str) -> Option<&dyn AiBackend> {
54 self.built_ins.get(name).map(|b| b.as_ref())
55 }
56
57 pub fn has(&self, name: &str) -> bool {
58 self.built_ins.contains_key(name)
59 }
60}
61
62pub fn dispatch(
65 backend_name: &str,
66 req: &Request,
67 config: &AiConfig,
68 ctx: &BackendCtx<'_>,
69 registry: &Registry,
70) -> Result<Response, String> {
71 if let Some(b) = registry.get(backend_name) {
72 return b.invoke(req, ctx);
73 }
74 if let Some(bcfg) = config.backends.get(backend_name) {
76 if !bcfg.cmd.is_empty() {
77 return super::backends::cmd::invoke(backend_name, bcfg, req, ctx);
78 }
79 }
80 Err(format!(
81 "ai: backend '{backend_name}' not found (no built-in, and no \
82 `[ai.backends.{backend_name}]` with non-empty `cmd` in ~/.recon/config.toml)"
83 ))
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 struct FakeBackend;
91 impl AiBackend for FakeBackend {
92 fn name(&self) -> &'static str { "fake" }
93 fn invoke(&self, _req: &Request, _ctx: &BackendCtx<'_>) -> Result<Response, String> {
94 Ok(Response {
95 text: "ok".into(),
96 backend: "fake".into(),
97 model: None,
98 duration: Duration::from_millis(1),
99 exit_code: 0,
100 })
101 }
102 }
103
104 #[test]
105 fn registry_round_trip() {
106 let mut reg = Registry::empty();
107 reg.register(Box::new(FakeBackend));
108 assert!(reg.has("fake"));
109 assert!(!reg.has("missing"));
110 let b = reg.get("fake").expect("present");
111 assert_eq!(b.name(), "fake");
112 }
113}