Skip to main content

supercode_harness/
pricing.rs

1//! BP-7 (catalog §4a "Turn/budget caps" — the *spend* cap; "Per-turn
2//! cost/usage accounting" — the *cost* half): token→dollars on the request
3//! path.
4//!
5//! Until BP-7 the only pricing in the workspace was
6//! [`crate::pricing_ref`], whose own module doc says it is "not used
7//! anywhere on the request path" — which is exactly why the ledger's
8//! `turn-budget-caps` row read "no spend/budget cap at all — no pricing
9//! exists on the request path". This module is that missing piece, and
10//! nothing more: a per-million-token price pair for a model id, resolved
11//! from (1) the config's explicit override
12//! ([`crate::Config::price_input_per_mtok`] /
13//! [`crate::Config::price_output_per_mtok`]) or (2) a small built-in table
14//! of published list prices for the model families the parity presets pin.
15//!
16//! **Deliberately fails closed.** [`resolve`] returns `None` for a model it
17//! cannot price rather than guessing, and [`crate::Agent::new`] refuses to
18//! build an agent that arms `core.max_budget_usd` against an unpriceable
19//! model. A spend cap that silently never bites is worse than no cap: the
20//! caller believes they are protected.
21
22/// Per-million-token prices for one model, in US dollars.
23#[derive(Debug, Clone, Copy, PartialEq)]
24pub struct ModelPrice {
25    /// Input (prompt) tokens, dollars per million.
26    pub input_per_mtok: f64,
27    /// Output (completion) tokens, dollars per million.
28    pub output_per_mtok: f64,
29}
30
31impl ModelPrice {
32    /// Dollar cost of one round-trip's token counts.
33    ///
34    /// Cached prompt tokens are billed at the full input rate here: the
35    /// provider-reported discount varies per provider and per cache tier,
36    /// and over-reporting cost is the safe direction for a *cap* (a budget
37    /// that stops slightly early never overspends). Named rather than
38    /// silently assumed — see the `per-turn-cost-usage-accounting` ledger
39    /// row's note.
40    pub fn cost_usd(&self, prompt_tokens: u64, completion_tokens: u64) -> f64 {
41        (prompt_tokens as f64 / 1_000_000.0) * self.input_per_mtok
42            + (completion_tokens as f64 / 1_000_000.0) * self.output_per_mtok
43    }
44}
45
46/// Published list prices for the model families the parity presets pin,
47/// matched by SUBSTRING on the model id so the provider-prefixed spellings
48/// (`anthropic/claude-opus-4-8`, `claude-opus-4-8`,
49/// `us.anthropic.claude-opus-4-8-v1:0`) all resolve to the same row.
50///
51/// Longest pattern first — `claude-haiku` must win over a shorter prefix
52/// that would also match.
53const BUILT_IN_PRICES: &[(&str, ModelPrice)] = &[
54    (
55        "claude-opus",
56        ModelPrice {
57            input_per_mtok: crate::pricing_ref::REF_INPUT_PER_MTOK,
58            output_per_mtok: crate::pricing_ref::REF_OUTPUT_PER_MTOK,
59        },
60    ),
61    (
62        "claude-sonnet",
63        ModelPrice {
64            input_per_mtok: 3.00,
65            output_per_mtok: 15.00,
66        },
67    ),
68    (
69        "claude-haiku",
70        ModelPrice {
71            input_per_mtok: 1.00,
72            output_per_mtok: 5.00,
73        },
74    ),
75];
76
77/// The built-in list price for `model`, if this build knows one.
78pub fn built_in(model: &str) -> Option<ModelPrice> {
79    BUILT_IN_PRICES
80        .iter()
81        .find(|(pattern, _)| model.contains(pattern))
82        .map(|(_, price)| *price)
83}
84
85/// Resolve the price to bill `model` at: the explicit config override when
86/// BOTH halves are set, else the built-in table, else `None`.
87///
88/// Both override halves are required together on purpose — an input price
89/// with no output price would silently bill completions at zero.
90pub fn resolve(
91    model: &str,
92    price_input_per_mtok: Option<f64>,
93    price_output_per_mtok: Option<f64>,
94) -> Option<ModelPrice> {
95    match (price_input_per_mtok, price_output_per_mtok) {
96        (Some(input_per_mtok), Some(output_per_mtok)) => Some(ModelPrice {
97            input_per_mtok,
98            output_per_mtok,
99        }),
100        _ => built_in(model),
101    }
102}