1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
//! BP-7 (catalog §4a "Turn/budget caps" — the *spend* cap; "Per-turn
//! cost/usage accounting" — the *cost* half): token→dollars on the request
//! path.
//!
//! Until BP-7 the only pricing in the workspace was
//! [`crate::pricing_ref`], whose own module doc says it is "not used
//! anywhere on the request path" — which is exactly why the ledger's
//! `turn-budget-caps` row read "no spend/budget cap at all — no pricing
//! exists on the request path". This module is that missing piece, and
//! nothing more: a per-million-token price pair for a model id, resolved
//! from (1) the config's explicit override
//! ([`crate::Config::price_input_per_mtok`] /
//! [`crate::Config::price_output_per_mtok`]) or (2) a small built-in table
//! of published list prices for the model families the parity presets pin.
//!
//! **Deliberately fails closed.** [`resolve`] returns `None` for a model it
//! cannot price rather than guessing, and [`crate::Agent::new`] refuses to
//! build an agent that arms `core.max_budget_usd` against an unpriceable
//! model. A spend cap that silently never bites is worse than no cap: the
//! caller believes they are protected.
/// Per-million-token prices for one model, in US dollars.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ModelPrice {
/// Input (prompt) tokens, dollars per million.
pub input_per_mtok: f64,
/// Output (completion) tokens, dollars per million.
pub output_per_mtok: f64,
}
impl ModelPrice {
/// Dollar cost of one round-trip's token counts.
///
/// Cached prompt tokens are billed at the full input rate here: the
/// provider-reported discount varies per provider and per cache tier,
/// and over-reporting cost is the safe direction for a *cap* (a budget
/// that stops slightly early never overspends). Named rather than
/// silently assumed — see the `per-turn-cost-usage-accounting` ledger
/// row's note.
pub fn cost_usd(&self, prompt_tokens: u64, completion_tokens: u64) -> f64 {
(prompt_tokens as f64 / 1_000_000.0) * self.input_per_mtok
+ (completion_tokens as f64 / 1_000_000.0) * self.output_per_mtok
}
}
/// Published list prices for the model families the parity presets pin,
/// matched by SUBSTRING on the model id so the provider-prefixed spellings
/// (`anthropic/claude-opus-4-8`, `claude-opus-4-8`,
/// `us.anthropic.claude-opus-4-8-v1:0`) all resolve to the same row.
///
/// Longest pattern first — `claude-haiku` must win over a shorter prefix
/// that would also match.
const BUILT_IN_PRICES: &[(&str, ModelPrice)] = &[
(
"claude-opus",
ModelPrice {
input_per_mtok: crate::pricing_ref::REF_INPUT_PER_MTOK,
output_per_mtok: crate::pricing_ref::REF_OUTPUT_PER_MTOK,
},
),
(
"claude-sonnet",
ModelPrice {
input_per_mtok: 3.00,
output_per_mtok: 15.00,
},
),
(
"claude-haiku",
ModelPrice {
input_per_mtok: 1.00,
output_per_mtok: 5.00,
},
),
];
/// The built-in list price for `model`, if this build knows one.
pub fn built_in(model: &str) -> Option<ModelPrice> {
BUILT_IN_PRICES
.iter()
.find(|(pattern, _)| model.contains(pattern))
.map(|(_, price)| *price)
}
/// Resolve the price to bill `model` at: the explicit config override when
/// BOTH halves are set, else the built-in table, else `None`.
///
/// Both override halves are required together on purpose — an input price
/// with no output price would silently bill completions at zero.
pub fn resolve(
model: &str,
price_input_per_mtok: Option<f64>,
price_output_per_mtok: Option<f64>,
) -> Option<ModelPrice> {
match (price_input_per_mtok, price_output_per_mtok) {
(Some(input_per_mtok), Some(output_per_mtok)) => Some(ModelPrice {
input_per_mtok,
output_per_mtok,
}),
_ => built_in(model),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn built_in_matches_every_provider_spelling_of_the_same_family() {
for spelling in [
"anthropic/claude-opus-4-8",
"claude-opus-4-8",
"us.anthropic.claude-opus-4-8-v1:0",
] {
assert_eq!(
built_in(spelling).map(|p| p.input_per_mtok),
Some(crate::pricing_ref::REF_INPUT_PER_MTOK),
"{spelling}"
);
}
}
#[test]
fn haiku_is_not_swallowed_by_a_broader_family_row() {
let haiku = built_in("anthropic/claude-haiku-4-5").expect("haiku is priced");
let opus = built_in("anthropic/claude-opus-4-8").expect("opus is priced");
assert!(haiku.input_per_mtok < opus.input_per_mtok);
}
#[test]
fn an_unknown_model_has_no_built_in_price() {
assert!(built_in("someone-elses/model-1").is_none());
}
#[test]
fn an_override_needs_both_halves_and_wins_over_the_table() {
assert_eq!(
resolve("anthropic/claude-opus-4-8", Some(1.0), Some(2.0)),
Some(ModelPrice {
input_per_mtok: 1.0,
output_per_mtok: 2.0
})
);
// Half an override falls back to the table rather than billing
// completions at zero.
assert_eq!(
resolve("anthropic/claude-opus-4-8", Some(1.0), None).map(|p| p.output_per_mtok),
Some(crate::pricing_ref::REF_OUTPUT_PER_MTOK)
);
assert!(resolve("someone-elses/model-1", None, Some(2.0)).is_none());
}
#[test]
fn cost_is_the_two_rates_summed_over_a_million() {
let price = ModelPrice {
input_per_mtok: 10.0,
output_per_mtok: 100.0,
};
// 1M input + 0.1M output = $10 + $10.
assert!((price.cost_usd(1_000_000, 100_000) - 20.0).abs() < 1e-9);
assert_eq!(price.cost_usd(0, 0), 0.0);
}
}