#![allow(dead_code)] //! InstrumentedLlm -- an LLM provider wrapper that captures per-call metrics. //! //! Wraps any `Arc` and transparently intercepts `complete()` //! and `complete_with_tools()` to record timing, token counts, and call metadata. use std::sync::Arc; use std::sync::atomic::{AtomicU32, Ordering}; use std::time::Instant; use async_trait::async_trait; use rust_decimal::Decimal; use tokio::sync::Mutex; use ironclaw::error::LlmError; use ironclaw::llm::{ CompletionRequest, CompletionResponse, LlmProvider, ModelMetadata, ToolCompletionRequest, ToolCompletionResponse, }; /// Metrics captured for a single LLM call. #[derive(Debug, Clone)] pub struct LlmCallRecord { pub input_tokens: u32, pub output_tokens: u32, pub duration_ms: u64, pub had_tool_calls: bool, } /// A transparent wrapper around any `LlmProvider` that records per-call metrics. pub struct InstrumentedLlm { inner: Arc, records: Mutex>, total_input_tokens: AtomicU32, total_output_tokens: AtomicU32, call_count: AtomicU32, } impl InstrumentedLlm { pub fn new(inner: Arc) -> Self { Self { inner, records: Mutex::new(Vec::new()), total_input_tokens: AtomicU32::new(0), total_output_tokens: AtomicU32::new(0), call_count: AtomicU32::new(0), } } pub fn call_count(&self) -> u32 { self.call_count.load(Ordering::Relaxed) } pub fn total_input_tokens(&self) -> u32 { self.total_input_tokens.load(Ordering::Relaxed) } pub fn total_output_tokens(&self) -> u32 { self.total_output_tokens.load(Ordering::Relaxed) } pub fn estimated_cost_usd(&self) -> f64 { let (input_cost, output_cost) = self.inner.cost_per_token(); let input_total = Decimal::from(self.total_input_tokens()); let output_total = Decimal::from(self.total_output_tokens()); let cost = input_cost * input_total + output_cost * output_total; use std::str::FromStr; f64::from_str(&cost.to_string()).unwrap_or(0.0) } pub async fn records(&self) -> Vec { self.records.lock().await.clone() } async fn record_call( &self, input_tokens: u32, output_tokens: u32, duration_ms: u64, had_tool_calls: bool, ) { self.call_count.fetch_add(1, Ordering::Relaxed); self.total_input_tokens .fetch_add(input_tokens, Ordering::Relaxed); self.total_output_tokens .fetch_add(output_tokens, Ordering::Relaxed); self.records.lock().await.push(LlmCallRecord { input_tokens, output_tokens, duration_ms, had_tool_calls, }); } } #[async_trait] impl LlmProvider for InstrumentedLlm { fn model_name(&self) -> &str { self.inner.model_name() } fn cost_per_token(&self) -> (Decimal, Decimal) { self.inner.cost_per_token() } async fn complete(&self, request: CompletionRequest) -> Result { let start = Instant::now(); let result = self.inner.complete(request).await; let elapsed = start.elapsed().as_millis() as u64; if let Ok(ref resp) = result { self.record_call(resp.input_tokens, resp.output_tokens, elapsed, false) .await; } result } async fn complete_with_tools( &self, request: ToolCompletionRequest, ) -> Result { let start = Instant::now(); let result = self.inner.complete_with_tools(request).await; let elapsed = start.elapsed().as_millis() as u64; if let Ok(ref resp) = result { let had_tool_calls = !resp.tool_calls.is_empty(); self.record_call( resp.input_tokens, resp.output_tokens, elapsed, had_tool_calls, ) .await; } result } async fn list_models(&self) -> Result, LlmError> { self.inner.list_models().await } async fn model_metadata(&self) -> Result { self.inner.model_metadata().await } fn effective_model_name(&self, requested_model: Option<&str>) -> String { self.inner.effective_model_name(requested_model) } fn active_model_name(&self) -> String { self.inner.active_model_name() } fn set_model(&self, model: &str) -> Result<(), LlmError> { self.inner.set_model(model) } fn calculate_cost(&self, input_tokens: u32, output_tokens: u32) -> Decimal { self.inner.calculate_cost(input_tokens, output_tokens) } }