use std::sync::atomic::{AtomicBool, Ordering};
use rust_decimal::Decimal;
use crate::core::models::{CostConfidence, CostEvent, EventType, Task};
static DEV_MODE: AtomicBool = AtomicBool::new(false);
pub fn is_dev_mode() -> bool {
DEV_MODE.load(Ordering::Relaxed)
}
pub fn enable_dev_mode() {
DEV_MODE.store(true, Ordering::Relaxed);
print_line("\x1b[36m[dexcost]\x1b[0m dev mode \u{2014} cloud sync disabled");
}
pub fn log_event(event: &CostEvent, task_type: &str) {
if !is_dev_mode() {
return;
}
let cost = &event.cost_usd;
match event.event_type {
EventType::LlmCall => {
let provider = event.provider.as_deref().unwrap_or("?");
let model = event.model.as_deref().unwrap_or("?");
let in_tok = event.input_tokens.unwrap_or(0);
let out_tok = event.output_tokens.unwrap_or(0);
let cached = event.cached_tokens.unwrap_or(0);
let retry_tag = if event.is_retry {
" \x1b[33m(retry)\x1b[0m"
} else {
""
};
let cache_tag = if cached > 0 {
format!(" cached: {}", cached)
} else {
String::new()
};
print_line(&format!(
"\x1b[32m\u{2713}\x1b[0m llm_call {}/{} {} in / {} out{} ${}{}{}",
provider,
model,
in_tok,
out_tok,
cache_tag,
cost,
retry_tag,
task_tag(task_type),
));
}
EventType::ExternalCost | EventType::ComputeCost => {
let service = event.service_name.as_deref().unwrap_or("unknown");
let event_type_str = match event.event_type {
EventType::ExternalCost => "external_cost",
EventType::ComputeCost => "compute_cost",
_ => "external_cost",
};
if event.cost_confidence == CostConfidence::Unknown || *cost == Decimal::ZERO {
print_line(&format!(
"\x1b[33m\u{26a0}\x1b[0m {} {} $0.00 \x1b[33m(no rate configured)\x1b[0m{}",
event_type_str,
service,
task_tag(task_type),
));
} else {
print_line(&format!(
"\x1b[32m\u{2713}\x1b[0m {} {} ${}{}",
event_type_str,
service,
cost,
task_tag(task_type),
));
}
}
EventType::RetryMarker => {
let reason = event.retry_reason.as_deref().unwrap_or("unknown");
print_line(&format!(
"\x1b[33m\u{21bb}\x1b[0m retry_marker reason: {} ${}{}",
reason,
cost,
task_tag(task_type),
));
}
EventType::Network => {
let host = event
.details
.get("host")
.and_then(|v| v.as_str())
.unwrap_or("?");
print_line(&format!(
"\x1b[36m\u{2192}\x1b[0m network {} ${}{}",
host,
cost,
task_tag(task_type),
));
}
EventType::GpuCost => {
let runtime = event
.details
.get("gpu_runtime")
.and_then(|v| v.as_str())
.unwrap_or("?");
print_line(&format!(
"\x1b[35m\u{25c6}\x1b[0m gpu_cost {} ${}{}",
runtime,
cost,
task_tag(task_type),
));
}
EventType::GpuUtilizationSignal => {
let pct = event
.details
.get("sm_util_pct")
.and_then(|v| v.as_f64());
let pct_s = match pct {
Some(p) => format!("{:.1}%", p),
None => "n/a".to_string(),
};
print_line(&format!(
"\x1b[34m\u{25cb}\x1b[0m gpu_signal sm_util_pct: {}{}",
pct_s,
task_tag(task_type),
));
}
}
}
pub fn log_task_complete(task: &Task) {
if !is_dev_mode() {
return;
}
let status = match task.status {
crate::core::models::TaskStatus::Pending => "pending",
crate::core::models::TaskStatus::Success => "success",
crate::core::models::TaskStatus::Failed => "failed",
crate::core::models::TaskStatus::Running => "running",
};
let retry_info = if task.retry_count > 0 {
format!(
" retries: {} retry cost: ${}",
task.retry_count, task.retry_cost_usd
)
} else {
String::new()
};
print_line(&format!(
"\x1b[36m\u{2713}\x1b[0m task {} {} total: ${}{}",
status, task.task_type, task.total_cost_usd, retry_info,
));
}
fn task_tag(task_type: &str) -> String {
if task_type.is_empty() {
String::new()
} else {
format!(" \x1b[90m(task: {})\x1b[0m", task_type)
}
}
fn print_line(msg: &str) {
eprintln!("\x1b[36m[dexcost]\x1b[0m {}", msg);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dev_mode_default_off() {
let _ = is_dev_mode();
}
#[test]
fn test_log_event_noop_when_off() {
DEV_MODE.store(false, Ordering::Relaxed);
let event = CostEvent::new("task-1", EventType::LlmCall);
log_event(&event, "test_task");
}
#[test]
fn test_log_task_complete_noop_when_off() {
DEV_MODE.store(false, Ordering::Relaxed);
let task = Task::new("test_task");
log_task_complete(&task);
}
#[test]
fn test_task_tag_empty() {
assert_eq!(task_tag(""), String::new());
}
#[test]
fn test_task_tag_present() {
let tag = task_tag("resolve_ticket");
assert!(tag.contains("resolve_ticket"));
}
}