layover_core/cost/mod.rs
1//! What a run actually cost, and where that figure came from.
2//!
3//! Fuel answers "may this itinerary keep going?". This module answers the questions an operator
4//! asks afterwards: *where did the money go, and do I believe the number?*
5//!
6//! # Why provenance is a first-class field
7//!
8//! A cost figure can come from three places, and they are not interchangeable:
9//!
10//! - the runner reported it, which is the only figure worth trusting;
11//! - Layover derived it from token counts and a rate card, which is an estimate;
12//! - nothing was reported at all, which is a hole.
13//!
14//! Collapsing those into one number is how budgets quietly become fiction. A sibling project that
15//! priced runs from a hand-maintained rate card ran **2.7× over actual** — it billed one model at
16//! `$75` per million output tokens where the provider charged `$25` — and nothing in the totals
17//! said "this is a guess". [`CostSource`] exists so that can never be invisible here: totals are
18//! reported separately by source, and [`ledger::Summary`] will tell you what share of a bill is
19//! actually measured.
20
21pub mod ledger;
22pub mod rates;
23pub mod reserve;
24pub mod window;
25
26use jiff::Timestamp;
27
28use serde::{Deserialize, Serialize};
29
30use crate::agent::AgentName;
31use crate::flight::{ItineraryId, RunId};
32
33pub use ledger::{Ledger, Summary};
34pub use rates::{ModelRates, RateCard};
35pub use reserve::{Reserve, ReserveState};
36pub use window::{RETENTION_DAYS, Span, Window};
37
38/// Tokens consumed by one run.
39///
40/// Cache reads and writes are tracked apart from ordinary input because providers price them
41/// differently — often by an order of magnitude — so folding them together would make any derived
42/// cost wrong in a way that looks plausible.
43#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
44pub struct TokenUsage {
45 /// Prompt tokens billed at the input rate.
46 pub input: u64,
47 /// Generated tokens.
48 pub output: u64,
49 /// Tokens served from the provider's prompt cache.
50 pub cache_read: u64,
51 /// Tokens written into the provider's prompt cache.
52 pub cache_write: u64,
53}
54
55impl TokenUsage {
56 /// Total tokens, however they were billed.
57 #[must_use]
58 pub fn total(&self) -> u64 {
59 self.input
60 .saturating_add(self.output)
61 .saturating_add(self.cache_read)
62 .saturating_add(self.cache_write)
63 }
64
65 /// Returns `true` when nothing was recorded.
66 #[must_use]
67 pub fn is_empty(&self) -> bool {
68 self.total() == 0
69 }
70
71 /// Adds another run's usage to this one, saturating rather than wrapping.
72 #[must_use]
73 pub fn saturating_add(self, other: Self) -> Self {
74 Self {
75 input: self.input.saturating_add(other.input),
76 output: self.output.saturating_add(other.output),
77 cache_read: self.cache_read.saturating_add(other.cache_read),
78 cache_write: self.cache_write.saturating_add(other.cache_write),
79 }
80 }
81}
82
83/// Where a cost figure came from.
84///
85/// Ordered by how much it can be trusted, so `max` over a set of sources gives the weakest link.
86#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
87#[serde(rename_all = "snake_case")]
88pub enum CostSource {
89 /// The runner reported the figure. The only kind worth billing against.
90 Reported,
91 /// Derived from token counts and a [`RateCard`]. An estimate, and labelled as one.
92 RateCard,
93 /// The runner reported neither cost nor tokens. The figure is zero and means nothing.
94 Unreported,
95}
96
97impl CostSource {
98 /// Returns `true` when the figure came from the runner itself.
99 #[must_use]
100 pub fn is_measured(&self) -> bool {
101 matches!(self, Self::Reported)
102 }
103}
104
105/// What one run cost, and how well that is known.
106#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
107pub struct RunCost {
108 /// The run this describes.
109 pub run: RunId,
110 /// The chain it belonged to.
111 pub itinerary: ItineraryId,
112 /// Which agent was run.
113 pub agent: AgentName,
114 /// The pipeline whose trigger began this chain, when one did.
115 ///
116 /// Carried here rather than only on the run record so that spend can be attributed to the
117 /// *workflow* that caused it. "What does the nightly sweep cost me" is the first question a
118 /// factory with several pipelines raises, and per-agent totals cannot answer it when an agent
119 /// belongs to more than one.
120 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub pipeline: Option<crate::pipeline::PipelineName>,
122 /// Which model, when the runner said.
123 pub model: Option<String>,
124 /// Tokens consumed, as far as they are known.
125 pub usage: TokenUsage,
126 /// Cost in US dollars. Always zero when `source` is [`CostSource::Unreported`].
127 pub usd: f64,
128 /// Where `usd` came from.
129 pub source: CostSource,
130 /// When the run finished.
131 pub at: Timestamp,
132}
133
134impl RunCost {
135 /// Records a cost the runner reported directly.
136 ///
137 /// A non-finite or negative figure is treated as no report at all rather than trusted: it
138 /// comes from parsing a child process's output, and a `NaN` in a budget poisons every total
139 /// downstream of it.
140 #[must_use]
141 pub fn reported(
142 run: RunId,
143 itinerary: ItineraryId,
144 agent: AgentName,
145 model: Option<String>,
146 usage: TokenUsage,
147 usd: f64,
148 ) -> Self {
149 // A zero alongside real token counts is not a measurement, it is a runner that did not
150 // fill the field in. Believing it is how every economic rail is defeated at once: Fuel
151 // debits nothing, the Reserve records nothing, and the run cap — the deterministic
152 // fallback for exactly this case — never engages, because the cost *was* reported.
153 //
154 // Tokens are kept so a rate card can price it later. Zero cost with zero tokens is left
155 // alone: a run that genuinely did nothing is a real measurement, and the distinction
156 // between that and silence is the whole reason this field exists.
157 let implausible = usd == 0.0 && !usage.is_empty();
158
159 let (usd, source) = if usd.is_finite() && usd >= 0.0 && !implausible {
160 (usd, CostSource::Reported)
161 } else {
162 (0.0, CostSource::Unreported)
163 };
164
165 Self {
166 run,
167 itinerary,
168 agent,
169 pipeline: None,
170 model,
171 usage,
172 usd,
173 source,
174 at: Timestamp::now(),
175 }
176 }
177
178 /// Records a run whose runner said nothing about cost.
179 #[must_use]
180 pub fn unreported(
181 run: RunId,
182 itinerary: ItineraryId,
183 agent: AgentName,
184 model: Option<String>,
185 ) -> Self {
186 Self {
187 run,
188 itinerary,
189 agent,
190 pipeline: None,
191 model,
192 usage: TokenUsage::default(),
193 usd: 0.0,
194 source: CostSource::Unreported,
195 at: Timestamp::now(),
196 }
197 }
198
199 /// Attributes the cost to the workflow that caused it.
200 #[must_use]
201 pub fn from_pipeline(mut self, pipeline: crate::pipeline::PipelineName) -> Self {
202 self.pipeline = Some(pipeline);
203 self
204 }
205
206 /// Overrides when the run finished, for tests and for replaying a persisted ledger.
207 #[must_use]
208 pub fn at(mut self, at: Timestamp) -> Self {
209 self.at = at;
210 self
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 fn usage() -> TokenUsage {
219 TokenUsage {
220 input: 1_000,
221 output: 500,
222 cache_read: 250,
223 cache_write: 100,
224 }
225 }
226
227 fn run_cost(usd: f64) -> RunCost {
228 RunCost::reported(
229 RunId::generate(),
230 ItineraryId::generate(),
231 "analyst".into(),
232 Some("claude-opus-5".to_owned()),
233 usage(),
234 usd,
235 )
236 }
237
238 #[test]
239 fn token_totals_cover_every_billed_kind() {
240 assert_eq!(usage().total(), 1_850);
241 assert!(!usage().is_empty());
242 assert!(TokenUsage::default().is_empty());
243 }
244
245 #[test]
246 fn token_usage_adds_without_wrapping() {
247 let huge = TokenUsage {
248 input: u64::MAX,
249 ..TokenUsage::default()
250 };
251
252 assert_eq!(huge.saturating_add(usage()).input, u64::MAX);
253 }
254
255 #[test]
256 fn a_reported_cost_is_marked_as_measured() {
257 let cost = run_cost(1.25);
258
259 assert_eq!(cost.source, CostSource::Reported);
260 assert!(cost.source.is_measured());
261 assert!((cost.usd - 1.25).abs() < 1e-9);
262 }
263
264 #[test]
265 fn a_genuine_zero_is_still_a_report() {
266 // A run that truly cost nothing is different from a runner that said nothing, and the
267 // difference decides whether the budget rail is working. With no tokens either, zero is
268 // a real measurement.
269 let cost = RunCost::reported(
270 RunId::generate(),
271 ItineraryId::generate(),
272 "analyst".into(),
273 None,
274 TokenUsage::default(),
275 0.0,
276 );
277
278 assert_eq!(cost.source, CostSource::Reported);
279 }
280
281 #[test]
282 fn zero_dollars_alongside_real_tokens_is_silence_rather_than_a_measurement() {
283 // The cheapest way to defeat every economic rail at once. Believing a zero report means
284 // Fuel debits nothing, the Reserve records nothing, and the run cap — the deterministic
285 // fallback for exactly this case — never engages, because as far as the accounting is
286 // concerned the cost *was* reported.
287 let cost = run_cost(0.0);
288
289 assert_eq!(cost.source, CostSource::Unreported);
290 assert!(
291 !cost.usage.is_empty(),
292 "the tokens are kept so a rate card can price it"
293 );
294 }
295
296 #[test]
297 fn an_implausible_report_is_downgraded_rather_than_trusted() {
298 for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -1.0] {
299 let cost = run_cost(bad);
300
301 assert_eq!(
302 cost.source,
303 CostSource::Unreported,
304 "{bad} must not be treated as a measured cost"
305 );
306 assert!((cost.usd - 0.0).abs() < f64::EPSILON);
307 }
308 }
309
310 #[test]
311 fn an_unreported_run_carries_no_figures_at_all() {
312 let cost = RunCost::unreported(
313 RunId::generate(),
314 ItineraryId::generate(),
315 "analyst".into(),
316 None,
317 );
318
319 assert_eq!(cost.source, CostSource::Unreported);
320 assert!(!cost.source.is_measured());
321 assert!(cost.usage.is_empty());
322 }
323
324 #[test]
325 fn sources_order_from_most_to_least_trustworthy() {
326 // `max` over a set of sources therefore yields the weakest link, which is what a summary
327 // should report rather than the most flattering one.
328 assert!(CostSource::Reported < CostSource::RateCard);
329 assert!(CostSource::RateCard < CostSource::Unreported);
330 }
331}