Skip to main content

obol_core/transcript/provider/
openai.rs

1//! OpenAI Responses-API `usage` object -> `ProviderTokens`. `input_tokens` is a
2//! *total* that still includes cached tokens, so the uncached bucket is the
3//! difference — the cached-subtraction a producer must never do itself.
4
5use super::ProviderTokens;
6use serde_json::Value;
7
8/// Normalize an OpenAI Responses `usage` object. `input_uncached =
9/// input_tokens - input_tokens_details.cached_tokens` (clamped ≥ 0);
10/// `cache_read = cached_tokens`; reasoning tokens fold into `output`.
11pub 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}