Skip to main content

ferrin_spec/language_model/
usage.rs

1//! Token usage reported by language models.
2
3use serde::Deserialize;
4use serde::Serialize;
5
6use crate::json::JsonObject;
7
8/// Token usage of a single language model call or of several added calls.
9///
10/// Every counter is optional because providers report different subsets.
11/// Standard fields that cannot be mapped are `None`; the provider's original
12/// usage object is available in `raw`.
13#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
14pub struct Usage {
15    /// Input (prompt) token counts.
16    #[serde(default)]
17    pub input: InputTokens,
18    /// Output (completion) token counts.
19    #[serde(default)]
20    pub output: OutputTokens,
21    /// Provider-specific usage object as returned by the API.
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub raw: Option<JsonObject>,
24}
25
26/// Input token counts.
27#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
28pub struct InputTokens {
29    /// Total input tokens, including cached tokens.
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub total: Option<u64>,
32    /// Input tokens that were neither read from nor written to a cache.
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub no_cache: Option<u64>,
35    /// Input tokens read from a prompt cache.
36    #[serde(default, skip_serializing_if = "Option::is_none")]
37    pub cache_read: Option<u64>,
38    /// Input tokens written to a prompt cache.
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub cache_write: Option<u64>,
41}
42
43/// Output token counts.
44#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
45pub struct OutputTokens {
46    /// Total output tokens, including reasoning tokens.
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub total: Option<u64>,
49    /// Output tokens that are visible text.
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub text: Option<u64>,
52    /// Output tokens spent on reasoning.
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub reasoning: Option<u64>,
55}
56
57/// Adds two optional counters: `None + None = None`, `Some(a) + None = Some(a)`.
58#[must_use]
59pub fn add_token_counts(a: Option<u64>, b: Option<u64>) -> Option<u64> {
60    match (a, b) {
61        (None, None) => None,
62        (Some(a), None) => Some(a),
63        (None, Some(b)) => Some(b),
64        (Some(a), Some(b)) => Some(a.saturating_add(b)),
65    }
66}
67
68impl Usage {
69    /// Creates a usage with only the total input and output counts set.
70    #[must_use]
71    pub fn totals(input: u64, output: u64) -> Self {
72        Self {
73            input: InputTokens {
74                total: Some(input),
75                ..InputTokens::default()
76            },
77            output: OutputTokens {
78                total: Some(output),
79                ..OutputTokens::default()
80            },
81            raw: None,
82        }
83    }
84
85    /// Adds two usages counter by counter.
86    ///
87    /// `None + None = None`; `Some(a) + None = Some(a)`. The `raw` object is
88    /// dropped because provider payloads cannot be combined generically.
89    #[must_use]
90    pub fn add(&self, other: &Usage) -> Usage {
91        Usage {
92            input: InputTokens {
93                total: add_token_counts(self.input.total, other.input.total),
94                no_cache: add_token_counts(self.input.no_cache, other.input.no_cache),
95                cache_read: add_token_counts(self.input.cache_read, other.input.cache_read),
96                cache_write: add_token_counts(self.input.cache_write, other.input.cache_write),
97            },
98            output: OutputTokens {
99                total: add_token_counts(self.output.total, other.output.total),
100                text: add_token_counts(self.output.text, other.output.text),
101                reasoning: add_token_counts(self.output.reasoning, other.output.reasoning),
102            },
103            raw: None,
104        }
105    }
106
107    /// Total tokens: input total plus output total (`None` when both unknown).
108    #[must_use]
109    pub fn total_tokens(&self) -> Option<u64> {
110        add_token_counts(self.input.total, self.output.total)
111    }
112}