Skip to main content

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}