1use serde_json::{json, Value};
13
14#[derive(Debug, Default, Clone, Copy)]
15pub struct Usage {
16 pub prompt: u64,
17 pub completion: u64,
18 pub cache_read: u64,
19 pub cache_creation: u64,
20}
21
22impl Usage {
23 pub fn add(&mut self, other: &Usage) {
24 self.prompt += other.prompt;
25 self.completion += other.completion;
26 self.cache_read += other.cache_read;
27 self.cache_creation += other.cache_creation;
28 }
29
30 pub fn from_openai(usage: &Value) -> Usage {
33 let g = |k: &str| usage.get(k).and_then(|v| v.as_u64()).unwrap_or(0);
34 let cache_read = usage
35 .get("prompt_tokens_details")
36 .and_then(|d| d.get("cached_tokens"))
37 .and_then(|v| v.as_u64())
38 .unwrap_or(0);
39 Usage {
40 prompt: g("prompt_tokens"),
41 completion: g("completion_tokens"),
42 cache_read,
43 cache_creation: 0,
44 }
45 }
46
47 pub fn from_anthropic(usage: &Value) -> Usage {
50 let g = |k: &str| usage.get(k).and_then(|v| v.as_u64()).unwrap_or(0);
51 Usage {
52 prompt: g("input_tokens"),
53 completion: g("output_tokens"),
54 cache_read: g("cache_read_input_tokens"),
55 cache_creation: g("cache_creation_input_tokens"),
56 }
57 }
58
59 pub fn from_cursor(usage: &Value) -> Usage {
62 let g = |k: &str| usage.get(k).and_then(|v| v.as_u64()).unwrap_or(0);
63 Usage {
64 prompt: g("inputTokens"),
65 completion: g("outputTokens"),
66 cache_read: g("cacheReadTokens"),
67 cache_creation: g("cacheWriteTokens"),
68 }
69 }
70
71 pub fn from_any(usage: &Value) -> Usage {
74 if usage.get("prompt_tokens").is_some() || usage.get("completion_tokens").is_some() {
75 Usage::from_openai(usage)
76 } else {
77 Usage::from_anthropic(usage)
78 }
79 }
80
81 pub fn to_json(self) -> Value {
82 json!({
83 "prompt": self.prompt,
84 "completion": self.completion,
85 "cache_read": self.cache_read,
86 "cache_creation": self.cache_creation,
87 })
88 }
89
90 pub fn footer_line(&self) -> String {
93 let mut s = format!("prompt:{}", fmt_tokens(self.prompt));
94 if self.cache_read > 0 || self.cache_creation > 0 {
95 s.push_str(&format!(
96 " cache_r:{} cache_w:{}",
97 fmt_tokens(self.cache_read),
98 fmt_tokens(self.cache_creation),
99 ));
100 }
101 s.push_str(&format!(" tokens:{}", fmt_tokens(self.completion)));
102 s
103 }
104}
105
106fn fmt_tokens(n: u64) -> String {
109 const K: f64 = 1000.0;
110 if n < 1000 {
111 return n.to_string();
112 }
113 if n < 1_000_000 {
114 return fmt_scaled(n as f64 / K, "k");
115 }
116 if n < 1_000_000_000 {
117 return fmt_scaled(n as f64 / (K * K), "M");
118 }
119 fmt_scaled(n as f64 / (K * K * K), "G")
120}
121
122fn fmt_scaled(value: f64, suffix: &str) -> String {
123 let t = (value * 10.0).round() / 10.0;
124 if (t - t.floor()).abs() < 0.001 {
125 format!("{}{}", t as u64, suffix)
126 } else {
127 format!("{t:.1}{suffix}")
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use serde_json::json;
135
136 #[test]
137 fn footer_shape_matches_boro() {
138 let u = Usage {
139 prompt: 1234,
140 completion: 56,
141 ..Default::default()
142 };
143 assert_eq!(u.footer_line(), "prompt:1.2k tokens:56");
144 let u = Usage {
145 prompt: 1234,
146 completion: 56,
147 cache_read: 90,
148 cache_creation: 78,
149 };
150 assert_eq!(
151 u.footer_line(),
152 "prompt:1.2k cache_r:90 cache_w:78 tokens:56"
153 );
154 }
155
156 #[test]
157 fn fmt_tokens_scales() {
158 assert_eq!(fmt_tokens(515), "515");
159 assert_eq!(fmt_tokens(999), "999");
160 assert_eq!(fmt_tokens(24_300), "24.3k");
161 assert_eq!(fmt_tokens(2000), "2k"); assert_eq!(fmt_tokens(1_500_000), "1.5M");
163 }
164
165 #[test]
166 fn add_sums_fields() {
167 let mut a = Usage {
168 prompt: 10,
169 completion: 1,
170 cache_read: 2,
171 cache_creation: 3,
172 };
173 a.add(&Usage {
174 prompt: 5,
175 completion: 4,
176 cache_read: 6,
177 cache_creation: 7,
178 });
179 assert_eq!(
180 (a.prompt, a.completion, a.cache_read, a.cache_creation),
181 (15, 5, 8, 10)
182 );
183 }
184
185 #[test]
186 fn parse_openai_and_anthropic() {
187 let o = Usage::from_openai(&json!({
188 "prompt_tokens": 100, "completion_tokens": 20,
189 "prompt_tokens_details": {"cached_tokens": 80}
190 }));
191 assert_eq!((o.prompt, o.completion, o.cache_read), (100, 20, 80));
192 let a = Usage::from_anthropic(&json!({
193 "input_tokens": 7, "output_tokens": 9,
194 "cache_read_input_tokens": 11, "cache_creation_input_tokens": 13
195 }));
196 assert_eq!(
197 (a.prompt, a.completion, a.cache_read, a.cache_creation),
198 (7, 9, 11, 13)
199 );
200 }
201
202 #[test]
203 fn parse_cursor() {
204 let c = Usage::from_cursor(&json!({
205 "inputTokens": 12660, "outputTokens": 93,
206 "cacheReadTokens": 23872, "cacheWriteTokens": 5
207 }));
208 assert_eq!(
209 (c.prompt, c.completion, c.cache_read, c.cache_creation),
210 (12660, 93, 23872, 5)
211 );
212 }
213}