use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ThermodynamicCostRecord {
pub tenant_cid: String,
pub workflow_id: String,
#[serde(default)]
pub agent_cid: String,
#[serde(default)]
pub input_tokens: i64,
#[serde(default)]
pub output_tokens: i64,
#[serde(default)]
pub total_tokens: i64,
#[serde(default)]
pub cost_usd: f64,
#[serde(default)]
pub gpu_seconds: f64,
pub timestamp: f64,
}
pub fn get_postgres_schema() -> &'static str {
include_str!("../migrations/001_thermodynamic_costs.sql")
}
pub fn verify_cost_and_update(
budget_limit_usd: f64,
records: Vec<ThermodynamicCostRecord>,
new_record: ThermodynamicCostRecord,
) -> Result<Vec<ThermodynamicCostRecord>, String> {
if new_record.cost_usd < 0.0 {
return Err(format!(
"Cost must be non-negative, got {}",
new_record.cost_usd
));
}
let mut tenant_totals = std::collections::HashMap::new();
for r in &records {
let entry = tenant_totals.entry(&r.tenant_cid).or_insert(0.0);
*entry += r.cost_usd;
}
let current = tenant_totals
.get(&new_record.tenant_cid)
.cloned()
.unwrap_or(0.0);
if current + new_record.cost_usd > budget_limit_usd {
return Err(format!(
"Tenant '{}' budget exceeded: ${:.4} > ${:.2} limit",
new_record.tenant_cid,
current + new_record.cost_usd,
budget_limit_usd
));
}
let mut updated_records = records;
updated_records.push(new_record);
Ok(updated_records)
}