rig_core/completion/cache_cost.rs
1//! What prompt caching cost and saved, priced the same way on every provider.
2
3use std::iter::Sum;
4use std::ops::{Add, AddAssign};
5
6use serde::{Deserialize, Serialize};
7
8use super::Usage;
9
10/// Prices of one model and tier, in USD per 1M tokens (storage: per 1M
11/// token-hours). The built-in catalog's [`Pricing`](crate::catalog::Pricing)
12/// holds the input, cached-read and cache-write prices of the models it
13/// lists, but no storage price. A provider without a separate write price
14/// bills writes at `input`; one without storage leaves `storage_per_hour`
15/// at zero.
16///
17/// ```no_run
18/// use rig_core::completion::CacheRates;
19///
20/// // A 5-minute cache write at 1.25 times the input price, reads at a tenth.
21/// let rates = CacheRates { input: 3.0, cached_read: 0.3, cache_write: 3.75, storage_per_hour: 0.0 };
22/// # let _ = rates;
23/// ```
24#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
25pub struct CacheRates {
26 /// Uncached input.
27 pub input: f64,
28 /// Input read from a cache.
29 pub cached_read: f64,
30 /// Input written to a cache, or put into a cache resource.
31 pub cache_write: f64,
32 /// Storing one token for one hour, for providers that bill cache storage.
33 pub storage_per_hour: f64,
34}
35
36/// A request's or a run's input tokens by how they are billed, plus the
37/// token-hours its cache resources were stored. Sum the costs of a run's
38/// calls (and of any cache resources beside them) and price the total with
39/// [`CacheCost::usd`].
40///
41/// A run priced the same way on any provider, with the caller's rates from
42/// the provider's price list:
43///
44/// ```no_run
45/// use rig_core::completion::{CacheCost, CacheRates, Usage};
46/// use rig_core::providers::gemini::{AutoCache, CacheBook};
47///
48/// # let calls: Vec<Usage> = Vec::new();
49/// # let book = CacheBook::new(AutoCache::default());
50/// let run: CacheCost = calls.iter().map(CacheCost::from_usage).sum();
51///
52/// let opus = CacheRates { input: 4.0, cached_read: 0.2, cache_write: 5.0, storage_per_hour: 0.0 };
53/// println!("${:.3}, {:.1}% saved", run.usd(&opus), run.saving(&opus) * 100.0);
54///
55/// // Gemini: the calls, plus the cache book's creation and storage.
56/// let gemini = run + CacheCost::from(&book.report());
57/// let flash = CacheRates { input: 0.75, cached_read: 0.075, cache_write: 0.75, storage_per_hour: 0.5 };
58/// println!("${:.3}", gemini.usd(&flash));
59/// ```
60#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
61pub struct CacheCost {
62 /// Input billed at the uncached price.
63 pub uncached_input: u64,
64 /// Input read from a cache.
65 pub cache_reads: u64,
66 /// Input written to a cache.
67 pub cache_writes: u64,
68 /// Σ tokens × hours of stored cache resources.
69 pub storage_token_hours: f64,
70}
71
72impl CacheCost {
73 /// The billed input of one call: the input neither read from nor written
74 /// to a cache is `input_tokens - cached_input_tokens -
75 /// cache_creation_input_tokens`. An unreported counter counts as zero;
76 /// reads and writes above `input_tokens` leave no uncached input rather
77 /// than a negative one.
78 pub fn from_usage(usage: &Usage) -> Self {
79 let input = usage.input_tokens.unwrap_or(0);
80 let cache_reads = usage.cached_input_tokens.unwrap_or(0);
81 let cache_writes = usage.cache_creation_input_tokens.unwrap_or(0);
82 Self {
83 uncached_input: input.saturating_sub(cache_reads + cache_writes),
84 cache_reads,
85 cache_writes,
86 storage_token_hours: 0.0,
87 }
88 }
89
90 /// Every input token, however it was billed.
91 pub fn prompt_tokens(&self) -> u64 {
92 self.uncached_input + self.cache_reads + self.cache_writes
93 }
94
95 /// Input cost in USD at `rates`: uncached, read, written and stored.
96 pub fn usd(&self, rates: &CacheRates) -> f64 {
97 (self.uncached_input as f64 * rates.input
98 + self.cache_reads as f64 * rates.cached_read
99 + self.cache_writes as f64 * rates.cache_write
100 + self.storage_token_hours * rates.storage_per_hour)
101 / 1e6
102 }
103
104 /// Input cost in USD at `rates` had every prompt token been uncached.
105 pub fn uncached_usd(&self, rates: &CacheRates) -> f64 {
106 self.prompt_tokens() as f64 * rates.input / 1e6
107 }
108
109 /// The share of the uncached input cost caching saved: `1 - usd /
110 /// uncached_usd`. Negative when caching cost more; zero without input.
111 pub fn saving(&self, rates: &CacheRates) -> f64 {
112 let uncached = self.uncached_usd(rates);
113 if uncached > 0.0 {
114 1.0 - self.usd(rates) / uncached
115 } else {
116 0.0
117 }
118 }
119}
120
121impl Add for CacheCost {
122 type Output = Self;
123
124 fn add(mut self, other: Self) -> Self {
125 self += other;
126 self
127 }
128}
129
130impl AddAssign for CacheCost {
131 fn add_assign(&mut self, other: Self) {
132 self.uncached_input += other.uncached_input;
133 self.cache_reads += other.cache_reads;
134 self.cache_writes += other.cache_writes;
135 self.storage_token_hours += other.storage_token_hours;
136 }
137}
138
139impl Sum for CacheCost {
140 fn sum<I: Iterator<Item = Self>>(costs: I) -> Self {
141 costs.fold(Self::default(), Add::add)
142 }
143}
144
145#[cfg(test)]
146mod tests;