1use std::fmt;
2use std::str::FromStr;
3
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use thiserror::Error;
6
7pub const MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991;
9const MAX_DECIMAL_DIGITS: usize = 36;
10const MAX_DECIMAL_SCALE: usize = 18;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
14#[serde(transparent)]
15pub struct TokenCount(u64);
16
17impl TokenCount {
18 pub const fn new(value: u64) -> Result<Self, UsageError> {
25 if value <= MAX_SAFE_INTEGER {
26 Ok(Self(value))
27 } else {
28 Err(UsageError::UnsafeInteger)
29 }
30 }
31
32 #[must_use]
34 pub const fn get(self) -> u64 {
35 self.0
36 }
37}
38
39impl<'de> Deserialize<'de> for TokenCount {
40 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
41 where
42 D: Deserializer<'de>,
43 {
44 let value = u64::deserialize(deserializer)?;
45 Self::new(value).map_err(serde::de::Error::custom)
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
51#[serde(rename_all = "camelCase")]
52#[allow(clippy::struct_field_names)] pub struct Usage {
54 input_tokens: TokenCount,
55 output_tokens: TokenCount,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 cache_read_tokens: Option<TokenCount>,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 cache_write_tokens: Option<TokenCount>,
60 #[serde(skip_serializing_if = "Option::is_none")]
61 reasoning_tokens: Option<TokenCount>,
62}
63
64impl Usage {
65 #[must_use]
67 pub const fn new(input_tokens: TokenCount, output_tokens: TokenCount) -> Self {
68 Self {
69 input_tokens,
70 output_tokens,
71 cache_read_tokens: None,
72 cache_write_tokens: None,
73 reasoning_tokens: None,
74 }
75 }
76
77 #[must_use]
79 pub const fn input_tokens(&self) -> TokenCount {
80 self.input_tokens
81 }
82
83 #[must_use]
85 pub const fn output_tokens(&self) -> TokenCount {
86 self.output_tokens
87 }
88
89 #[must_use]
91 pub const fn cache_read_tokens(&self) -> Option<TokenCount> {
92 self.cache_read_tokens
93 }
94
95 #[must_use]
97 pub const fn cache_write_tokens(&self) -> Option<TokenCount> {
98 self.cache_write_tokens
99 }
100
101 #[must_use]
103 pub const fn reasoning_tokens(&self) -> Option<TokenCount> {
104 self.reasoning_tokens
105 }
106
107 #[must_use]
109 pub const fn with_cache_read(mut self, value: TokenCount) -> Self {
110 self.cache_read_tokens = Some(value);
111 self
112 }
113
114 #[must_use]
116 pub const fn with_cache_write(mut self, value: TokenCount) -> Self {
117 self.cache_write_tokens = Some(value);
118 self
119 }
120
121 pub fn with_reasoning(mut self, value: TokenCount) -> Result<Self, UsageError> {
128 if value > self.output_tokens {
129 return Err(UsageError::ReasoningExceedsOutput);
130 }
131 self.reasoning_tokens = Some(value);
132 Ok(self)
133 }
134
135 pub fn total_tokens(&self) -> Result<TokenCount, UsageError> {
142 let total = [
143 Some(self.input_tokens),
144 Some(self.output_tokens),
145 self.cache_read_tokens,
146 self.cache_write_tokens,
147 ]
148 .into_iter()
149 .flatten()
150 .try_fold(0_u64, |total, value| total.checked_add(value.get()))
151 .ok_or(UsageError::TotalOverflow)?;
152 TokenCount::new(total)
153 }
154}
155
156#[derive(Deserialize)]
157#[serde(rename_all = "camelCase")]
158struct RawUsage {
159 #[serde(rename = "inputTokens")]
160 input: TokenCount,
161 #[serde(rename = "outputTokens")]
162 output: TokenCount,
163 #[serde(default, rename = "cacheReadTokens")]
164 cache_read: Option<TokenCount>,
165 #[serde(default, rename = "cacheWriteTokens")]
166 cache_write: Option<TokenCount>,
167 #[serde(default, rename = "reasoningTokens")]
168 reasoning: Option<TokenCount>,
169}
170
171impl<'de> Deserialize<'de> for Usage {
172 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
173 where
174 D: Deserializer<'de>,
175 {
176 let raw = RawUsage::deserialize(deserializer)?;
177 let mut usage = Self::new(raw.input, raw.output);
178 usage.cache_read_tokens = raw.cache_read;
179 usage.cache_write_tokens = raw.cache_write;
180 if let Some(reasoning) = raw.reasoning {
181 usage = usage
182 .with_reasoning(reasoning)
183 .map_err(serde::de::Error::custom)?;
184 }
185 usage.total_tokens().map_err(serde::de::Error::custom)?;
186 Ok(usage)
187 }
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
192pub enum UsageError {
193 #[error("token count exceeds the JSON safe-integer range")]
195 UnsafeInteger,
196 #[error("reasoning tokens must be a subset of output tokens")]
198 ReasoningExceedsOutput,
199 #[error("total token count exceeds the supported range")]
201 TotalOverflow,
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
206pub struct DecimalAmount(String);
207
208impl DecimalAmount {
209 #[must_use]
211 pub fn as_str(&self) -> &str {
212 &self.0
213 }
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
218pub enum DecimalAmountParseError {
219 #[error("amount must use canonical non-negative decimal text")]
221 InvalidFormat,
222 #[error("amount exceeds the supported precision or scale")]
224 TooPrecise,
225}
226
227impl FromStr for DecimalAmount {
228 type Err = DecimalAmountParseError;
229
230 fn from_str(value: &str) -> Result<Self, Self::Err> {
231 let (integer, fraction) = value
232 .split_once('.')
233 .map_or((value, None), |(left, right)| (left, Some(right)));
234 if integer.is_empty()
235 || !integer.bytes().all(|byte| byte.is_ascii_digit())
236 || (integer.len() > 1 && integer.starts_with('0'))
237 {
238 return Err(DecimalAmountParseError::InvalidFormat);
239 }
240 if let Some(fraction) = fraction {
241 if fraction.is_empty()
242 || !fraction.bytes().all(|byte| byte.is_ascii_digit())
243 || fraction.ends_with('0')
244 {
245 return Err(DecimalAmountParseError::InvalidFormat);
246 }
247 if fraction.len() > MAX_DECIMAL_SCALE {
248 return Err(DecimalAmountParseError::TooPrecise);
249 }
250 }
251 let digits = integer.len() + fraction.map_or(0, str::len);
252 if digits > MAX_DECIMAL_DIGITS {
253 return Err(DecimalAmountParseError::TooPrecise);
254 }
255 Ok(Self(value.to_owned()))
256 }
257}
258
259impl fmt::Display for DecimalAmount {
260 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
261 formatter.write_str(&self.0)
262 }
263}
264
265impl Serialize for DecimalAmount {
266 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
267 where
268 S: Serializer,
269 {
270 serializer.serialize_str(&self.0)
271 }
272}
273
274impl<'de> Deserialize<'de> for DecimalAmount {
275 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
276 where
277 D: Deserializer<'de>,
278 {
279 String::deserialize(deserializer)?
280 .parse()
281 .map_err(serde::de::Error::custom)
282 }
283}
284
285#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
287pub struct CurrencyCode(String);
288
289impl CurrencyCode {
290 #[must_use]
292 pub fn as_str(&self) -> &str {
293 &self.0
294 }
295}
296
297#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
299#[error("currency must contain exactly three uppercase ASCII letters")]
300pub struct CurrencyCodeParseError;
301
302impl FromStr for CurrencyCode {
303 type Err = CurrencyCodeParseError;
304
305 fn from_str(value: &str) -> Result<Self, Self::Err> {
306 if value.len() == 3 && value.bytes().all(|byte| byte.is_ascii_uppercase()) {
307 Ok(Self(value.to_owned()))
308 } else {
309 Err(CurrencyCodeParseError)
310 }
311 }
312}
313
314impl fmt::Display for CurrencyCode {
315 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
316 formatter.write_str(&self.0)
317 }
318}
319
320impl Serialize for CurrencyCode {
321 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
322 where
323 S: Serializer,
324 {
325 serializer.serialize_str(&self.0)
326 }
327}
328
329impl<'de> Deserialize<'de> for CurrencyCode {
330 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
331 where
332 D: Deserializer<'de>,
333 {
334 String::deserialize(deserializer)?
335 .parse()
336 .map_err(serde::de::Error::custom)
337 }
338}
339
340#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
342#[serde(rename_all = "snake_case")]
343pub enum CostUnit {
344 MajorCurrency,
346}
347
348#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
350#[serde(rename_all = "camelCase")]
351pub struct ExactCost {
352 amount: DecimalAmount,
353 currency: CurrencyCode,
354 unit: CostUnit,
355}
356
357impl ExactCost {
358 #[must_use]
360 pub const fn new(amount: DecimalAmount, currency: CurrencyCode) -> Self {
361 Self {
362 amount,
363 currency,
364 unit: CostUnit::MajorCurrency,
365 }
366 }
367
368 #[must_use]
370 pub const fn amount(&self) -> &DecimalAmount {
371 &self.amount
372 }
373
374 #[must_use]
376 pub const fn currency(&self) -> &CurrencyCode {
377 &self.currency
378 }
379
380 #[must_use]
382 pub const fn unit(&self) -> CostUnit {
383 self.unit
384 }
385}