use std::collections::HashMap;
pub const JOULES_PER_KWH: f64 = 3_600_000.0;
pub const DEFAULT_CARBON_INTENSITY: f64 = 400.0;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CloudProvider {
Aws,
Gcp,
Azure,
OnPrem,
}
impl CloudProvider {
pub fn name(&self) -> &'static str {
match self {
Self::Aws => "AWS",
Self::Gcp => "GCP",
Self::Azure => "Azure",
Self::OnPrem => "On-Premise",
}
}
}
#[derive(Debug, Clone)]
pub struct GpuPricing {
pub provider: CloudProvider,
pub gpu_type: String,
pub price_per_hour: f64,
pub power_watts: f64,
}
impl GpuPricing {
pub fn new(
provider: CloudProvider,
gpu_type: &str,
price_per_hour: f64,
power_watts: f64,
) -> Self {
Self {
provider,
gpu_type: gpu_type.to_string(),
price_per_hour,
power_watts,
}
}
pub fn price_per_second(&self) -> f64 {
self.price_per_hour / 3600.0
}
pub fn joules_per_second(&self) -> f64 {
self.power_watts
}
}
pub fn default_gpu_pricing() -> Vec<GpuPricing> {
vec![
GpuPricing::new(CloudProvider::Aws, "A100-40GB", 4.10, 400.0),
GpuPricing::new(CloudProvider::Aws, "A100-80GB", 5.12, 400.0),
GpuPricing::new(CloudProvider::Aws, "H100", 8.22, 700.0),
GpuPricing::new(CloudProvider::Gcp, "A100-40GB", 3.67, 400.0),
GpuPricing::new(CloudProvider::Gcp, "A100-80GB", 4.87, 400.0),
GpuPricing::new(CloudProvider::Gcp, "H100", 7.65, 700.0),
GpuPricing::new(CloudProvider::Azure, "A100-40GB", 3.85, 400.0),
GpuPricing::new(CloudProvider::Azure, "A100-80GB", 4.95, 400.0),
GpuPricing::new(CloudProvider::Azure, "H100", 8.00, 700.0),
GpuPricing::new(CloudProvider::OnPrem, "A100-40GB", 0.04, 400.0),
GpuPricing::new(CloudProvider::OnPrem, "H100", 0.07, 700.0),
]
}
#[derive(Debug, Clone, Default)]
pub struct EnergyMeasurement {
pub joules: f64,
pub duration_sec: f64,
pub power_watts: f64,
}
impl EnergyMeasurement {
pub fn from_power_duration(power_watts: f64, duration_sec: f64) -> Self {
Self {
joules: power_watts * duration_sec,
duration_sec,
power_watts,
}
}
pub fn from_joules_duration(joules: f64, duration_sec: f64) -> Self {
let power_watts = if duration_sec > 0.0 {
joules / duration_sec
} else {
0.0
};
Self {
joules,
duration_sec,
power_watts,
}
}
pub fn kwh(&self) -> f64 {
self.joules / JOULES_PER_KWH
}
}
#[derive(Debug, Clone)]
pub struct CostResult {
pub total_cost: f64,
pub cost_per_token: f64,
pub cost_per_million_tokens: f64,
pub energy_joules: f64,
pub energy_kwh: f64,
pub carbon_g: f64,
pub duration_sec: f64,
pub token_count: u64,
}
impl CostResult {
pub fn new(
cost: f64,
energy_joules: f64,
carbon_g: f64,
duration_sec: f64,
token_count: u64,
) -> Self {
let cost_per_token = if token_count > 0 {
cost / token_count as f64
} else {
0.0
};
Self {
total_cost: cost,
cost_per_token,
cost_per_million_tokens: cost_per_token * 1_000_000.0,
energy_joules,
energy_kwh: energy_joules / JOULES_PER_KWH,
carbon_g,
duration_sec,
token_count,
}
}
pub fn to_json(&self) -> String {
format!(
r#"{{"total_cost":{:.6},"cost_per_million_tokens":{:.4},"energy_kwh":{:.6},"carbon_g":{:.2},"duration_sec":{:.2},"token_count":{}}}"#,
self.total_cost,
self.cost_per_million_tokens,
self.energy_kwh,
self.carbon_g,
self.duration_sec,
self.token_count
)
}
}
#[derive(Debug, Clone)]
pub struct CostComparison {
pub baseline: CostResult,
pub current: CostResult,
pub cost_change_percent: f64,
pub energy_change_percent: f64,
pub is_regression: bool,
}
impl CostComparison {
pub fn new(baseline: CostResult, current: CostResult) -> Self {
let cost_change_percent = if baseline.total_cost > 0.0 {
((current.total_cost - baseline.total_cost) / baseline.total_cost) * 100.0
} else {
0.0
};
let energy_change_percent = if baseline.energy_joules > 0.0 {
((current.energy_joules - baseline.energy_joules) / baseline.energy_joules) * 100.0
} else {
0.0
};
Self {
is_regression: cost_change_percent > 5.0, baseline,
current,
cost_change_percent,
energy_change_percent,
}
}
}
#[derive(Debug, Clone)]
pub struct BudgetAlert {
pub message: String,
pub current_spend: f64,
pub budget_limit: f64,
pub percent_used: f64,
}
#[derive(Debug)]
pub struct CostTracker {
pricing: HashMap<String, GpuPricing>,
current_gpu: String,
current_provider: CloudProvider,
carbon_intensity: f64,
history: Vec<CostResult>,
max_history: usize,
budget_limit: Option<f64>,
total_spend: f64,
}
impl Default for CostTracker {
fn default() -> Self {
Self::new()
}
}
impl CostTracker {
pub fn new() -> Self {
let pricing: HashMap<String, GpuPricing> = default_gpu_pricing()
.into_iter()
.map(|p| (format!("{}-{}", p.provider.name(), p.gpu_type), p))
.collect();
Self {
pricing,
current_gpu: "A100-40GB".to_string(),
current_provider: CloudProvider::Aws,
carbon_intensity: DEFAULT_CARBON_INTENSITY,
history: Vec::new(),
max_history: 1000,
budget_limit: None,
total_spend: 0.0,
}
}
pub fn with_gpu(mut self, provider: CloudProvider, gpu_type: &str) -> Self {
self.current_provider = provider;
self.current_gpu = gpu_type.to_string();
self
}
pub fn with_carbon_intensity(mut self, intensity: f64) -> Self {
self.carbon_intensity = intensity;
self
}
pub fn with_budget(mut self, limit: f64) -> Self {
self.budget_limit = Some(limit);
self
}
fn current_pricing(&self) -> Option<&GpuPricing> {
let key = format!("{}-{}", self.current_provider.name(), self.current_gpu);
self.pricing.get(&key)
}
pub fn calculate_cost(&mut self, duration_sec: f64, token_count: u64) -> CostResult {
let pricing = self.current_pricing().cloned().unwrap_or_else(|| {
GpuPricing::new(self.current_provider, &self.current_gpu, 5.0, 400.0)
});
let cost = pricing.price_per_second() * duration_sec;
let energy_joules = pricing.joules_per_second() * duration_sec;
let energy_kwh = energy_joules / JOULES_PER_KWH;
let carbon_g = energy_kwh * self.carbon_intensity;
let result = CostResult::new(cost, energy_joules, carbon_g, duration_sec, token_count);
self.total_spend += cost;
self.history.push(result.clone());
while self.history.len() > self.max_history {
self.history.remove(0);
}
result
}
pub fn calculate_from_energy(
&mut self,
energy: &EnergyMeasurement,
token_count: u64,
) -> CostResult {
let pricing = self.current_pricing().cloned().unwrap_or_else(|| {
GpuPricing::new(self.current_provider, &self.current_gpu, 5.0, 400.0)
});
let cost = pricing.price_per_second() * energy.duration_sec;
let energy_kwh = energy.kwh();
let carbon_g = energy_kwh * self.carbon_intensity;
let result = CostResult::new(
cost,
energy.joules,
carbon_g,
energy.duration_sec,
token_count,
);
self.total_spend += cost;
self.history.push(result.clone());
while self.history.len() > self.max_history {
self.history.remove(0);
}
result
}
pub fn total_spend(&self) -> f64 {
self.total_spend
}
pub fn check_budget(&self) -> Option<BudgetAlert> {
let limit = self.budget_limit?;
let percent_used = (self.total_spend / limit) * 100.0;
if percent_used >= 80.0 {
Some(BudgetAlert {
message: format!(
"Budget alert: {:.1}% used (${:.2} of ${:.2})",
percent_used, self.total_spend, limit
),
current_spend: self.total_spend,
budget_limit: limit,
percent_used,
})
} else {
None
}
}
pub fn detect_cost_creep(&self) -> Option<f64> {
if self.history.len() < 10 {
return None;
}
let recent: f64 = self
.history
.iter()
.rev()
.take(10)
.map(|r| r.cost_per_million_tokens)
.sum::<f64>()
/ 10.0;
let older_start = self.history.len().saturating_sub(20);
let older: f64 = self.history[older_start..older_start + 10.min(self.history.len() - 10)]
.iter()
.map(|r| r.cost_per_million_tokens)
.sum::<f64>()
/ 10.0;
if older > 0.0 {
let change = ((recent - older) / older) * 100.0;
if change > 10.0 {
return Some(change);
}
}
None
}
pub fn history(&self) -> &[CostResult] {
&self.history
}
pub fn export_csv(&self) -> String {
let mut lines = vec![
"duration_sec,token_count,total_cost,cost_per_million,energy_kwh,carbon_g".to_string(),
];
for result in &self.history {
lines.push(format!(
"{:.2},{},{:.6},{:.4},{:.6},{:.2}",
result.duration_sec,
result.token_count,
result.total_cost,
result.cost_per_million_tokens,
result.energy_kwh,
result.carbon_g
));
}
lines.join("\n")
}
pub fn export_json(&self) -> String {
let entries: Vec<String> = self.history.iter().map(|r| r.to_json()).collect();
format!("[{}]", entries.join(","))
}
pub fn clear_history(&mut self) {
self.history.clear();
self.total_spend = 0.0;
}
}
#[cfg(test)]
mod tests;