use std::collections::HashMap;
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::Mutex;
use crate::core::models::{CostConfidence, CostEvent, EventType, PricingSource};
use crate::pricing::engine::PricingEngine;
use crate::transport::buffer::EventBuffer;
#[derive(Debug, Clone)]
struct PendingRun {
start_time: Instant,
model: String,
}
pub struct DexcostCallbackHandler {
buffer: Arc<Mutex<EventBuffer>>,
pricing: Arc<Mutex<PricingEngine>>,
task_id: String,
pending: Mutex<HashMap<String, PendingRun>>,
}
impl DexcostCallbackHandler {
pub fn new(
buffer: Arc<Mutex<EventBuffer>>,
pricing: Arc<Mutex<PricingEngine>>,
task_id: impl Into<String>,
) -> Self {
Self {
buffer,
pricing,
task_id: task_id.into(),
pending: Mutex::new(HashMap::new()),
}
}
pub async fn on_llm_start(&self, run_id: &str, model: &str) {
let mut pending = self.pending.lock().await;
pending.insert(
run_id.to_string(),
PendingRun {
start_time: Instant::now(),
model: model.to_string(),
},
);
}
pub async fn on_llm_end(
&self,
run_id: &str,
input_tokens: i64,
output_tokens: i64,
) -> CostEvent {
let pending = self.pending.lock().await.remove(run_id);
let (model, latency_ms) = match pending {
Some(p) => (
p.model,
Some(p.start_time.elapsed().as_millis() as i64),
),
None => ("unknown".to_string(), None),
};
let has_usage = input_tokens > 0 || output_tokens > 0;
let mut event = CostEvent::new(&self.task_id, EventType::LlmCall);
event.provider = Some("langchain".to_string());
event.model = Some(model.clone());
event.latency_ms = latency_ms;
if has_usage {
event.input_tokens = Some(input_tokens);
event.output_tokens = Some(output_tokens);
let result = {
let engine = self.pricing.lock().await;
engine
.get_cost(&model, input_tokens, output_tokens, 0, 0)
.await
};
event.cost_usd = result.cost_usd;
event.cost_confidence = result.cost_confidence;
event.pricing_source = Some(result.pricing_source);
event.pricing_version = Some(result.pricing_version);
} else {
event.cost_confidence = CostConfidence::Unknown;
event.pricing_source = Some(PricingSource::Unknown);
}
self.buffer.lock().await.add_event(event.clone());
event
}
pub async fn on_llm_error(&self, run_id: &str, error_type: &str, error: &str) -> CostEvent {
let pending = self.pending.lock().await.remove(run_id);
let (model, latency_ms) = match pending {
Some(p) => (
p.model,
Some(p.start_time.elapsed().as_millis() as i64),
),
None => ("unknown".to_string(), None),
};
let mut event = CostEvent::new(&self.task_id, EventType::LlmCall);
event.provider = Some("langchain".to_string());
event.model = Some(model);
event.latency_ms = latency_ms;
event.cost_confidence = CostConfidence::Unknown;
event.pricing_source = Some(PricingSource::Unknown);
event.details.insert(
"error".to_string(),
serde_json::Value::String(error.to_string()),
);
event.details.insert(
"error_type".to_string(),
serde_json::Value::String(error_type.to_string()),
);
self.buffer.lock().await.add_event(event.clone());
event
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fixtures() -> (Arc<Mutex<EventBuffer>>, Arc<Mutex<PricingEngine>>) {
(
Arc::new(Mutex::new(EventBuffer::new().expect("buffer"))),
Arc::new(Mutex::new(PricingEngine::new())),
)
}
#[tokio::test]
async fn test_on_llm_end_records_event() {
let (buffer, pricing) = fixtures();
let handler = DexcostCallbackHandler::new(buffer.clone(), pricing, "task-lc");
handler.on_llm_start("run-1", "gpt-4o").await;
let event = handler.on_llm_end("run-1", 1000, 500).await;
assert_eq!(event.event_type, EventType::LlmCall);
assert_eq!(event.provider.as_deref(), Some("langchain"));
assert_eq!(event.model.as_deref(), Some("gpt-4o"));
assert_eq!(event.input_tokens, Some(1000));
assert_eq!(event.output_tokens, Some(500));
let buf = buffer.lock().await;
assert_eq!(buf.event_count(), 1);
}
#[tokio::test]
async fn test_on_llm_error_records_failure_with_error_type() {
let (buffer, pricing) = fixtures();
let handler = DexcostCallbackHandler::new(buffer.clone(), pricing, "task-lc");
handler.on_llm_start("run-err", "gpt-4o").await;
let event = handler
.on_llm_error("run-err", "rate_limit", "429 Too Many Requests")
.await;
assert_eq!(event.event_type, EventType::LlmCall);
assert_eq!(event.cost_confidence, CostConfidence::Unknown);
assert_eq!(
event.details.get("error_type"),
Some(&serde_json::Value::String("rate_limit".to_string()))
);
assert_eq!(
event.details.get("error"),
Some(&serde_json::Value::String(
"429 Too Many Requests".to_string()
))
);
let buf = buffer.lock().await;
assert_eq!(buf.event_count(), 1);
}
#[tokio::test]
async fn test_on_llm_end_without_start_uses_unknown_model() {
let (buffer, pricing) = fixtures();
let handler = DexcostCallbackHandler::new(buffer, pricing, "task-lc");
let event = handler.on_llm_end("missing-run", 0, 0).await;
assert_eq!(event.model.as_deref(), Some("unknown"));
assert_eq!(event.cost_confidence, CostConfidence::Unknown);
}
}