Skip to main content

agent_top_core/
pricing.rs

1//! Static price table, USD per million tokens.
2//!
3//! Anthropic prices cached 2026-06-24 from the published price list. Cache
4//! writes are 1.25x input for the 5-minute TTL and 2x for the 1-hour TTL on
5//! every model; cache reads are 0.1x input except Claude Fable 5.1 (0.025x).
6//! OpenAI, Google and other vendors are not priced here: their tokens are
7//! counted but reported as "unpriced" until a user-supplied table exists
8//! (see RFC-103 in the internal handbook).
9
10use crate::model::TokenUsage;
11
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct Price {
14    pub input: f64,
15    pub output: f64,
16    pub cache_write_5m: f64,
17    pub cache_write_1h: f64,
18    pub cache_read: f64,
19}
20
21impl Price {
22    const fn anthropic(input: f64, output: f64, cache_read: f64) -> Price {
23        Price { input, output, cache_write_5m: input * 1.25, cache_write_1h: input * 2.0, cache_read }
24    }
25
26    /// Cost in USD of `usage` at this price.
27    pub fn cost(&self, usage: &TokenUsage) -> f64 {
28        const M: f64 = 1_000_000.0;
29        usage.input as f64 * self.input / M
30            + usage.cache_write_5m as f64 * self.cache_write_5m / M
31            + usage.cache_write_1h as f64 * self.cache_write_1h / M
32            + usage.cache_read as f64 * self.cache_read / M
33            + usage.output as f64 * self.output / M
34    }
35}
36
37/// Longest-prefix table. Order matters: `claude-fable-5-1` must precede `claude-fable-5`.
38const TABLE: &[(&str, Price)] = &[
39    ("claude-fable-5-1", Price::anthropic(10.0, 50.0, 0.25)),
40    ("claude-mythos-5-1", Price::anthropic(10.0, 50.0, 0.25)),
41    ("claude-fable-5", Price::anthropic(10.0, 50.0, 1.0)),
42    ("claude-mythos-5", Price::anthropic(10.0, 50.0, 1.0)),
43    ("claude-opus-5", Price::anthropic(5.0, 25.0, 0.5)),
44    ("claude-opus-4-8", Price::anthropic(5.0, 25.0, 0.5)),
45    ("claude-opus-4-7", Price::anthropic(5.0, 25.0, 0.5)),
46    ("claude-opus-4-6", Price::anthropic(5.0, 25.0, 0.5)),
47    ("claude-sonnet-5", Price::anthropic(2.0, 10.0, 0.2)),
48    ("claude-sonnet-4-6", Price::anthropic(3.0, 15.0, 0.3)),
49    ("claude-haiku-4-5", Price::anthropic(1.0, 5.0, 0.1)),
50];
51
52/// Look up a price by model id. Date-suffixed ids (`claude-sonnet-4-6-20251114`)
53/// and vendor-prefixed ids (`anthropic.claude-opus-5`) resolve to the base model.
54pub fn price_for(model: &str) -> Option<Price> {
55    let m = model.trim().to_ascii_lowercase();
56    let m = m.strip_prefix("anthropic.").unwrap_or(&m);
57    let m = m.strip_prefix("us.anthropic.").unwrap_or(m);
58    TABLE.iter().filter(|(prefix, _)| m.starts_with(prefix)).max_by_key(|(prefix, _)| prefix.len()).map(|(_, p)| *p)
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn longest_prefix_wins() {
67        assert_eq!(price_for("claude-fable-5-1").unwrap().cache_read, 0.25);
68        assert_eq!(price_for("claude-fable-5").unwrap().cache_read, 1.0);
69        assert_eq!(price_for("claude-sonnet-4-6-20251114").unwrap().input, 3.0);
70        assert!(price_for("gpt-5-codex").is_none());
71        assert!(price_for("<synthetic>").is_none());
72    }
73
74    #[test]
75    fn cost_arithmetic() {
76        let p = price_for("claude-sonnet-5").unwrap();
77        let u = TokenUsage { input: 1_000_000, output: 1_000_000, ..Default::default() };
78        assert!((p.cost(&u) - 12.0).abs() < 1e-9);
79        let u = TokenUsage { cache_write_1h: 1_000_000, ..Default::default() };
80        assert!((p.cost(&u) - 4.0).abs() < 1e-9);
81    }
82}