use std::collections::HashMap;
use bitrouter_sdk::language_model::Usage;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ModelPricing {
pub input_micro_usd_per_token: Option<f64>,
pub output_micro_usd_per_token: Option<f64>,
pub context_tiers: Vec<ContextTier>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct ContextTier {
pub above_input_tokens: u64,
pub input_micro_usd_per_token: Option<f64>,
pub output_micro_usd_per_token: Option<f64>,
}
impl ModelPricing {
pub fn new(input_micro_usd_per_token: f64, output_micro_usd_per_token: f64) -> Self {
Self {
input_micro_usd_per_token: Some(input_micro_usd_per_token),
output_micro_usd_per_token: Some(output_micro_usd_per_token),
context_tiers: Vec::new(),
}
}
pub fn partial(input: Option<f64>, output: Option<f64>) -> Self {
Self {
input_micro_usd_per_token: input,
output_micro_usd_per_token: output,
context_tiers: Vec::new(),
}
}
pub fn resolve_for_input_tokens(&self, input_tokens: u64) -> ModelPricing {
match self
.context_tiers
.iter()
.filter(|t| input_tokens > t.above_input_tokens)
.max_by_key(|t| t.above_input_tokens)
{
Some(tier) => ModelPricing {
input_micro_usd_per_token: tier.input_micro_usd_per_token,
output_micro_usd_per_token: tier.output_micro_usd_per_token,
context_tiers: Vec::new(),
},
None => ModelPricing {
input_micro_usd_per_token: self.input_micro_usd_per_token,
output_micro_usd_per_token: self.output_micro_usd_per_token,
context_tiers: Vec::new(),
},
}
}
pub fn is_unconfigured(&self) -> bool {
self.input_micro_usd_per_token.is_none() && self.output_micro_usd_per_token.is_none()
}
pub fn is_complete(&self) -> bool {
self.input_micro_usd_per_token.is_some() && self.output_micro_usd_per_token.is_some()
}
}
#[derive(Debug, Clone, Default)]
pub struct PricingTable {
entries: HashMap<(String, String), ModelPricing>,
}
impl PricingTable {
pub fn new() -> Self {
Self::default()
}
pub fn insert(
&mut self,
provider: impl Into<String>,
service_id: impl Into<String>,
pricing: ModelPricing,
) {
self.entries
.insert((provider.into(), service_id.into()), pricing);
}
pub fn resolve(&self, provider: &str, service_id: &str) -> Option<ModelPricing> {
self.entries
.get(&BorrowedKey(provider, service_id) as &dyn KeyLike)
.cloned()
}
}
struct BorrowedKey<'a>(&'a str, &'a str);
trait KeyLike {
fn parts(&self) -> (&str, &str);
}
impl KeyLike for (String, String) {
fn parts(&self) -> (&str, &str) {
(self.0.as_str(), self.1.as_str())
}
}
impl KeyLike for BorrowedKey<'_> {
fn parts(&self) -> (&str, &str) {
(self.0, self.1)
}
}
impl std::hash::Hash for dyn KeyLike + '_ {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
let (a, b) = self.parts();
a.hash(state);
b.hash(state);
}
}
impl PartialEq for dyn KeyLike + '_ {
fn eq(&self, other: &Self) -> bool {
self.parts() == other.parts()
}
}
impl Eq for dyn KeyLike + '_ {}
impl<'a> std::borrow::Borrow<dyn KeyLike + 'a> for (String, String) {
fn borrow(&self) -> &(dyn KeyLike + 'a) {
self
}
}
pub const MAX_TRUSTED_TOKENS: u64 = 10_000_000;
pub fn calculate_charge_micro_usd(usage: &Usage, pricing: &ModelPricing) -> Option<i64> {
let pricing = pricing.resolve_for_input_tokens(usage.prompt_tokens);
let input = bucket_charge(usage.prompt_tokens, pricing.input_micro_usd_per_token)?;
let output = bucket_charge(usage.completion_tokens, pricing.output_micro_usd_per_token)?;
Some((input + output).round().max(0.0) as i64)
}
fn bucket_charge(tokens: u64, rate: Option<f64>) -> Option<f64> {
let tokens = tokens.min(MAX_TRUSTED_TOKENS);
match (tokens, rate) {
(0, _) => Some(0.0),
(n, Some(r)) => Some(n as f64 * r),
(_, None) => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_pricing_reads_as_unconfigured() {
assert!(ModelPricing::default().is_unconfigured());
assert!(!ModelPricing::new(1.0, 2.0).is_unconfigured());
}
#[test]
fn charge_is_input_plus_output() {
let pricing = ModelPricing::new(2.0, 10.0);
let usage = Usage {
prompt_tokens: 100,
completion_tokens: 50,
reasoning_tokens: 0,
..Default::default()
};
assert_eq!(calculate_charge_micro_usd(&usage, &pricing), Some(700));
}
#[test]
fn partial_pricing_with_nonzero_output_returns_none() {
let pricing = ModelPricing::partial(Some(2.0), None);
let usage = Usage {
prompt_tokens: 100,
completion_tokens: 50, ..Default::default()
};
assert_eq!(
calculate_charge_micro_usd(&usage, &pricing),
None,
"missing rate × nonzero usage MUST refuse to bill"
);
}
#[test]
fn zero_usage_bucket_with_missing_rate_is_zero() {
let pricing = ModelPricing::partial(Some(2.0), None);
let usage = Usage {
prompt_tokens: 100,
completion_tokens: 0,
..Default::default()
};
assert_eq!(calculate_charge_micro_usd(&usage, &pricing), Some(200));
}
#[test]
fn adversarial_token_count_is_clamped() {
let pricing = ModelPricing::new(1.0, 1.0);
let usage = Usage {
prompt_tokens: u64::MAX,
completion_tokens: 0,
..Default::default()
};
let charge = calculate_charge_micro_usd(&usage, &pricing).unwrap();
assert_eq!(charge, MAX_TRUSTED_TOKENS as i64);
}
#[test]
fn is_complete_predicate() {
assert!(!ModelPricing::default().is_complete());
assert!(!ModelPricing::partial(Some(1.0), None).is_complete());
assert!(!ModelPricing::partial(None, Some(1.0)).is_complete());
assert!(ModelPricing::new(1.0, 1.0).is_complete());
}
#[test]
fn table_resolve_keys_by_provider_and_service_id() {
let mut table = PricingTable::new();
table.insert("openai", "gpt-5-2026-01", ModelPricing::new(1.0, 4.0));
assert!(table.resolve("openai", "gpt-5-2026-01").is_some());
assert!(table.resolve("openai", "gpt-5").is_none());
assert!(table.resolve("anthropic", "gpt-5-2026-01").is_none());
}
fn tiered() -> ModelPricing {
ModelPricing {
input_micro_usd_per_token: Some(1.3),
output_micro_usd_per_token: Some(7.8),
context_tiers: vec![ContextTier {
above_input_tokens: 128_000,
input_micro_usd_per_token: Some(2.0),
output_micro_usd_per_token: Some(12.0),
}],
}
}
#[test]
fn resolve_picks_base_at_or_below_threshold() {
let p = tiered();
assert_eq!(
p.resolve_for_input_tokens(0).input_micro_usd_per_token,
Some(1.3)
);
assert_eq!(
p.resolve_for_input_tokens(128_000)
.input_micro_usd_per_token,
Some(1.3)
);
}
#[test]
fn resolve_picks_tier_above_threshold() {
let p = tiered();
let hi = p.resolve_for_input_tokens(128_001);
assert_eq!(hi.input_micro_usd_per_token, Some(2.0));
assert_eq!(hi.output_micro_usd_per_token, Some(12.0));
assert!(hi.context_tiers.is_empty(), "resolved bracket is flat");
}
#[test]
fn resolve_highest_applicable_tier_is_order_independent() {
let p = ModelPricing {
input_micro_usd_per_token: Some(1.0),
output_micro_usd_per_token: Some(2.0),
context_tiers: vec![
ContextTier {
above_input_tokens: 256_000,
input_micro_usd_per_token: Some(4.0),
output_micro_usd_per_token: Some(8.0),
},
ContextTier {
above_input_tokens: 128_000,
input_micro_usd_per_token: Some(2.0),
output_micro_usd_per_token: Some(4.0),
},
],
};
assert_eq!(
p.resolve_for_input_tokens(1_000).input_micro_usd_per_token,
Some(1.0)
);
assert_eq!(
p.resolve_for_input_tokens(200_000)
.input_micro_usd_per_token,
Some(2.0)
);
assert_eq!(
p.resolve_for_input_tokens(300_000)
.input_micro_usd_per_token,
Some(4.0)
);
}
#[test]
fn charge_uses_the_bracket_selected_by_prompt_size() {
let pricing = tiered();
let lo = Usage {
prompt_tokens: 100_000,
completion_tokens: 1_000,
..Default::default()
};
assert_eq!(calculate_charge_micro_usd(&lo, &pricing), Some(137_800));
let hi = Usage {
prompt_tokens: 200_000,
completion_tokens: 1_000,
..Default::default()
};
assert_eq!(calculate_charge_micro_usd(&hi, &pricing), Some(412_000));
}
#[test]
fn flat_pricing_charges_identically_at_every_size() {
let pricing = ModelPricing::new(2.0, 10.0);
for n in [10_u64, 200_000] {
let usage = Usage {
prompt_tokens: n,
completion_tokens: 0,
..Default::default()
};
assert_eq!(
calculate_charge_micro_usd(&usage, &pricing),
Some((n as f64 * 2.0) as i64)
);
}
}
#[test]
fn tier_inherits_partial_skip_when_bucket_rate_missing() {
let pricing = ModelPricing {
input_micro_usd_per_token: Some(1.3),
output_micro_usd_per_token: Some(7.8),
context_tiers: vec![ContextTier {
above_input_tokens: 128_000,
input_micro_usd_per_token: Some(2.0),
output_micro_usd_per_token: None,
}],
};
let hi = Usage {
prompt_tokens: 200_000,
completion_tokens: 1_000, ..Default::default()
};
assert_eq!(calculate_charge_micro_usd(&hi, &pricing), None);
}
}