obol_core/transcript/provider/
openai.rs1use super::ProviderTokens;
6use serde_json::Value;
7
8pub fn normalize(usage: &Value) -> ProviderTokens {
12 let g = |k: &str| usage.get(k).and_then(Value::as_u64).unwrap_or(0);
13 let nested = |obj: &str, k: &str| {
14 usage
15 .get(obj)
16 .and_then(|d| d.get(k))
17 .and_then(Value::as_u64)
18 .unwrap_or(0)
19 };
20 let cached = nested("input_tokens_details", "cached_tokens");
21 let reasoning = nested("output_tokens_details", "reasoning_tokens");
22 ProviderTokens {
23 input_uncached: g("input_tokens").saturating_sub(cached),
24 cache_read: cached,
25 cache_write_5m: 0,
26 cache_write_1h: 0,
27 output: g("output_tokens") + reasoning,
28 }
29}
30
31#[cfg(test)]
32mod tests {
33 use super::*;
34 use serde_json::json;
35
36 #[test]
37 fn subtracts_cached_from_input_and_folds_reasoning_into_output() {
38 let usage = json!({
39 "input_tokens": 100,
40 "input_tokens_details": {"cached_tokens": 40},
41 "output_tokens": 20,
42 "output_tokens_details": {"reasoning_tokens": 5}
43 });
44 let t = normalize(&usage);
45 assert_eq!(t.input_uncached, 60);
46 assert_eq!(t.cache_read, 40);
47 assert_eq!(t.cache_write_5m, 0);
48 assert_eq!(t.cache_write_1h, 0);
49 assert_eq!(t.output, 25);
50 }
51
52 #[test]
53 fn handles_missing_details() {
54 let usage = json!({"input_tokens": 70, "output_tokens": 6});
55 let t = normalize(&usage);
56 assert_eq!(t.input_uncached, 70);
57 assert_eq!(t.cache_read, 0);
58 assert_eq!(t.output, 6);
59 }
60}