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
//! 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),
}
}