Skip to main content

bedrock_cost/
lib.rs

1//! # bedrock-cost
2//!
3//! Calculate AWS Bedrock invocation cost across the non-Anthropic vendors
4//! that Bedrock hosts: Meta Llama, Mistral, Cohere, Amazon Titan, and AI21
5//! Jurassic. For Anthropic Claude models on Bedrock, use the companion
6//! [`claude-cost`](https://crates.io/crates/claude-cost) crate, which
7//! handles the cache_creation / cache_read fields Claude exposes.
8//!
9//! Bedrock IDs are normalized so cross-region inference profile prefixes
10//! (`us.`, `eu.`, `apac.`) and ARN paths resolve to the same base model.
11//!
12//! ## Quick example
13//!
14//! ```
15//! use bedrock_cost::{Usage, default_pricing};
16//!
17//! let pricing = default_pricing("meta.llama3-70b-instruct-v1:0").unwrap();
18//! let usage = Usage { input_tokens: 1_000_000, output_tokens: 500_000 };
19//! let cost = pricing.cost_for(&usage);
20//! assert!(cost > 0.0);
21//! ```
22//!
23//! ## Cross-region inference profile
24//!
25//! ```
26//! use bedrock_cost::default_pricing;
27//! assert_eq!(
28//!     default_pricing("us.meta.llama3-70b-instruct-v1:0"),
29//!     default_pricing("meta.llama3-70b-instruct-v1:0")
30//! );
31//! ```
32//!
33//! Pricing is best-effort and dated; verify against
34//! <https://aws.amazon.com/bedrock/pricing/> before using these numbers
35//! for billing.
36
37#![deny(missing_docs)]
38
39#[cfg(feature = "serde")]
40use serde::{Deserialize, Serialize};
41
42const INFERENCE_PROFILE_PREFIXES: &[&str] = &["us.", "eu.", "apac."];
43
44/// Token usage as returned by Bedrock Converse (`inputTokens` /
45/// `outputTokens`).
46#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
47#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
48#[cfg_attr(feature = "serde", serde(default))]
49pub struct Usage {
50    /// Tokens in the input prompt.
51    pub input_tokens: u64,
52    /// Tokens in the model output.
53    pub output_tokens: u64,
54}
55
56impl Usage {
57    /// Total tokens billed (input + output).
58    pub fn total_tokens(&self) -> u64 {
59        self.input_tokens + self.output_tokens
60    }
61}
62
63/// Per-model rates, USD per 1M tokens.
64#[derive(Debug, Clone, Copy, PartialEq)]
65pub struct Pricing {
66    /// Fresh input tokens.
67    pub input_per_mtok: f64,
68    /// Output tokens.
69    pub output_per_mtok: f64,
70}
71
72impl Pricing {
73    /// Compute USD cost for the given usage.
74    pub fn cost_for(&self, usage: &Usage) -> f64 {
75        (usage.input_tokens as f64 * self.input_per_mtok
76            + usage.output_tokens as f64 * self.output_per_mtok)
77            / 1_000_000.0
78    }
79}
80
81/// Strip ARN, inference-profile, and version suffixes from a Bedrock
82/// model id.
83///
84/// `us.meta.llama3-70b-instruct-v1:0` -> `meta.llama3-70b-instruct`
85pub fn normalize_model_id(id: &str) -> &str {
86    let mut s = id;
87
88    // ARN -> tail after final `/`
89    if s.starts_with("arn:aws:bedrock:") {
90        if let Some(slash) = s.rfind('/') {
91            s = &s[slash + 1..];
92        }
93    }
94
95    // Cross-region inference-profile prefix
96    for prefix in INFERENCE_PROFILE_PREFIXES {
97        if let Some(rest) = s.strip_prefix(prefix) {
98            s = rest;
99            break;
100        }
101    }
102
103    // Trailing `-v\d+:\d+` version suffix
104    if let Some(idx) = s.rfind("-v") {
105        let tail = &s[idx + 2..];
106        if tail
107            .splitn(2, ':')
108            .all(|part| !part.is_empty() && part.chars().all(|c| c.is_ascii_digit()))
109            && tail.contains(':')
110        {
111            s = &s[..idx];
112        }
113    }
114
115    s
116}
117
118/// Built-in pricing table. Source: aws.amazon.com/bedrock/pricing as of
119/// 2026-Q2 for the us-east-1 region. VERIFY before billing — Bedrock
120/// pricing varies by region.
121pub const DEFAULT_PRICING_TABLE: &[(&str, Pricing)] = &[
122    // Meta Llama 3 family
123    (
124        "meta.llama3-8b-instruct",
125        Pricing { input_per_mtok: 0.30, output_per_mtok: 0.60 },
126    ),
127    (
128        "meta.llama3-70b-instruct",
129        Pricing { input_per_mtok: 2.65, output_per_mtok: 3.50 },
130    ),
131    (
132        "meta.llama3-1-8b-instruct",
133        Pricing { input_per_mtok: 0.22, output_per_mtok: 0.22 },
134    ),
135    (
136        "meta.llama3-1-70b-instruct",
137        Pricing { input_per_mtok: 0.72, output_per_mtok: 0.72 },
138    ),
139    (
140        "meta.llama3-1-405b-instruct",
141        Pricing { input_per_mtok: 5.32, output_per_mtok: 16.00 },
142    ),
143    // Mistral
144    (
145        "mistral.mistral-large-2407",
146        Pricing { input_per_mtok: 2.00, output_per_mtok: 6.00 },
147    ),
148    (
149        "mistral.mistral-small-2402",
150        Pricing { input_per_mtok: 1.00, output_per_mtok: 3.00 },
151    ),
152    // Cohere Command R / R+
153    (
154        "cohere.command-r-plus",
155        Pricing { input_per_mtok: 3.00, output_per_mtok: 15.00 },
156    ),
157    (
158        "cohere.command-r",
159        Pricing { input_per_mtok: 0.50, output_per_mtok: 1.50 },
160    ),
161    // Amazon Titan Text
162    (
163        "amazon.titan-text-premier",
164        Pricing { input_per_mtok: 0.50, output_per_mtok: 1.50 },
165    ),
166    (
167        "amazon.titan-text-express",
168        Pricing { input_per_mtok: 0.20, output_per_mtok: 0.60 },
169    ),
170    (
171        "amazon.titan-text-lite",
172        Pricing { input_per_mtok: 0.15, output_per_mtok: 0.20 },
173    ),
174    // AI21 Jamba
175    (
176        "ai21.jamba-1-5-large",
177        Pricing { input_per_mtok: 2.00, output_per_mtok: 8.00 },
178    ),
179    (
180        "ai21.jamba-1-5-mini",
181        Pricing { input_per_mtok: 0.20, output_per_mtok: 0.40 },
182    ),
183];
184
185/// Look up the price table entry for a Bedrock model id.
186pub fn default_pricing(model_id: &str) -> Option<Pricing> {
187    let key = normalize_model_id(model_id);
188    DEFAULT_PRICING_TABLE
189        .iter()
190        .find(|(k, _)| *k == key)
191        .map(|(_, p)| *p)
192}