otelite_api/
pricing_cache.rs1use otelite_core::pricing::{PricingDatabase, LITELLM_RAW_URL};
14use std::sync::Arc;
15use std::time::Duration;
16use tokio::sync::RwLock;
17use tracing::{debug, info, warn};
18
19pub const REFRESH_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60);
22
23pub const INITIAL_FETCH_TIMEOUT: Duration = Duration::from_secs(10);
26
27pub const REFRESH_FETCH_TIMEOUT: Duration = Duration::from_secs(30);
29
30#[derive(Clone)]
32pub struct PricingCache {
33 inner: Arc<RwLock<PricingState>>,
34}
35
36#[derive(Clone)]
37pub struct PricingState {
38 pub db: PricingDatabase,
39 pub last_fetched_unix_ms: Option<i64>,
41 pub last_failed_unix_ms: Option<i64>,
43}
44
45impl PricingCache {
46 pub fn new() -> Self {
47 Self {
48 inner: Arc::new(RwLock::new(PricingState {
49 db: PricingDatabase::empty(),
50 last_fetched_unix_ms: None,
51 last_failed_unix_ms: None,
52 })),
53 }
54 }
55
56 pub async fn snapshot(&self) -> PricingState {
57 self.inner.read().await.clone()
58 }
59
60 async fn refresh(&self, timeout: Duration) {
63 let client = match reqwest::Client::builder().timeout(timeout).build() {
64 Ok(c) => c,
65 Err(e) => {
66 warn!("pricing: failed to build HTTP client: {e}");
67 return;
68 },
69 };
70 match client.get(LITELLM_RAW_URL).send().await {
71 Ok(resp) if resp.status().is_success() => {
72 let body = match resp.text().await {
73 Ok(b) => b,
74 Err(e) => {
75 warn!("pricing: failed to read LiteLLM response: {e}");
76 self.record_failure().await;
77 return;
78 },
79 };
80 match PricingDatabase::from_litellm_json(&body) {
81 Ok(db) => {
82 let count = db.len();
83 let mut state = self.inner.write().await;
84 state.db = db;
85 state.last_fetched_unix_ms = Some(now_unix_ms());
86 info!("pricing: loaded {count} LiteLLM entries");
87 },
88 Err(e) => {
89 warn!("pricing: failed to parse LiteLLM JSON: {e}");
90 self.record_failure().await;
91 },
92 }
93 },
94 Ok(resp) => {
95 warn!("pricing: LiteLLM fetch returned HTTP {}", resp.status());
96 self.record_failure().await;
97 },
98 Err(e) => {
99 debug!("pricing: LiteLLM fetch failed: {e}");
100 self.record_failure().await;
101 },
102 }
103 }
104
105 async fn record_failure(&self) {
106 let mut state = self.inner.write().await;
107 state.last_failed_unix_ms = Some(now_unix_ms());
108 }
109
110 pub fn spawn_refresher(self) -> Self {
113 let task_cache = self.clone();
114 tokio::spawn(async move {
115 task_cache.refresh(INITIAL_FETCH_TIMEOUT).await;
116 let mut ticker = tokio::time::interval(REFRESH_INTERVAL);
117 ticker.tick().await;
120 loop {
121 ticker.tick().await;
122 task_cache.refresh(REFRESH_FETCH_TIMEOUT).await;
123 }
124 });
125 self
126 }
127}
128
129impl Default for PricingCache {
130 fn default() -> Self {
131 Self::new()
132 }
133}
134
135fn now_unix_ms() -> i64 {
136 use std::time::{SystemTime, UNIX_EPOCH};
137 SystemTime::now()
138 .duration_since(UNIX_EPOCH)
139 .map(|d| d.as_millis() as i64)
140 .unwrap_or(0)
141}