Skip to main content

nanocodex_agent/
usage.rs

1pub use nanocodex_oai_api::pricing::{CostStatus, EstimatedUsdCost, ServiceTier, UsdAmount};
2use serde::{Deserialize, Serialize};
3
4/// Exact token accounting for every Responses call in one logical agent turn.
5///
6/// Cache-read and cache-write tokens are subsets of input tokens. Reasoning
7/// tokens are a subset of output tokens. The values are summed from provider
8/// usage records across warmup, generation, tool continuation, steering, and
9/// compaction calls made before the turn reaches its terminal boundary. Check
10/// [`Self::cost_status`] to distinguish a provider-omitted usage record from a
11/// genuine zero-token total.
12#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
13#[allow(clippy::struct_field_names)]
14pub struct TurnUsage {
15    input_tokens: u64,
16    cached_input_tokens: u64,
17    cache_write_input_tokens: u64,
18    output_tokens: u64,
19    reasoning_output_tokens: u64,
20    total_tokens: u64,
21    estimated_cost: Option<Box<EstimatedUsdCost>>,
22    cost_status: CostStatus,
23}
24
25/// Exact turn usage reported by an external backend.
26///
27/// Every field is named and required so wire adapters cannot silently swap or
28/// default adjacent token counters.
29#[derive(Clone, Debug, Eq, PartialEq)]
30pub struct ReportedTurnUsage {
31    /// All input tokens billed or reported by the backend.
32    pub input_tokens: u64,
33    /// Input tokens served from the backend's prompt cache.
34    pub cached_input_tokens: u64,
35    /// Input tokens newly written into the backend's prompt cache.
36    pub cache_write_input_tokens: u64,
37    /// All output tokens billed or reported by the backend.
38    pub output_tokens: u64,
39    /// Reasoning tokens included within `output_tokens`.
40    pub reasoning_output_tokens: u64,
41    /// Backend-reported aggregate token count.
42    pub total_tokens: u64,
43    /// Exact retained cost estimate, when one was reported.
44    pub estimated_cost: Option<EstimatedUsdCost>,
45    /// Availability and provenance of `estimated_cost`.
46    pub cost_status: CostStatus,
47}
48
49#[allow(clippy::struct_field_names)]
50#[derive(Clone)]
51#[cfg(feature = "openai")]
52pub(crate) struct TurnUsageCounts {
53    pub(crate) input_tokens: u64,
54    pub(crate) cached_input_tokens: u64,
55    pub(crate) cache_write_input_tokens: u64,
56    pub(crate) output_tokens: u64,
57    pub(crate) reasoning_output_tokens: u64,
58    pub(crate) total_tokens: u64,
59    pub(crate) reported: bool,
60    pub(crate) estimated_cost: Option<EstimatedUsdCost>,
61}
62
63impl TurnUsage {
64    /// Constructs exact usage reported by an external backend.
65    ///
66    /// All counts, the optional retained estimate, and its status are explicit
67    /// so a wire boundary cannot silently default an omitted field. This API
68    /// lets dependency-light backends construct usage without a serialization
69    /// round trip.
70    #[must_use]
71    pub fn from_reported(reported: ReportedTurnUsage) -> Self {
72        Self {
73            input_tokens: reported.input_tokens,
74            cached_input_tokens: reported.cached_input_tokens,
75            cache_write_input_tokens: reported.cache_write_input_tokens,
76            output_tokens: reported.output_tokens,
77            reasoning_output_tokens: reported.reasoning_output_tokens,
78            total_tokens: reported.total_tokens,
79            estimated_cost: reported.estimated_cost.map(Box::new),
80            cost_status: reported.cost_status,
81        }
82    }
83
84    #[cfg(feature = "openai")]
85    pub(crate) fn from_counts(counts: TurnUsageCounts) -> Self {
86        let (estimated_cost, cost_status) = if !counts.reported {
87            (None, CostStatus::UsageNotReported)
88        } else {
89            (
90                counts.estimated_cost.map(Box::new),
91                CostStatus::EstimatedFromUsage,
92            )
93        };
94        Self {
95            input_tokens: counts.input_tokens,
96            cached_input_tokens: counts.cached_input_tokens,
97            cache_write_input_tokens: counts.cache_write_input_tokens,
98            output_tokens: counts.output_tokens,
99            reasoning_output_tokens: counts.reasoning_output_tokens,
100            total_tokens: counts.total_tokens,
101            estimated_cost,
102            cost_status,
103        }
104    }
105
106    /// Returns all input tokens billed or reported by the provider.
107    #[must_use]
108    pub const fn input_tokens(&self) -> u64 {
109        self.input_tokens
110    }
111
112    /// Returns input tokens served from the provider's prompt cache.
113    #[must_use]
114    pub const fn cached_input_tokens(&self) -> u64 {
115        self.cached_input_tokens
116    }
117
118    /// Returns input tokens newly written into the provider's prompt cache.
119    #[must_use]
120    pub const fn cache_write_input_tokens(&self) -> u64 {
121        self.cache_write_input_tokens
122    }
123
124    /// Returns all output tokens billed or reported by the provider.
125    #[must_use]
126    pub const fn output_tokens(&self) -> u64 {
127        self.output_tokens
128    }
129
130    /// Returns reasoning tokens included within [`Self::output_tokens`].
131    #[must_use]
132    pub const fn reasoning_output_tokens(&self) -> u64 {
133        self.reasoning_output_tokens
134    }
135
136    /// Returns the provider-reported total token count.
137    #[must_use]
138    pub const fn total_tokens(&self) -> u64 {
139        self.total_tokens
140    }
141
142    /// Returns the automatic local USD estimate.
143    ///
144    /// Nanocodex applies the selected model's built-in rates for the requested
145    /// processing mode. `None` means the provider omitted usage;
146    /// [`Self::cost_status`] distinguishes that from a genuine zero-token
147    /// estimate.
148    #[must_use]
149    pub fn estimated_cost(&self) -> Option<&EstimatedUsdCost> {
150        self.estimated_cost.as_deref()
151    }
152
153    /// Returns why an estimate is present or unavailable.
154    #[must_use]
155    pub const fn cost_status(&self) -> CostStatus {
156        self.cost_status
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::{CostStatus, ReportedTurnUsage, TurnUsage};
163
164    #[test]
165    fn externally_reported_usage_preserves_exact_counts_and_cost_status() {
166        let usage = TurnUsage::from_reported(ReportedTurnUsage {
167            input_tokens: 13,
168            cached_input_tokens: 5,
169            cache_write_input_tokens: 2,
170            output_tokens: 8,
171            reasoning_output_tokens: 3,
172            total_tokens: 21,
173            estimated_cost: None,
174            cost_status: CostStatus::UsageNotReported,
175        });
176
177        assert_eq!(usage.input_tokens(), 13);
178        assert_eq!(usage.cached_input_tokens(), 5);
179        assert_eq!(usage.cache_write_input_tokens(), 2);
180        assert_eq!(usage.output_tokens(), 8);
181        assert_eq!(usage.reasoning_output_tokens(), 3);
182        assert_eq!(usage.total_tokens(), 21);
183        assert_eq!(usage.cost_status(), CostStatus::UsageNotReported);
184        assert!(usage.estimated_cost().is_none());
185    }
186}