mod client; mod history; pub use client::{ ChatMessage, ConfigError, LlmBackend, LlmClient, LlmError, DEFAULT_BASE_URL, DEFAULT_MODEL, ENV_BASE_URL, ENV_MODEL, ENV_TOKEN, OLLAMA_DEFAULT_BASE_URL, OLLAMA_DEFAULT_MODEL, }; pub use history::ConversationHistory; use once_cell::sync::Lazy; use parking_lot::RwLock; use std::sync::Arc; /// Shared mutable LLM client. Modules read via `current()`; the user can /// hot-swap backends at runtime via `swap_to(LlmBackend::Ollama)` etc. /// /// `None` means LLM is disabled (no GROQ_TOKEN and no Ollama reachable, or /// `init_global()` was never called). static GLOBAL: Lazy>>> = Lazy::new(|| RwLock::new(None)); /// Initialise the global client from env vars (`JARVIS_LLM`, `GROQ_TOKEN`, ...). /// Idempotent — replaces any previous global. Returns Ok even when the chosen /// backend can't be probed: an Ollama server may not be running yet but we /// stash the client anyway and let calls fail on first request. pub fn init_global() -> Result<(), ConfigError> { let c = LlmClient::from_env()?; log::info!("LLM global initialised: backend={}, model={}", c.backend().name(), c.model()); *GLOBAL.write() = Some(Arc::new(c)); Ok(()) } /// Return a clone of the active client. Cheap (Arc). pub fn current() -> Option> { GLOBAL.read().clone() } /// Name of the currently active backend, or "none" if not initialised. pub fn current_backend_name() -> &'static str { match GLOBAL.read().as_ref().map(|c| c.backend()) { Some(LlmBackend::Groq) => "groq", Some(LlmBackend::Ollama) => "ollama", None => "none", } } /// Hot-swap to a different backend. Returns the new backend's name on success. pub fn swap_to(backend: LlmBackend) -> Result<&'static str, ConfigError> { let c = match backend { LlmBackend::Groq => LlmClient::groq()?, LlmBackend::Ollama => LlmClient::ollama(), }; let name = c.backend().name(); log::info!("LLM swapped → backend={}, model={}", name, c.model()); *GLOBAL.write() = Some(Arc::new(c)); Ok(name) } /// Parse a backend name (case-insensitive) into the enum. pub fn parse_backend(name: &str) -> Option { match name.trim().to_lowercase().as_str() { "groq" | "cloud" | "облако" | "клауд" => Some(LlmBackend::Groq), "ollama" | "local" | "локал" | "локальн" | "локальный" => Some(LlmBackend::Ollama), _ => None, } }