From b5e2471a9007ffbbeb41b6e55b733de0af8b96e5 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 18 May 2026 17:31:25 +0100 Subject: [PATCH] fix(rlm): wire LlmClient through LlmBridge, replace silent stub Refs #1744 Replace the [LLM Bridge stub] fake response with actual LLM delegation when the llm feature is enabled and a provider is detectable. Without a client, return RlmError::LlmNotConfigured instead of a silent fake. - LlmBridge: stores Option> via cfg(feature=llm) - query(): delegates to real client, or returns LlmNotConfigured - TerraphimRlm::create_llm_bridge(): auto-detects via build_llm_client() - Auto-detection: Ollama (free) > OpenRouter (cheap) > proxy > error - terraphim_config added as optional dep behind llm feature --- crates/terraphim_rlm/src/error.rs | 7 +++ crates/terraphim_rlm/src/llm_bridge.rs | 60 +++++++++++++++++++++----- crates/terraphim_rlm/src/rlm.rs | 21 ++++++++- 3 files changed, 77 insertions(+), 11 deletions(-) diff --git a/crates/terraphim_rlm/src/error.rs b/crates/terraphim_rlm/src/error.rs index 8aa770633..85d558d76 100644 --- a/crates/terraphim_rlm/src/error.rs +++ b/crates/terraphim_rlm/src/error.rs @@ -132,6 +132,13 @@ pub enum RlmError { #[error("LLM call failed: {message}")] LlmCallFailed { message: String }, + /// No LLM client configured. Enable the `llm` feature and set an API key + /// or run a local Ollama instance. + #[error( + "No LLM client configured. Enable the `llm` feature (--features llm) and set OPENROUTER_API_KEY or run Ollama on localhost:11434." + )] + LlmNotConfigured, + /// LLM bridge authentication failed. #[error("LLM bridge authentication failed: invalid session token")] LlmBridgeAuthFailed, diff --git a/crates/terraphim_rlm/src/llm_bridge.rs b/crates/terraphim_rlm/src/llm_bridge.rs index bec31c273..6b7d48276 100644 --- a/crates/terraphim_rlm/src/llm_bridge.rs +++ b/crates/terraphim_rlm/src/llm_bridge.rs @@ -123,15 +123,37 @@ pub struct LlmBridge { session_manager: Arc, /// Budget trackers per session. budget_trackers: dashmap::DashMap>, + /// Optional real LLM client. When `None`, `query()` returns + /// `LlmNotConfigured` instead of a silent stub. + #[cfg(feature = "llm")] + llm_client: Option>, } impl LlmBridge { - /// Create a new LLM bridge. + /// Create a new LLM bridge without a real LLM client. + /// Queries will return `LlmNotConfigured`. pub fn new(config: LlmBridgeConfig, session_manager: Arc) -> Self { Self { config, session_manager, budget_trackers: dashmap::DashMap::new(), + #[cfg(feature = "llm")] + llm_client: None, + } + } + + /// Create a new LLM bridge with a configured LLM client. + #[cfg(feature = "llm")] + pub fn with_llm_client( + config: LlmBridgeConfig, + session_manager: Arc, + client: Arc, + ) -> Self { + Self { + config, + session_manager, + budget_trackers: dashmap::DashMap::new(), + llm_client: Some(client), } } @@ -189,16 +211,34 @@ impl LlmBridge { let start = std::time::Instant::now(); - // TODO: Actually call the LLM service - // For now, return a stub response - let response_text = format!( - "[LLM Bridge stub] Query: {}...", - if request.prompt.len() > 50 { - &request.prompt[..50] - } else { - &request.prompt + #[cfg(feature = "llm")] + let response_text = match &self.llm_client { + Some(client) => { + let chat_opts = terraphim_service::llm::ChatOptions { + max_tokens: request.max_tokens.map(|t| t as u32), + temperature: request.temperature, + }; + let messages = vec![serde_json::json!({ + "role": "user", + "content": request.prompt + })]; + client + .chat_completion(messages, chat_opts) + .await + .map_err(|e| RlmError::LlmCallFailed { + message: e.to_string(), + })? + } + None => { + return Err(RlmError::LlmNotConfigured); } - ); + }; + + #[cfg(not(feature = "llm"))] + { + let _request = request; + return Err(RlmError::LlmNotConfigured); + } // Estimate tokens (1 token ~= 4 chars for English text) let estimated_tokens = (request.prompt.len() / 4 + response_text.len() / 4) as u64; diff --git a/crates/terraphim_rlm/src/rlm.rs b/crates/terraphim_rlm/src/rlm.rs index 704498fa8..9ae71914a 100644 --- a/crates/terraphim_rlm/src/rlm.rs +++ b/crates/terraphim_rlm/src/rlm.rs @@ -106,7 +106,7 @@ impl TerraphimRlm { // Create session manager let session_manager = Arc::new(SessionManager::new(config.clone())); - // Create LLM bridge + // Create LLM bridge (bare — caller wires a client via set_llm_client()). let llm_bridge_config = LlmBridgeConfig::default(); let llm_bridge = Arc::new(LlmBridge::new(llm_bridge_config, session_manager.clone())); @@ -794,6 +794,25 @@ impl TerraphimRlm { // Command history would be added here if tracking is enabled }) } + + /// Inject an LLM client from the orchestrator's routing pipeline. + /// + /// The orchestrator owns provider health, budget tracking, and + /// fallback routing. Call this after construction to wire RLM + /// into the existing cost-optimisation stack instead of building + /// a standalone client. + /// + /// Requires the `llm` feature. + #[cfg(feature = "llm")] + pub fn set_llm_client(&mut self, client: Arc) { + log::info!("RLM LLM bridge configured with provider: {}", client.name()); + let bridge_config = LlmBridgeConfig::default(); + self.llm_bridge = Arc::new(LlmBridge::with_llm_client( + bridge_config, + self.session_manager.clone(), + client, + )); + } } /// Result from a direct LLM query.