1use crate::pricing::calculator::ModelPrice;
2use serde::Deserialize;
3use std::collections::HashMap;
4use std::path::Path;
5
6#[derive(Debug, Deserialize, Default)]
7pub struct Config {
8 #[serde(default)]
9 pub subscription: Vec<SubscriptionPeriod>,
10 #[serde(default)]
11 pub pricing_override: HashMap<String, PriceOverride>,
12}
13
14#[derive(Debug, Deserialize)]
15pub struct SubscriptionPeriod {
16 pub start_date: String,
17 pub monthly_price_usd: f64,
18 pub plan: Option<String>,
19}
20
21#[derive(Debug, Deserialize)]
22pub struct PriceOverride {
23 pub base_input: f64,
24 pub cache_write_5m: f64,
25 pub cache_write_1h: f64,
26 pub cache_read: f64,
27 pub output: f64,
28}
29
30impl Config {
31 pub fn load(path: &Path) -> anyhow::Result<Self> {
32 let content = std::fs::read_to_string(path)?;
33 Ok(toml::from_str(&content)?)
34 }
35
36 pub fn to_model_prices(&self) -> HashMap<String, ModelPrice> {
37 self.pricing_override
38 .iter()
39 .map(|(k, v)| {
40 (
41 k.clone(),
42 ModelPrice {
43 base_input: v.base_input,
44 cache_write_5m: v.cache_write_5m,
45 cache_write_1h: v.cache_write_1h,
46 cache_read: v.cache_read,
47 output: v.output,
48 },
49 )
50 })
51 .collect()
52 }
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58
59 #[test]
60 fn parse_config() {
61 let toml_str = r#"
62[[subscription]]
63start_date = "2026-01-01"
64monthly_price_usd = 100.0
65plan = "max_5x"
66
67[[subscription]]
68start_date = "2026-03-01"
69monthly_price_usd = 200.0
70plan = "max_20x"
71
72[pricing_override.claude-opus-4-6]
73base_input = 5.0
74cache_write_5m = 6.25
75cache_write_1h = 10.0
76cache_read = 0.50
77output = 25.0
78"#;
79
80 let config: Config = toml::from_str(toml_str).unwrap();
81
82 assert_eq!(config.subscription.len(), 2);
83 assert_eq!(config.subscription[0].start_date, "2026-01-01");
84 assert!((config.subscription[0].monthly_price_usd - 100.0).abs() < f64::EPSILON);
85 assert_eq!(config.subscription[0].plan.as_deref(), Some("max_5x"));
86 assert_eq!(config.subscription[1].start_date, "2026-03-01");
87 assert!((config.subscription[1].monthly_price_usd - 200.0).abs() < f64::EPSILON);
88 assert_eq!(config.subscription[1].plan.as_deref(), Some("max_20x"));
89
90 assert_eq!(config.pricing_override.len(), 1);
91 let opus = &config.pricing_override["claude-opus-4-6"];
92 assert!((opus.base_input - 5.0).abs() < f64::EPSILON);
93 assert!((opus.cache_write_5m - 6.25).abs() < f64::EPSILON);
94 assert!((opus.cache_write_1h - 10.0).abs() < f64::EPSILON);
95 assert!((opus.cache_read - 0.50).abs() < f64::EPSILON);
96 assert!((opus.output - 25.0).abs() < f64::EPSILON);
97
98 let model_prices = config.to_model_prices();
99 assert_eq!(model_prices.len(), 1);
100 let mp = &model_prices["claude-opus-4-6"];
101 assert!((mp.base_input - 5.0).abs() < f64::EPSILON);
102 assert!((mp.output - 25.0).abs() < f64::EPSILON);
103 }
104
105 #[test]
106 fn empty_config() {
107 let config: Config = toml::from_str("").unwrap();
108 assert!(config.subscription.is_empty());
109 assert!(config.pricing_override.is_empty());
110 assert!(config.to_model_prices().is_empty());
111 }
112}