1use chrono::{Datelike, DateTime, Local, Utc};
2use crate::Error;
3use ragit_fs::{
4 WriteMode,
5 create_dir_all,
6 exists,
7 parent,
8 read_string,
9 write_string,
10};
11use ragit_pdl::{Message, JsonType};
12use serde_json::{Map, Value};
13use std::collections::HashMap;
14use std::ops::AddAssign;
15
16#[derive(Clone, Debug)]
17pub struct AuditRecordAt {
18 pub path: String,
19 pub id: String,
20}
21
22#[derive(Clone, Copy, Debug)]
23pub struct AuditRecord {
24 pub input_tokens: u64,
25 pub output_tokens: u64,
26
27 pub input_cost: u64,
29 pub output_cost: u64,
30}
31
32impl AddAssign<AuditRecord> for AuditRecord {
33 fn add_assign(&mut self, rhs: AuditRecord) {
34 self.input_tokens += rhs.input_tokens;
35 self.output_tokens += rhs.output_tokens;
36 self.input_cost += rhs.input_cost;
37 self.output_cost += rhs.output_cost;
38 }
39}
40
41impl From<&AuditRecord> for Value {
42 fn from(r: &AuditRecord) -> Value {
43 Value::Array(vec![
44 Value::from(r.input_tokens),
45 Value::from(r.output_tokens),
46 Value::from(r.input_cost),
47 Value::from(r.output_cost),
48 ])
49 }
50}
51
52impl TryFrom<&Value> for AuditRecord {
53 type Error = Error;
54
55 fn try_from(j: &Value) -> Result<AuditRecord, Error> {
56 let mut result = vec![];
57
58 match &j {
59 Value::Array(arr) => {
60 if arr.len() != 4 {
61 return Err(Error::WrongSchema(format!("expected an array of length 4, but got length {}", arr.len())));
62 }
63
64 for r in arr.iter() {
65 match r.as_u64() {
66 Some(n) => {
67 result.push(n);
68 },
69 None => {
70 return Err(Error::JsonTypeError {
71 expected: JsonType::U64,
72 got: r.into(),
73 });
74 },
75 }
76 }
77
78 Ok(AuditRecord {
79 input_tokens: result[0],
80 output_tokens: result[1],
81 input_cost: result[2],
82 output_cost: result[3],
83 })
84 },
85 _ => Err(Error::JsonTypeError {
86 expected: JsonType::Array,
87 got: j.into(),
88 }),
89 }
90 }
91}
92
93fn records_from_json(j: &Value) -> Result<HashMap<String, AuditRecord>, Error> {
94 match j {
95 Value::Object(obj) => {
96 let mut result = HashMap::with_capacity(obj.len());
97
98 for (key, value) in obj.iter() {
99 result.insert(key.to_string(), AuditRecord::try_from(value)?);
100 }
101
102 Ok(result)
103 },
104 Value::Array(arr) => {
105 let mut result: HashMap<String, AuditRecord> = HashMap::new();
106
107 for r in arr.iter() {
108 let AuditRecordLegacy {
109 time,
110 input,
111 output,
112 input_weight,
113 output_weight,
114 } = AuditRecordLegacy::try_from(r)?;
115 let date = match DateTime::<Utc>::from_timestamp(time as i64, 0) {
117 Some(date) => format!("{:04}{:02}{:02}", date.year(), date.month(), date.day()),
118 None => format!("19700101"),
119 };
120 let new_record = AuditRecord {
121 input_tokens: input,
122 output_tokens: output,
123 input_cost: input * input_weight / 1000,
124 output_cost: output * output_weight / 1000,
125 };
126
127 match result.get_mut(&date) {
128 Some(record) => { *record += new_record; },
129 None => { result.insert(date, new_record); },
130 }
131 }
132
133 Ok(result)
134 },
135 _ => Err(Error::JsonTypeError {
136 expected: JsonType::Object,
137 got: j.into(),
138 }),
139 }
140}
141
142#[derive(Clone)]
143pub struct Tracker(pub HashMap<String, HashMap<String, AuditRecord>>); impl Tracker {
146 pub fn new() -> Self {
147 Tracker(HashMap::new())
148 }
149
150 pub fn load_from_file(path: &str) -> Result<Self, Error> {
151 let content = read_string(path)?;
152 let j: Value = serde_json::from_str(&content)?;
153 Tracker::try_from(&j)
154 }
155
156 pub fn save_to_file(&self, path: &str) -> Result<(), Error> {
157 Ok(write_string(
158 path,
159 &serde_json::to_string_pretty(&Value::from(self))?,
160 WriteMode::Atomic,
161 )?)
162 }
163}
164
165impl TryFrom<&Value> for Tracker {
166 type Error = Error;
167
168 fn try_from(v: &Value) -> Result<Tracker, Error> {
169 match v {
170 Value::Object(obj) => {
171 let mut result = HashMap::new();
172
173 for (k, v) in obj.iter() {
174 result.insert(k.to_string(), records_from_json(v)?);
175 }
176
177 Ok(Tracker(result))
178 },
179 _ => Err(Error::JsonTypeError {
180 expected: JsonType::Object,
181 got: v.into(),
182 }),
183 }
184 }
185}
186
187impl From<&Tracker> for Value {
188 fn from(t: &Tracker) -> Value {
189 Value::Object(t.0.iter().map(
190 |(id, records)| (
191 id.to_string(),
192 Value::Object(
193 records.iter().map(
194 |(date, record)| (
195 date.to_string(),
196 Value::from(record),
197 )
198 ).collect::<Map<_, _>>()
199 ),
200 )
201 ).collect())
202 }
203}
204
205pub fn dump_api_usage(
206 at: &AuditRecordAt,
207 input_tokens: u64,
208 output_tokens: u64,
209
210 input_weight: u64,
212 output_weight: u64,
213
214 _clean_up_records: bool,
216) -> Result<(), Error> {
217 let mut tracker = Tracker::load_from_file(&at.path)?;
218 let today = Local::now();
219 let today = format!("{:04}{:02}{:02}", today.year(), today.month(), today.day());
220 let new_record = AuditRecord {
221 input_tokens,
222 output_tokens,
223 input_cost: input_tokens * input_weight / 1000,
224 output_cost: output_tokens * output_weight / 1000,
225 };
226
227 match tracker.0.get_mut(&at.id) {
228 Some(records) => match records.get_mut(&today) {
229 Some(record) => {
230 *record += new_record;
231 },
232 None => {
233 records.insert(today, new_record);
234 },
235 },
236 None => {
237 tracker.0.insert(at.id.clone(), [(today, new_record)].into_iter().collect());
238 },
239 }
240
241 tracker.save_to_file(&at.path)?;
242 Ok(())
243}
244
245pub fn get_user_usage_data_since(at: AuditRecordAt, since: DateTime<Local>) -> Option<HashMap<String, AuditRecord>> {
246 let since = format!("{:04}{:02}{:02}", since.year(), since.month(), since.day());
247
248 match Tracker::load_from_file(&at.path) {
249 Ok(tracker) => match tracker.0.get(&at.id) {
250 Some(records) => Some(records.iter().filter(
251 |(date, _)| date >= &&since
252 ).map(
253 |(date, record)| (date.to_string(), record.clone())
254 ).collect()),
255 None => None,
256 },
257 _ => None,
258 }
259}
260
261pub fn get_usage_data_since(path: &str, since: DateTime<Local>) -> Option<HashMap<String, AuditRecord>> {
262 let since = format!("{:04}{:02}{:02}", since.year(), since.month(), since.day());
263
264 match Tracker::load_from_file(path) {
265 Ok(tracker) => {
266 let mut result = HashMap::new();
267
268 for records in tracker.0.values() {
269 for (date, record) in records.iter() {
270 if date >= &since {
271 result.insert(date.to_string(), record.clone());
272 }
273 }
274 }
275
276 Some(result)
277 },
278 _ => None,
279 }
280}
281
282pub fn calc_usage(records: &HashMap<String, AuditRecord>) -> String {
284 let mut total: u64 = records.values().map(
286 |AuditRecord { input_cost, output_cost, .. }| *input_cost + *output_cost
287 ).sum();
288
289 total /= 1000;
291
292 format!("{:.3}", total as f64 / 1_000.0)
293}
294
295pub fn dump_pdl(
296 messages: &[Message],
297 response: &str,
298 reasoning: &Option<String>,
299 path: &str,
300 metadata: String,
301) -> Result<(), Error> {
302 let mut markdown = vec![];
303
304 for message in messages.iter() {
305 markdown.push(format!(
306 "\n\n<|{:?}|>\n\n{}",
307 message.role,
308 message.content.iter().map(|c| c.to_string()).collect::<Vec<String>>().join(""),
309 ));
310 }
311
312 markdown.push(format!(
313 "\n\n<|Assistant|>{}\n\n{response}",
314 if let Some(reasoning) = reasoning {
315 format!("\n\n<|Reasoning|>\n\n{reasoning}\n\n")
316 } else {
317 String::new()
318 },
319 ));
320 markdown.push(format!("{}# {metadata} #{}", '{', '}')); if let Ok(parent) = parent(path) {
323 if !exists(&parent) {
324 create_dir_all(&parent)?;
325 }
326 }
327
328 write_string(
329 path,
330 &markdown.join("\n"),
331 WriteMode::CreateOrTruncate,
332 )?;
333
334 Ok(())
335}
336
337impl From<AuditRecordLegacy> for Value {
343 fn from(r: AuditRecordLegacy) -> Value {
344 Value::Array(vec![
345 Value::from(r.time),
346 Value::from(r.input),
347 Value::from(r.output),
348 Value::from(r.input_weight),
349 Value::from(r.output_weight),
350 ])
351 }
352}
353
354#[derive(Clone, Copy, Debug)]
355pub struct AuditRecordLegacy {
356 pub time: u64,
357 pub input: u64,
358 pub output: u64,
359
360 pub input_weight: u64,
362 pub output_weight: u64,
363}
364
365impl TryFrom<&Value> for AuditRecordLegacy {
366 type Error = Error;
367
368 fn try_from(j: &Value) -> Result<AuditRecordLegacy, Error> {
369 let mut result = vec![];
370
371 match &j {
372 Value::Array(arr) => {
373 if arr.len() != 5 {
374 return Err(Error::WrongSchema(format!("expected an array of length 5, but got length {}", arr.len())));
375 }
376
377 for r in arr.iter() {
378 match r.as_u64() {
379 Some(n) => {
380 result.push(n);
381 },
382 None => {
383 return Err(Error::JsonTypeError {
384 expected: JsonType::U64,
385 got: r.into(),
386 });
387 },
388 }
389 }
390
391 Ok(AuditRecordLegacy {
392 time: result[0],
393 input: result[1],
394 output: result[2],
395 input_weight: result[3],
396 output_weight: result[4],
397 })
398 },
399 _ => Err(Error::JsonTypeError {
400 expected: JsonType::Array,
401 got: j.into(),
402 }),
403 }
404 }
405}