1use std::fmt;
4use std::str::FromStr;
5
6use serde::{Deserialize, Serialize};
7
8use crate::json::js_f64;
9
10pub type TimestampMs = i64;
12
13#[derive(
16 Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
17)]
18#[serde(rename_all = "lowercase")]
19pub enum ThinkingLevel {
20 #[default]
22 Off,
23 Minimal,
25 Low,
27 Medium,
29 High,
31 Xhigh,
33 Max,
35}
36
37impl ThinkingLevel {
38 pub const ALL: [Self; 7] = [
40 Self::Off,
41 Self::Minimal,
42 Self::Low,
43 Self::Medium,
44 Self::High,
45 Self::Xhigh,
46 Self::Max,
47 ];
48
49 #[must_use]
51 pub const fn as_str(self) -> &'static str {
52 match self {
53 Self::Off => "off",
54 Self::Minimal => "minimal",
55 Self::Low => "low",
56 Self::Medium => "medium",
57 Self::High => "high",
58 Self::Xhigh => "xhigh",
59 Self::Max => "max",
60 }
61 }
62
63 #[must_use]
66 pub fn clamp(self, available: &[Self]) -> Self {
67 if available.contains(&self) {
68 return self;
69 }
70 let requested = self as usize;
71 Self::ALL[requested..]
72 .iter()
73 .chain(Self::ALL[..requested].iter().rev())
74 .copied()
75 .find(|level| available.contains(level))
76 .unwrap_or_else(|| available.first().copied().unwrap_or(Self::Off))
77 }
78}
79
80impl fmt::Display for ThinkingLevel {
81 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82 f.write_str(self.as_str())
83 }
84}
85
86#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
88#[error("unknown thinking level: {0}")]
89pub struct UnknownThinkingLevel(pub String);
90
91impl FromStr for ThinkingLevel {
92 type Err = UnknownThinkingLevel;
93
94 fn from_str(s: &str) -> Result<Self, Self::Err> {
95 Self::ALL
96 .into_iter()
97 .find(|level| level.as_str() == s)
98 .ok_or_else(|| UnknownThinkingLevel(s.to_owned()))
99 }
100}
101
102#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
104#[serde(rename_all = "camelCase")]
105pub enum StopReason {
106 Pending,
108 Stop,
110 Length,
112 ToolUse,
114 Error,
116 Aborted,
118 Deferred,
120}
121
122#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
124#[serde(rename_all = "lowercase")]
125pub enum InputModality {
126 Text,
128 Image,
130}
131
132#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
134#[serde(rename_all = "lowercase")]
135pub enum CacheRetention {
136 None,
138 Short,
140 Long,
142}
143
144#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
146#[serde(rename_all = "camelCase")]
147pub struct UsageCost {
148 #[serde(with = "js_f64")]
150 pub input: f64,
151 #[serde(with = "js_f64")]
153 pub output: f64,
154 #[serde(with = "js_f64")]
156 pub cache_read: f64,
157 #[serde(with = "js_f64")]
159 pub cache_write: f64,
160 #[serde(with = "js_f64")]
162 pub total: f64,
163}
164
165#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
167#[serde(rename_all = "camelCase")]
168pub struct Usage {
169 pub input: u64,
171 pub output: u64,
173 pub cache_read: u64,
175 pub cache_write: u64,
177 #[serde(
179 rename = "cacheWrite1h",
180 default,
181 skip_serializing_if = "Option::is_none"
182 )]
183 pub cache_write_1h: Option<u64>,
184 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub reasoning: Option<u64>,
187 pub total_tokens: u64,
189 #[serde(default)]
191 pub cost: UsageCost,
192}
193
194impl Usage {
195 pub fn accumulate(&mut self, other: &Self) {
197 self.input += other.input;
198 self.output += other.output;
199 self.cache_read += other.cache_read;
200 self.cache_write += other.cache_write;
201 self.cache_write_1h = add_opt(self.cache_write_1h, other.cache_write_1h);
202 self.reasoning = add_opt(self.reasoning, other.reasoning);
203 self.total_tokens += other.total_tokens;
204 self.cost.input += other.cost.input;
205 self.cost.output += other.cost.output;
206 self.cost.cache_read += other.cost.cache_read;
207 self.cost.cache_write += other.cost.cache_write;
208 self.cost.total += other.cost.total;
209 }
210}
211
212fn add_opt(a: Option<u64>, b: Option<u64>) -> Option<u64> {
213 match (a, b) {
214 (None, None) => None,
215 (a, b) => Some(a.unwrap_or(0) + b.unwrap_or(0)),
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use serde_json::json;
223
224 #[test]
225 fn thinking_level_wire_values() {
226 assert_eq!(
227 serde_json::to_value(ThinkingLevel::Xhigh).ok(),
228 Some(json!("xhigh"))
229 );
230 assert_eq!("max".parse::<ThinkingLevel>(), Ok(ThinkingLevel::Max));
231 assert!("x_high".parse::<ThinkingLevel>().is_err());
232 }
233
234 #[test]
235 fn clamp_prefers_higher_then_lower() {
236 use ThinkingLevel::{High, Low, Max, Medium, Minimal, Off, Xhigh};
237 let available = [Off, Minimal, Low, Medium, High];
238 assert_eq!(Xhigh.clamp(&available), High);
239 assert_eq!(Max.clamp(&available), High);
240 assert_eq!(Low.clamp(&available), Low);
241 assert_eq!(Minimal.clamp(&[Off, Medium]), Medium);
242 assert_eq!(High.clamp(&[Off]), Off);
243 assert_eq!(High.clamp(&[]), Off);
244 }
245
246 #[test]
247 fn usage_round_trips_pi_shape() {
248 let raw = json!({"input":0,"output":0,"cacheRead":50000,"cacheWrite":0,"totalTokens":50000,
249 "cost":{"input":0,"output":0,"cacheRead":0.015,"cacheWrite":0,"total":0.015}});
250 let usage: Usage = serde_json::from_value(raw.clone()).unwrap_or_default();
251 assert_eq!(usage.cache_read, 50_000);
252 assert_eq!(serde_json::to_value(usage).ok(), Some(raw));
253 let text = serde_json::to_string(&usage).unwrap_or_default();
254 assert!(text.contains("\"input\":0,"), "{text}");
255 }
256}