ccboard_core/pricing/
cache.rs1use super::litellm::{CachedPricing, LITELLM_PRICING_URL};
6use crate::pricing::ModelPricing;
7use anyhow::{Context, Result};
8use std::collections::HashMap;
9use std::path::PathBuf;
10
11const CACHE_TTL_DAYS: i64 = 7;
13
14pub fn cache_path() -> Result<PathBuf> {
16 let cache_dir = dirs::cache_dir()
17 .context("Could not determine cache directory")?
18 .join("ccboard");
19
20 std::fs::create_dir_all(&cache_dir)
21 .with_context(|| format!("Failed to create cache directory: {}", cache_dir.display()))?;
22
23 Ok(cache_dir.join("pricing.json"))
24}
25
26pub fn load_cached_pricing() -> Result<Option<HashMap<String, ModelPricing>>> {
28 let path = cache_path()?;
29
30 if !path.exists() {
31 tracing::debug!("No pricing cache found at {}", path.display());
32 return Ok(None);
33 }
34
35 let content = std::fs::read_to_string(&path)
36 .with_context(|| format!("Failed to read pricing cache: {}", path.display()))?;
37
38 let cached: CachedPricing = serde_json::from_str(&content)
39 .with_context(|| format!("Failed to parse pricing cache: {}", path.display()))?;
40
41 let now = chrono::Utc::now();
43 let age = now.signed_duration_since(cached.last_updated);
44
45 if age.num_days() > CACHE_TTL_DAYS {
46 tracing::info!(
47 "Pricing cache expired ({} days old, TTL: {} days)",
48 age.num_days(),
49 CACHE_TTL_DAYS
50 );
51 return Ok(None);
52 }
53
54 tracing::info!(
55 "Loaded {} model prices from cache ({} days old)",
56 cached.models.len(),
57 age.num_days()
58 );
59
60 Ok(Some(cached.models))
61}
62
63pub fn save_pricing_cache(models: HashMap<String, ModelPricing>) -> Result<()> {
65 let path = cache_path()?;
66
67 let cached = CachedPricing {
68 last_updated: chrono::Utc::now(),
69 models,
70 source: LITELLM_PRICING_URL.to_string(),
71 };
72
73 let json =
74 serde_json::to_string_pretty(&cached).context("Failed to serialize pricing cache")?;
75
76 std::fs::write(&path, json)
77 .with_context(|| format!("Failed to write pricing cache: {}", path.display()))?;
78
79 tracing::info!("Saved {} model prices to cache", cached.models.len());
80
81 Ok(())
82}
83
84pub fn clear_pricing_cache() -> Result<()> {
86 let path = cache_path()?;
87
88 if path.exists() {
89 std::fs::remove_file(&path)
90 .with_context(|| format!("Failed to remove pricing cache: {}", path.display()))?;
91 tracing::info!("Cleared pricing cache");
92 }
93
94 Ok(())
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn test_cache_path() {
103 let path = cache_path().unwrap();
104 assert!(path.to_string_lossy().contains("ccboard"));
105 assert!(path.to_string_lossy().ends_with("pricing.json"));
106 }
107}