1#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
3pub struct TokenUsage {
4 pub input_tokens: u64,
5 pub output_tokens: u64,
6}
7
8impl TokenUsage {
9 pub fn total_tokens(self) -> u64 {
10 self.input_tokens.saturating_add(self.output_tokens)
11 }
12}
13
14#[derive(Debug, Clone, Copy, PartialEq)]
16pub struct ModelPricing {
17 pub input_per_million_usd: f64,
18 pub output_per_million_usd: f64,
19}
20
21impl ModelPricing {
22 #[expect(
23 clippy::cast_precision_loss,
24 reason = "token counts stay far below 2^52"
25 )]
26 pub fn estimate_cost_usd(self, usage: TokenUsage) -> CostEstimate {
27 let input_cost = (usage.input_tokens as f64 / 1_000_000.0) * self.input_per_million_usd;
28 let output_cost = (usage.output_tokens as f64 / 1_000_000.0) * self.output_per_million_usd;
29 CostEstimate {
30 input_cost_usd: input_cost,
31 output_cost_usd: output_cost,
32 total_cost_usd: input_cost + output_cost,
33 }
34 }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Default)]
39pub struct CostEstimate {
40 pub input_cost_usd: f64,
41 pub output_cost_usd: f64,
42 pub total_cost_usd: f64,
43}
44const FREE: ModelPricing = ModelPricing {
46 input_per_million_usd: 0.0,
47 output_per_million_usd: 0.0,
48};
49
50pub fn pricing_for_model(model: &str) -> ModelPricing {
52 let normalized = model.trim().to_lowercase();
53
54 if normalized.starts_with("github-copilot/") {
56 return FREE;
57 }
58
59 if normalized.contains("-free") || normalized.ends_with("free") {
61 return FREE;
62 }
63
64 if normalized.starts_with("deepseek") {
66 return ModelPricing {
67 input_per_million_usd: 0.55,
68 output_per_million_usd: 2.19,
69 };
70 }
71
72 if normalized == "o3" {
74 return ModelPricing {
75 input_per_million_usd: 2.0,
76 output_per_million_usd: 8.0,
77 };
78 }
79 if normalized == "o4-mini" || normalized.contains("gpt-4.1-mini") {
80 return ModelPricing {
81 input_per_million_usd: 0.15,
82 output_per_million_usd: 0.6,
83 };
84 }
85 if normalized.contains("gpt-4.1") && !normalized.contains("mini") {
86 return ModelPricing {
87 input_per_million_usd: 2.0,
88 output_per_million_usd: 8.0,
89 };
90 }
91 if normalized.starts_with("gpt-5") && normalized.contains("mini") {
93 return ModelPricing {
94 input_per_million_usd: 0.15,
95 output_per_million_usd: 0.6,
96 };
97 }
98 if normalized.starts_with("gpt-5") {
100 return ModelPricing {
101 input_per_million_usd: 2.0,
102 output_per_million_usd: 8.0,
103 };
104 }
105
106 if normalized.contains("opus") {
108 return ModelPricing {
109 input_per_million_usd: 15.0,
110 output_per_million_usd: 75.0,
111 };
112 }
113 if normalized.contains("sonnet") {
114 return ModelPricing {
115 input_per_million_usd: 3.0,
116 output_per_million_usd: 15.0,
117 };
118 }
119 if normalized.contains("haiku") {
120 return ModelPricing {
121 input_per_million_usd: 0.8,
122 output_per_million_usd: 4.0,
123 };
124 }
125
126 ModelPricing {
128 input_per_million_usd: 1.0,
129 output_per_million_usd: 4.0,
130 }
131}