use std::fmt;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct GpuPricing {
pub a100: f64,
pub h100: f64,
pub rtx4090: f64,
pub t4: f64,
pub l4: f64,
}
impl Default for GpuPricing {
fn default() -> Self {
Self {
a100: 3.00,
h100: 4.50,
rtx4090: 1.20,
t4: 0.35,
l4: 0.80,
}
}
}
impl fmt::Display for GpuPricing {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"A100=${:.2}/h H100=${:.2}/h RTX4090=${:.2}/h T4=${:.2}/h L4=${:.2}/h",
self.a100, self.h100, self.rtx4090, self.t4, self.l4
)
}
}
#[must_use]
fn model_throughput(model: &str, pricing: &GpuPricing) -> Option<(&'static str, f64, f64)> {
let lower = model.to_ascii_lowercase();
if lower.contains("gpt-4") || lower.contains("claude-3-opus") {
Some(("large", 40.0, pricing.a100))
} else if lower.contains("gpt-3.5") || lower.contains("claude-3-sonnet") {
Some(("medium", 80.0, pricing.l4))
} else if lower.contains("llama-70b") || lower.contains("mixtral") {
Some(("open-large", 50.0, pricing.a100))
} else if lower.contains("llama-7b")
|| lower.contains("llama-8b")
|| lower.contains("mistral-7b")
{
Some(("open-small", 120.0, pricing.t4))
} else {
Some(("unknown", 80.0, pricing.l4))
}
}
#[must_use]
#[tracing::instrument(skip(pricing), fields(tasks, avg_tokens_per_task, model))]
pub fn estimate_crew_cost(
tasks: usize,
avg_tokens_per_task: u64,
model: &str,
pricing: &GpuPricing,
) -> f64 {
let Some((_tier, tps, hourly)) = model_throughput(model, pricing) else {
return 0.0;
};
let total_tokens = tasks as f64 * avg_tokens_per_task as f64;
let seconds = total_tokens / tps;
let hours = seconds / 3600.0;
tracing::debug!(
total_tokens,
gpu_hours = hours,
hourly_rate = hourly,
"crew cost estimate"
);
hours * hourly
}
const BUDGET_MODELS: &[&str] = &[
"llama-7b",
"mistral-7b",
"gpt-3.5-turbo",
"llama-70b",
"mixtral-8x7b",
"claude-3-sonnet",
"gpt-4",
"claude-3-opus",
];
#[must_use]
#[tracing::instrument(skip(pricing))]
pub fn select_cheapest_model(
budget_usd: f64,
tasks: usize,
avg_tokens_per_task: u64,
pricing: &GpuPricing,
) -> Option<&'static str> {
let mut candidates: Vec<(&str, f64)> = BUDGET_MODELS
.iter()
.map(|&m| {
(
m,
estimate_crew_cost(tasks, avg_tokens_per_task, m, pricing),
)
})
.collect();
candidates.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
for (model, cost) in &candidates {
if *cost <= budget_usd {
tracing::info!(
model,
cost,
budget_usd,
"selected cheapest model within budget"
);
return Some(model);
}
}
tracing::warn!(budget_usd, "no model fits within budget");
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_pricing_values() {
let p = GpuPricing::default();
assert!((p.a100 - 3.00).abs() < f64::EPSILON);
assert!((p.h100 - 4.50).abs() < f64::EPSILON);
assert!((p.rtx4090 - 1.20).abs() < f64::EPSILON);
assert!((p.t4 - 0.35).abs() < f64::EPSILON);
assert!((p.l4 - 0.80).abs() < f64::EPSILON);
}
#[test]
fn estimate_crew_cost_gpt4() {
let pricing = GpuPricing::default();
let cost = estimate_crew_cost(10, 1000, "gpt-4", &pricing);
let expected = (10_000.0 / 40.0 / 3600.0) * 3.00;
assert!(
(cost - expected).abs() < 1e-6,
"expected {expected}, got {cost}"
);
}
#[test]
fn estimate_crew_cost_small_model() {
let pricing = GpuPricing::default();
let cost = estimate_crew_cost(10, 1000, "llama-7b", &pricing);
let expected = (10_000.0 / 120.0 / 3600.0) * 0.35;
assert!(
(cost - expected).abs() < 1e-6,
"expected {expected}, got {cost}"
);
}
#[test]
fn estimate_zero_tasks() {
let pricing = GpuPricing::default();
let cost = estimate_crew_cost(0, 1000, "gpt-4", &pricing);
assert!((cost - 0.0).abs() < f64::EPSILON);
}
#[test]
fn estimate_zero_tokens() {
let pricing = GpuPricing::default();
let cost = estimate_crew_cost(10, 0, "gpt-4", &pricing);
assert!((cost - 0.0).abs() < f64::EPSILON);
}
#[test]
fn select_cheapest_model_within_budget() {
let pricing = GpuPricing::default();
let model = select_cheapest_model(0.01, 5, 500, &pricing);
assert!(model.is_some());
let name = model.unwrap();
assert!(
name == "llama-7b" || name == "mistral-7b",
"expected cheap model, got {name}"
);
}
#[test]
fn select_cheapest_model_large_budget() {
let pricing = GpuPricing::default();
let model = select_cheapest_model(100.0, 5, 500, &pricing);
assert!(model.is_some());
}
#[test]
fn select_cheapest_model_zero_budget() {
let pricing = GpuPricing::default();
let model = select_cheapest_model(0.0, 100, 10_000, &pricing);
assert!(
model.is_none(),
"expected None for zero budget with big workload"
);
}
#[test]
fn pricing_display() {
let p = GpuPricing::default();
let s = format!("{p}");
assert!(s.contains("A100=$3.00/h"));
assert!(s.contains("T4=$0.35/h"));
}
#[test]
fn unknown_model_uses_medium_tier() {
let pricing = GpuPricing::default();
let cost = estimate_crew_cost(10, 1000, "some-custom-model", &pricing);
let expected = (10_000.0 / 80.0 / 3600.0) * 0.80;
assert!(
(cost - expected).abs() < 1e-6,
"expected {expected}, got {cost}"
);
}
}