Skip to main content

entrenar/ecosystem/batuta/
pricing.rs

1//! GPU pricing types and fallback pricing.
2
3use serde::{Deserialize, Serialize};
4
5/// GPU pricing information.
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
7pub struct GpuPricing {
8    /// GPU type identifier (e.g., "a100-80gb", "v100", "t4")
9    pub gpu_type: String,
10    /// Hourly rate in USD
11    pub hourly_rate: f64,
12    /// GPU memory in GB
13    pub memory_gb: u32,
14    /// Whether this is spot/preemptible pricing
15    pub is_spot: bool,
16    /// Provider name (e.g., "aws", "gcp", "azure")
17    pub provider: String,
18    /// Region identifier
19    pub region: String,
20}
21
22impl GpuPricing {
23    /// Create a new GPU pricing entry.
24    pub fn new(gpu_type: impl Into<String>, hourly_rate: f64, memory_gb: u32) -> Self {
25        Self {
26            gpu_type: gpu_type.into(),
27            hourly_rate,
28            memory_gb,
29            is_spot: false,
30            provider: "unknown".to_string(),
31            region: "unknown".to_string(),
32        }
33    }
34
35    /// Set spot pricing flag.
36    pub fn with_spot(mut self, is_spot: bool) -> Self {
37        self.is_spot = is_spot;
38        self
39    }
40
41    /// Set provider.
42    pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
43        self.provider = provider.into();
44        self
45    }
46
47    /// Set region.
48    pub fn with_region(mut self, region: impl Into<String>) -> Self {
49        self.region = region.into();
50        self
51    }
52}
53
54/// Fallback pricing when Batuta is unavailable.
55///
56/// Uses conservative estimates based on typical cloud provider pricing.
57#[derive(Debug, Clone)]
58pub struct FallbackPricing {
59    /// Default pricing for known GPU types
60    pricing: Vec<GpuPricing>,
61}
62
63impl Default for FallbackPricing {
64    fn default() -> Self {
65        Self::new()
66    }
67}
68
69impl FallbackPricing {
70    /// Create fallback pricing with typical cloud rates.
71    pub fn new() -> Self {
72        Self {
73            pricing: vec![
74                GpuPricing::new("a100-80gb", 3.00, 80)
75                    .with_provider("generic")
76                    .with_region("us-east-1"),
77                GpuPricing::new("a100-40gb", 2.50, 40)
78                    .with_provider("generic")
79                    .with_region("us-east-1"),
80                GpuPricing::new("v100", 2.00, 16).with_provider("generic").with_region("us-east-1"),
81                GpuPricing::new("t4", 0.50, 16).with_provider("generic").with_region("us-east-1"),
82                GpuPricing::new("l4", 0.75, 24).with_provider("generic").with_region("us-east-1"),
83                GpuPricing::new("a10g", 1.00, 24).with_provider("generic").with_region("us-east-1"),
84                GpuPricing::new("h100-80gb", 4.50, 80)
85                    .with_provider("generic")
86                    .with_region("us-east-1"),
87            ],
88        }
89    }
90
91    /// Get pricing for a GPU type.
92    pub fn get_rate(&self, gpu_type: &str) -> Option<&GpuPricing> {
93        let normalized = gpu_type.to_lowercase().replace(['-', '_'], "");
94        self.pricing.iter().find(|p| {
95            let p_normalized = p.gpu_type.to_lowercase().replace(['-', '_'], "");
96            p_normalized == normalized
97        })
98    }
99
100    /// Get all available pricing.
101    pub fn all_pricing(&self) -> &[GpuPricing] {
102        &self.pricing
103    }
104
105    /// Add or update pricing for a GPU type.
106    pub fn set_rate(&mut self, pricing: GpuPricing) {
107        let normalized = pricing.gpu_type.to_lowercase().replace(['-', '_'], "");
108        if let Some(existing) = self.pricing.iter_mut().find(|p| {
109            let p_normalized = p.gpu_type.to_lowercase().replace(['-', '_'], "");
110            p_normalized == normalized
111        }) {
112            *existing = pricing;
113        } else {
114            self.pricing.push(pricing);
115        }
116    }
117}