1use gpui::prelude::*;
9use gpui::{div, px, App, IntoElement, SharedString, Window};
10
11use super::tokenmeter::compact;
12use crate::devtools::Probed;
13use crate::icon::{Icon, IconName};
14use crate::theme::{theme, ColorName, Size};
15
16#[derive(Debug, Clone, Copy, Default, PartialEq)]
18pub struct AIPricing {
19 pub input_per_million: f64,
20 pub output_per_million: f64,
21 pub cache_read_per_million: f64,
23}
24
25impl AIPricing {
26 pub fn new(input_per_million: f64, output_per_million: f64) -> Self {
27 AIPricing {
28 input_per_million,
29 output_per_million,
30 cache_read_per_million: 0.0,
31 }
32 }
33
34 pub fn cache_read(mut self, per_million: f64) -> Self {
35 self.cache_read_per_million = per_million;
36 self
37 }
38}
39
40#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
42pub struct AIUsage {
43 pub input: u64,
44 pub output: u64,
45 pub cache_read: u64,
46}
47
48impl AIUsage {
49 pub fn new(input: u64, output: u64) -> Self {
50 AIUsage {
51 input,
52 output,
53 cache_read: 0,
54 }
55 }
56
57 pub fn cache_read(mut self, tokens: u64) -> Self {
58 self.cache_read = tokens;
59 self
60 }
61
62 pub fn total(self) -> u64 {
64 self
65 .input
66 .saturating_add(self.output)
67 .saturating_add(self.cache_read)
68 }
69
70 pub fn cost(self, pricing: AIPricing) -> f64 {
72 let per = |tokens: u64, price: f64| tokens as f64 / 1_000_000.0 * price;
73 per(self.input, pricing.input_per_million)
74 + per(self.output, pricing.output_per_million)
75 + per(self.cache_read, pricing.cache_read_per_million)
76 }
77}
78
79impl std::ops::Add for AIUsage {
83 type Output = AIUsage;
84
85 fn add(self, other: AIUsage) -> AIUsage {
86 AIUsage {
87 input: self.input.saturating_add(other.input),
88 output: self.output.saturating_add(other.output),
89 cache_read: self.cache_read.saturating_add(other.cache_read),
90 }
91 }
92}
93
94impl std::iter::Sum for AIUsage {
95 fn sum<I: Iterator<Item = AIUsage>>(iter: I) -> AIUsage {
96 iter.fold(AIUsage::default(), |total, usage| total + usage)
97 }
98}
99
100#[derive(IntoElement)]
102pub struct AICost {
103 usage: AIUsage,
104 pricing: AIPricing,
105 label: Option<SharedString>,
106 size: Size,
107 breakdown: bool,
109}
110
111impl AICost {
112 pub fn new(usage: AIUsage, pricing: AIPricing) -> Self {
113 AICost {
114 usage,
115 pricing,
116 label: None,
117 size: Size::Xs,
118 breakdown: false,
119 }
120 }
121
122 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
123 self.label = Some(label.into());
124 self
125 }
126
127 pub fn size(mut self, size: Size) -> Self {
128 self.size = size;
129 self
130 }
131
132 pub fn breakdown(mut self, breakdown: bool) -> Self {
134 self.breakdown = breakdown;
135 self
136 }
137
138 pub fn total(&self) -> f64 {
140 self.usage.cost(self.pricing)
141 }
142}
143
144pub fn format_cost(dollars: f64) -> String {
147 if !dollars.is_finite() || dollars <= 0.0 {
148 return "$0.00".to_string();
149 }
150 if dollars < 0.01 {
151 format!("${dollars:.4}")
152 } else if dollars < 1.0 {
153 format!("${dollars:.3}")
154 } else {
155 format!("${dollars:.2}")
156 }
157}
158
159impl RenderOnce for AICost {
160 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
161 let t = theme(cx);
162 let font = t.font_size(self.size);
163 let dimmed = t.dimmed().hsla();
164 let text_color = t.text().hsla();
165 let total = self.total();
166
167 div()
168 .flex()
169 .flex_col()
170 .gap(px(2.0))
171 .text_size(px(font))
172 .child(
173 div()
174 .flex()
175 .items_center()
176 .gap(px(6.0))
177 .text_color(dimmed)
178 .child(
179 Icon::new(IconName::Coins)
180 .size(Size::Xs)
181 .color(ColorName::Gray),
182 )
183 .children(self.label)
184 .child(
185 div()
186 .text_color(text_color)
187 .child(SharedString::from(format_cost(total))),
188 ),
189 )
190 .when(self.breakdown, |column| {
191 column.child(
192 div()
193 .flex()
194 .gap(px(10.0))
195 .text_color(dimmed)
196 .child(SharedString::from(format!(
197 "in {}",
198 compact(self.usage.input)
199 )))
200 .child(SharedString::from(format!(
201 "out {}",
202 compact(self.usage.output)
203 )))
204 .when(self.usage.cache_read > 0, |row| {
205 row.child(SharedString::from(format!(
206 "cache {}",
207 compact(self.usage.cache_read)
208 )))
209 }),
210 )
211 })
212 .probe("AICost")
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn cost_is_per_million_not_per_token() {
222 let usage = AIUsage::new(1_000_000, 0);
224 assert_eq!(usage.cost(AIPricing::new(3.0, 15.0)), 3.0);
225 let usage = AIUsage::new(0, 1_000_000);
226 assert_eq!(usage.cost(AIPricing::new(3.0, 15.0)), 15.0);
227 }
228
229 #[test]
230 fn cache_reads_bill_at_their_own_rate() {
231 let pricing = AIPricing::new(3.0, 15.0).cache_read(0.3);
232 let usage = AIUsage::new(0, 0).cache_read(2_000_000);
233 assert!((usage.cost(pricing) - 0.6).abs() < 1e-9);
234 }
235
236 #[test]
237 fn usage_adds_without_overflowing() {
238 let huge = AIUsage {
239 input: u64::MAX,
240 output: u64::MAX,
241 cache_read: u64::MAX,
242 };
243 assert_eq!((huge + huge).input, u64::MAX);
244 assert_eq!(huge.total(), u64::MAX);
245 let total: AIUsage = [AIUsage::new(1, 2), AIUsage::new(3, 4)].into_iter().sum();
247 assert_eq!(total, AIUsage::new(4, 6));
248 }
249
250 #[test]
251 fn tiny_amounts_stay_legible_and_bad_input_reads_as_zero() {
252 assert_eq!(format_cost(0.0), "$0.00");
253 assert_eq!(format_cost(-1.0), "$0.00");
254 assert_eq!(format_cost(f64::NAN), "$0.00");
255 assert_eq!(format_cost(0.00042), "$0.0004");
256 assert_eq!(format_cost(0.125), "$0.125");
257 assert_eq!(format_cost(12.3456), "$12.35");
258 }
259}