1use serde::{Deserialize, Serialize};
7
8use crate::ApiMode;
9
10#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
11pub struct Usage {
12 pub input_tokens: u64,
13 pub output_tokens: u64,
14 pub cache_read_tokens: u64,
15 pub cache_write_tokens: u64,
16 pub reasoning_tokens: u64,
17 pub total_tokens: u64,
18}
19
20#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
21pub struct Cost {
22 pub input_cost: f64,
23 pub output_cost: f64,
24 pub cache_read_cost: f64,
25 pub cache_write_cost: f64,
26 pub total_cost: f64,
27}
28
29impl Usage {
30 pub fn compute_total(&mut self) {
32 self.total_tokens = self.input_tokens + self.output_tokens;
33 }
34}
35
36pub fn normalize_usage(raw: &serde_json::Value, api_mode: ApiMode) -> Usage {
41 fn tok(v: &serde_json::Value) -> u64 {
43 v.as_u64().unwrap_or(0)
44 }
45
46 match api_mode {
47 ApiMode::ChatCompletions => Usage {
48 input_tokens: tok(&raw["prompt_tokens"]),
49 output_tokens: tok(&raw["completion_tokens"]),
50 cache_read_tokens: tok(&raw["prompt_tokens_details"]["cached_tokens"]),
51 total_tokens: tok(&raw["total_tokens"]),
52 ..Default::default()
53 },
54 ApiMode::AnthropicMessages => {
55 let input = tok(&raw["input_tokens"]);
56 let output = tok(&raw["output_tokens"]);
57 Usage {
58 input_tokens: input,
59 output_tokens: output,
60 cache_read_tokens: tok(&raw["cache_read_input_tokens"]),
61 cache_write_tokens: tok(&raw["cache_creation_input_tokens"]),
62 total_tokens: input + output,
63 ..Default::default()
64 }
65 }
66 ApiMode::CodexResponses => {
67 let input = tok(&raw["input_tokens"]);
68 let output = tok(&raw["output_tokens"]);
69 Usage {
70 input_tokens: input,
71 output_tokens: output,
72 total_tokens: input + output,
73 ..Default::default()
74 }
75 }
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn normalize_chat_completions() {
85 let raw = serde_json::json!({
86 "prompt_tokens": 100,
87 "completion_tokens": 50,
88 "total_tokens": 150,
89 "prompt_tokens_details": { "cached_tokens": 20 }
90 });
91 let usage = normalize_usage(&raw, ApiMode::ChatCompletions);
92 assert_eq!(usage.input_tokens, 100);
93 assert_eq!(usage.output_tokens, 50);
94 assert_eq!(usage.cache_read_tokens, 20);
95 assert_eq!(usage.total_tokens, 150);
96 }
97
98 #[test]
99 fn normalize_anthropic() {
100 let raw = serde_json::json!({
101 "input_tokens": 200,
102 "output_tokens": 80,
103 "cache_read_input_tokens": 50,
104 "cache_creation_input_tokens": 10
105 });
106 let usage = normalize_usage(&raw, ApiMode::AnthropicMessages);
107 assert_eq!(usage.input_tokens, 200);
108 assert_eq!(usage.output_tokens, 80);
109 assert_eq!(usage.cache_read_tokens, 50);
110 assert_eq!(usage.cache_write_tokens, 10);
111 assert_eq!(usage.total_tokens, 280);
112 }
113
114 #[test]
115 fn normalize_codex() {
116 let raw = serde_json::json!({
117 "input_tokens": 300,
118 "output_tokens": 100
119 });
120 let usage = normalize_usage(&raw, ApiMode::CodexResponses);
121 assert_eq!(usage.input_tokens, 300);
122 assert_eq!(usage.output_tokens, 100);
123 assert_eq!(usage.total_tokens, 400);
124 }
125
126 #[test]
127 fn usage_roundtrip() {
128 let usage = Usage {
129 input_tokens: 100,
130 output_tokens: 50,
131 cache_read_tokens: 20,
132 cache_write_tokens: 5,
133 reasoning_tokens: 10,
134 total_tokens: 150,
135 };
136 let json = serde_json::to_string(&usage).expect("serialize");
137 let deser: Usage = serde_json::from_str(&json).expect("deserialize");
138 assert_eq!(usage, deser);
139 }
140
141 #[test]
142 fn cost_roundtrip() {
143 let cost = Cost {
144 input_cost: 0.001,
145 output_cost: 0.003,
146 total_cost: 0.004,
147 ..Default::default()
148 };
149 let json = serde_json::to_string(&cost).expect("serialize");
150 let deser: Cost = serde_json::from_str(&json).expect("deserialize");
151 assert_eq!(cost, deser);
152 }
153}