feat(rust): add standalone cost calculator (#42604)

* feat: add standalone Rust text pricing crate

* feat(rust): harden standalone cost calculator

---------

Co-authored-by: Yujong Lee <yujong@berri.ai>
This commit is contained in:
devin-ai-integration[bot] 2026-09-22 22:50:28 +00:00 committed by GitHub
parent 58a05a9eae
commit 7177d3b6d1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 1109 additions and 0 deletions

View file

@ -2956,6 +2956,14 @@ dependencies = [
"url",
]
[[package]]
name = "litellm-cost"
version = "0.1.0"
dependencies = [
"criterion",
"proptest",
]
[[package]]
name = "litellm-framing"
version = "0.1.0"

View file

@ -0,0 +1,14 @@
[package]
name = "litellm-cost"
version = "0.1.0"
edition.workspace = true
license.workspace = true
repository.workspace = true
[dev-dependencies]
criterion.workspace = true
proptest.workspace = true
[[bench]]
name = "calculate"
harness = false

View file

@ -0,0 +1,13 @@
# litellm-cost
This crate calculates text token charges from rates and usage supplied by its caller. It is standalone and has no Python bridge or proxy integration
Call `compile(&pricing)` once for an immutable plan, then `plan.calculate(&request)` for each supported request. `calculate(&pricing, &request)` compiles on each call. A successful result exposes pre-multiplier component costs, selected rates, the multiplier, and derived `input()`, `output()`, and `total()` values
The caller states whether `prompt_tokens` includes cache tokens. Threshold selection uses total input tokens for either convention and selects one rate for the whole request. Thresholds are sorted when compiled, and duplicate thresholds or tier overrides fail deterministically. `Fast` selects priority rates; unknown tiers use standard rates
`Rate::Missing`, `Rate::Null`, and `Rate::Value(0.0)` remain distinct. Missing cache rates fall back to the selected input rate, and an absent one-hour write rate falls back to the selected write rate. Missing input or output rates return typed errors, including for zero usage. Python's sparse-entry behavior remains outside this native contract
The supported off-peak shape is one non-wrapping UTC daily window. The caller supplies the applicable regional multiplier after provider-specific selection. Negative or non-finite rates, ambiguous rules, inconsistent cache counts, incomplete write splits, invalid windows and overflow return errors. Callers must decline unsupported inputs before native execution if their public contract accepts those shapes
This crate does not select models, read catalogs, fetch provider prices, normalize multimodal usage, process provider-reported costs, or calculate non-token charges. It does not change proxy behavior. The reference fixture was generated by `tests/generate_python_reference.py` against the Python implementation at the commit recorded in `tests/python_reference.tsv`, using synthetic rates and fixed usage

View file

@ -0,0 +1,66 @@
use criterion::{Criterion, criterion_group, criterion_main};
use litellm_cost::{
Pricing, PromptConvention, Rate, Rates, Request, ServiceTier, ThresholdPolicy, ThresholdRates,
Usage, calculate, compile,
};
use std::hint::black_box;
fn bench(c: &mut Criterion) {
let pricing = Pricing {
standard: Rates {
input: Rate::Value(0.000002),
output: Rate::Value(0.000008),
cache_read: Rate::Value(0.0000005),
cache_write: Rate::Missing,
cache_write_1h: Rate::Missing,
},
tiers: &[],
thresholds: &[],
off_peak: None,
};
let request = Request {
usage: Usage {
prompt_tokens: 1000,
completion_tokens: 200,
cache_read_tokens: 250,
cache_write_tokens: 0,
cache_write_5m_tokens: None,
cache_write_1h_tokens: None,
prompt_convention: PromptConvention::IncludesCache,
},
service_tier: ServiceTier::Standard,
threshold_policy: ThresholdPolicy::Exclusive,
region_multiplier: None,
billed_at_utc_minute: None,
};
let plan = compile(&pricing).unwrap();
c.bench_function("native_compiled_calculation", |b| {
b.iter(|| black_box(plan.calculate(black_box(&request)).unwrap()))
});
c.bench_function("native_full_wrapper", |b| {
b.iter(|| black_box(calculate(black_box(&pricing), black_box(&request)).unwrap()))
});
c.bench_function("native_rate_compilation", |b| {
b.iter(|| black_box(compile(black_box(&pricing)).unwrap()))
});
let threshold = ThresholdRates {
above_prompt_tokens: 1000,
standard: Rates {
input: Rate::Value(0.000004),
output: Rate::Value(0.000016),
..Rates::EMPTY
},
tiers: &[],
};
let threshold_pricing = Pricing {
thresholds: &[threshold],
..pricing
};
let threshold_plan = compile(&threshold_pricing).unwrap();
c.bench_function("native_threshold_boundary", |b| {
b.iter(|| black_box(threshold_plan.calculate(black_box(&request)).unwrap()))
});
}
criterion_group!(benches, bench);
criterion_main!(benches);

View file

@ -0,0 +1,40 @@
use litellm_cost::{
Pricing, PromptConvention, Rate, Rates, Request, ServiceTier, ThresholdPolicy, Usage, compile,
};
fn main() {
let pricing = Pricing {
standard: Rates {
input: Rate::Value(2.0),
output: Rate::Value(4.0),
cache_read: Rate::Value(0.5),
cache_write: Rate::Value(3.0),
cache_write_1h: Rate::Missing,
},
tiers: &[],
thresholds: &[],
off_peak: None,
};
let request = Request {
usage: Usage {
prompt_tokens: 100,
completion_tokens: 20,
cache_read_tokens: 25,
cache_write_tokens: 10,
cache_write_5m_tokens: None,
cache_write_1h_tokens: None,
prompt_convention: PromptConvention::IncludesCache,
},
service_tier: ServiceTier::Standard,
threshold_policy: ThresholdPolicy::Exclusive,
region_multiplier: None,
billed_at_utc_minute: None,
};
let cost = compile(&pricing).unwrap().calculate(&request).unwrap();
println!(
"input={} output={} total={}",
cost.input(),
cost.output(),
cost.total()
);
}

View file

@ -0,0 +1,405 @@
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Rate {
Missing,
Null,
Value(f64),
}
impl Rate {
fn value(self) -> Option<f64> {
match self {
Self::Value(value) => Some(value),
Self::Missing | Self::Null => None,
}
}
fn or(self, fallback: Self) -> Self {
if self.value().is_some() {
self
} else {
fallback
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Rates {
pub input: Rate,
pub output: Rate,
pub cache_read: Rate,
pub cache_write: Rate,
pub cache_write_1h: Rate,
}
impl Rates {
pub const EMPTY: Self = Self {
input: Rate::Missing,
output: Rate::Missing,
cache_read: Rate::Missing,
cache_write: Rate::Missing,
cache_write_1h: Rate::Missing,
};
fn overlay(self, base: Self) -> Self {
Self {
input: self.input.or(base.input),
output: self.output.or(base.output),
cache_read: self.cache_read.or(base.cache_read),
cache_write: self.cache_write.or(base.cache_write),
cache_write_1h: self.cache_write_1h.or(base.cache_write_1h),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ServiceTier {
Standard,
Flex,
Priority,
Fast,
Ultrafast,
Unknown,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ThresholdPolicy {
Exclusive,
Inclusive,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PromptConvention {
IncludesCache,
ExcludesCache,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Usage {
pub prompt_tokens: u64,
pub completion_tokens: u64,
pub cache_read_tokens: u64,
pub cache_write_tokens: u64,
pub cache_write_5m_tokens: Option<u64>,
pub cache_write_1h_tokens: Option<u64>,
pub prompt_convention: PromptConvention,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TierRates {
pub tier: ServiceTier,
pub rates: Rates,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ThresholdRates<'a> {
pub above_prompt_tokens: u64,
pub standard: Rates,
pub tiers: &'a [TierRates],
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct OffPeakRates {
pub start_utc_minute: u16,
pub end_utc_minute: u16,
pub rates: Rates,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Pricing<'a> {
pub standard: Rates,
pub tiers: &'a [TierRates],
pub thresholds: &'a [ThresholdRates<'a>],
pub off_peak: Option<OffPeakRates>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Request {
pub usage: Usage,
pub service_tier: ServiceTier,
pub threshold_policy: ThresholdPolicy,
pub region_multiplier: Option<f64>,
pub billed_at_utc_minute: Option<u16>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Cost {
pub uncached_input: f64,
pub cache_read: f64,
pub cache_write_5m: f64,
pub cache_write_1h: f64,
pub output: f64,
pub multiplier: f64,
pub rates: EffectiveRates,
}
impl Cost {
pub fn input(self) -> f64 {
(self.uncached_input + self.cache_read + self.cache_write_5m + self.cache_write_1h)
* self.multiplier
}
pub fn output(self) -> f64 {
self.output * self.multiplier
}
pub fn total(self) -> f64 {
self.input() + self.output()
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct EffectiveRates {
pub input: f64,
pub output: f64,
pub cache_read: f64,
pub cache_write_5m: f64,
pub cache_write_1h: f64,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PricingError {
MissingInputRate,
MissingOutputRate,
InvalidRate,
InvalidRegionMultiplier,
InvalidBillingTime,
InvalidOffPeakWindow,
CacheExceedsPrompt,
InvalidCacheWriteDetails,
TokenCountOverflow,
DuplicateTier,
DuplicateThreshold,
DuplicateThresholdTier,
}
fn selected_tier(tier: ServiceTier) -> ServiceTier {
if tier == ServiceTier::Fast {
ServiceTier::Priority
} else {
tier
}
}
#[derive(Clone, Debug)]
struct CompiledThreshold {
above_prompt_tokens: u64,
standard: Rates,
tiers: Vec<TierRates>,
}
#[derive(Clone, Debug)]
pub struct PricingPlan {
standard: Rates,
tiers: Vec<TierRates>,
thresholds: Vec<CompiledThreshold>,
off_peak: Option<OffPeakRates>,
}
fn valid_rates(rates: Rates) -> bool {
[
rates.input,
rates.output,
rates.cache_read,
rates.cache_write,
rates.cache_write_1h,
]
.into_iter()
.all(|rate| {
rate.value()
.is_none_or(|value| value.is_finite() && value >= 0.0)
})
}
fn validate_tiers(tiers: &[TierRates], duplicate: PricingError) -> Result<(), PricingError> {
if tiers.iter().any(|entry| !valid_rates(entry.rates)) {
return Err(PricingError::InvalidRate);
}
if tiers.iter().enumerate().any(|(index, entry)| {
matches!(
entry.tier,
ServiceTier::Standard | ServiceTier::Unknown | ServiceTier::Fast
) || tiers[..index]
.iter()
.any(|previous| previous.tier == entry.tier)
}) {
return Err(duplicate);
}
Ok(())
}
pub fn compile(pricing: &Pricing<'_>) -> Result<PricingPlan, PricingError> {
if !valid_rates(pricing.standard) {
return Err(PricingError::InvalidRate);
}
validate_tiers(pricing.tiers, PricingError::DuplicateTier)?;
if let Some(window) = pricing.off_peak {
if window.start_utc_minute >= 1440
|| window.end_utc_minute > 1440
|| window.start_utc_minute >= window.end_utc_minute
{
return Err(PricingError::InvalidOffPeakWindow);
}
if !valid_rates(window.rates) {
return Err(PricingError::InvalidRate);
}
}
let mut thresholds: Vec<_> = pricing
.thresholds
.iter()
.map(|entry| {
if !valid_rates(entry.standard) {
return Err(PricingError::InvalidRate);
}
validate_tiers(entry.tiers, PricingError::DuplicateThresholdTier)?;
Ok(CompiledThreshold {
above_prompt_tokens: entry.above_prompt_tokens,
standard: entry.standard,
tiers: entry.tiers.to_vec(),
})
})
.collect::<Result<_, _>>()?;
thresholds.sort_unstable_by_key(|entry| entry.above_prompt_tokens);
if thresholds
.windows(2)
.any(|pair| pair[0].above_prompt_tokens == pair[1].above_prompt_tokens)
{
return Err(PricingError::DuplicateThreshold);
}
Ok(PricingPlan {
standard: pricing.standard,
tiers: pricing.tiers.to_vec(),
thresholds,
off_peak: pricing.off_peak,
})
}
impl PricingPlan {
fn resolve_rates(
&self,
request: &Request,
threshold_tokens: u64,
) -> Result<Rates, PricingError> {
let tier = selected_tier(request.service_tier);
let base = self
.tiers
.iter()
.find(|entry| tier != ServiceTier::Standard && entry.tier == tier)
.map_or(self.standard, |entry| entry.rates.overlay(self.standard));
let threshold = self.thresholds.iter().rev().find(|entry| {
threshold_tokens > entry.above_prompt_tokens
|| (request.threshold_policy == ThresholdPolicy::Inclusive
&& threshold_tokens == entry.above_prompt_tokens)
});
let selected = threshold.map_or(base, |entry| {
let standard = entry.standard.overlay(base);
entry
.tiers
.iter()
.find(|specific| tier != ServiceTier::Standard && specific.tier == tier)
.map_or(standard, |specific| specific.rates.overlay(standard))
});
match self.off_peak {
None => Ok(selected),
Some(window) => {
if window.start_utc_minute >= 1440
|| window.end_utc_minute > 1440
|| window.start_utc_minute >= window.end_utc_minute
{
return Err(PricingError::InvalidOffPeakWindow);
}
let minute = request
.billed_at_utc_minute
.ok_or(PricingError::InvalidBillingTime)?;
if minute >= 1440 {
return Err(PricingError::InvalidBillingTime);
}
if (window.start_utc_minute..window.end_utc_minute).contains(&minute) {
Ok(window.rates.overlay(selected))
} else {
Ok(selected)
}
}
}
}
fn checked_rate(rate: Rate, missing: PricingError) -> Result<f64, PricingError> {
let value = rate.value().ok_or(missing)?;
if !value.is_finite() || value < 0.0 {
return Err(PricingError::InvalidRate);
}
Ok(value)
}
pub fn calculate(&self, request: &Request) -> Result<Cost, PricingError> {
let usage = request.usage;
let cached = usage
.cache_read_tokens
.checked_add(usage.cache_write_tokens)
.ok_or(PricingError::TokenCountOverflow)?;
let (regular, threshold_tokens) = match usage.prompt_convention {
PromptConvention::IncludesCache => (
usage
.prompt_tokens
.checked_sub(cached)
.ok_or(PricingError::CacheExceedsPrompt)?,
usage.prompt_tokens,
),
PromptConvention::ExcludesCache => (
usage.prompt_tokens,
usage
.prompt_tokens
.checked_add(cached)
.ok_or(PricingError::TokenCountOverflow)?,
),
};
let writes = match (usage.cache_write_5m_tokens, usage.cache_write_1h_tokens) {
(None, None) => (usage.cache_write_tokens, 0),
(Some(five), Some(one)) if five.checked_add(one) == Some(usage.cache_write_tokens) => {
(five, one)
}
_ => return Err(PricingError::InvalidCacheWriteDetails),
};
let rates = self.resolve_rates(request, threshold_tokens)?;
let input = Self::checked_rate(rates.input, PricingError::MissingInputRate)?;
let output = Self::checked_rate(rates.output, PricingError::MissingOutputRate)?;
let read = Self::checked_rate(
rates.cache_read.or(rates.input),
PricingError::MissingInputRate,
)?;
let write = Self::checked_rate(
rates.cache_write.or(rates.input),
PricingError::MissingInputRate,
)?;
let write_1h = Self::checked_rate(
rates.cache_write_1h.or(rates.cache_write).or(rates.input),
PricingError::MissingInputRate,
)?;
let multiplier = request.region_multiplier.unwrap_or(1.0);
if !multiplier.is_finite() || multiplier <= 0.0 {
return Err(PricingError::InvalidRegionMultiplier);
}
let cost = Cost {
uncached_input: regular as f64 * input,
cache_read: usage.cache_read_tokens as f64 * read,
cache_write_5m: writes.0 as f64 * write,
cache_write_1h: writes.1 as f64 * write_1h,
output: usage.completion_tokens as f64 * output,
multiplier,
rates: EffectiveRates {
input,
output,
cache_read: read,
cache_write_5m: write,
cache_write_1h: write_1h,
},
};
if !cost.total().is_finite() {
return Err(PricingError::TokenCountOverflow);
}
Ok(cost)
}
}
pub fn calculate(pricing: &Pricing<'_>, request: &Request) -> Result<Cost, PricingError> {
compile(pricing)?.calculate(request)
}

View file

@ -0,0 +1,458 @@
use litellm_cost::{
OffPeakRates, Pricing, PricingError, PromptConvention, Rate, Rates, Request, ServiceTier,
ThresholdPolicy, ThresholdRates, TierRates, Usage, calculate, compile,
};
fn rates(input: Rate, output: Rate) -> Rates {
Rates {
input,
output,
..Rates::EMPTY
}
}
fn request() -> Request {
Request {
usage: Usage {
prompt_tokens: 100,
completion_tokens: 20,
cache_read_tokens: 25,
cache_write_tokens: 10,
cache_write_5m_tokens: None,
cache_write_1h_tokens: None,
prompt_convention: PromptConvention::IncludesCache,
},
service_tier: ServiceTier::Standard,
threshold_policy: ThresholdPolicy::Exclusive,
region_multiplier: None,
billed_at_utc_minute: None,
}
}
fn pricing(standard: Rates) -> Pricing<'static> {
Pricing {
standard,
tiers: &[],
thresholds: &[],
off_peak: None,
}
}
#[test]
fn breakdown_and_total_agree() {
let standard = Rates {
cache_read: Rate::Value(0.5),
cache_write: Rate::Value(3.0),
..rates(Rate::Value(2.0), Rate::Value(4.0))
};
let result = calculate(&pricing(standard), &request()).unwrap();
assert_eq!(result.uncached_input, 65.0 * 2.0);
assert_eq!(result.cache_read, 25.0 * 0.5);
assert_eq!(result.cache_write_5m, 10.0 * 3.0);
assert_eq!(result.output(), 20.0 * 4.0);
assert_eq!(result.total(), result.input() + result.output());
assert_eq!(result.rates.cache_read, 0.5);
}
#[test]
fn absent_null_and_zero_cache_rates_are_distinct() {
let base = rates(Rate::Value(2.0), Rate::Value(4.0));
for read in [Rate::Missing, Rate::Null] {
let standard = Rates {
cache_read: read,
..base
};
assert_eq!(
calculate(&pricing(standard), &request()).unwrap().input(),
200.0
);
}
let standard = Rates {
cache_read: Rate::Value(0.0),
cache_write: Rate::Value(0.0),
..base
};
assert_eq!(
calculate(&pricing(standard), &request()).unwrap().input(),
130.0
);
}
#[test]
fn equivalent_prompt_conventions_select_the_same_threshold() {
let threshold = ThresholdRates {
above_prompt_tokens: 90,
standard: rates(Rate::Value(5.0), Rate::Value(8.0)),
tiers: &[],
};
let specification = Pricing {
standard: rates(Rate::Value(2.0), Rate::Value(4.0)),
tiers: &[],
thresholds: &[threshold],
off_peak: None,
};
let included = request();
let excluded = Request {
usage: Usage {
prompt_tokens: 65,
prompt_convention: PromptConvention::ExcludesCache,
..included.usage
},
..included
};
let plan = compile(&specification).unwrap();
assert_eq!(plan.calculate(&included), plan.calculate(&excluded));
assert_eq!(plan.calculate(&included).unwrap().rates.input, 5.0);
}
#[test]
fn split_writes_and_invalid_accounting() {
let standard = Rates {
cache_read: Rate::Value(0.5),
cache_write: Rate::Value(3.0),
cache_write_1h: Rate::Value(5.0),
..rates(Rate::Value(2.0), Rate::Value(4.0))
};
let base = request();
let split = Request {
usage: Usage {
cache_write_5m_tokens: Some(4),
cache_write_1h_tokens: Some(6),
..base.usage
},
..base
};
let result = calculate(&pricing(standard), &split).unwrap();
assert_eq!(result.cache_write_5m, 12.0);
assert_eq!(result.cache_write_1h, 30.0);
let overlapping = Request {
usage: Usage {
prompt_tokens: 30,
..split.usage
},
..split
};
assert_eq!(
calculate(&pricing(standard), &overlapping),
Err(PricingError::CacheExceedsPrompt)
);
let incomplete = Request {
usage: Usage {
cache_write_1h_tokens: None,
..split.usage
},
..split
};
assert_eq!(
calculate(&pricing(standard), &incomplete),
Err(PricingError::InvalidCacheWriteDetails)
);
}
#[test]
fn threshold_tiers_and_boundaries() {
let priority = TierRates {
tier: ServiceTier::Priority,
rates: rates(Rate::Value(3.0), Rate::Missing),
};
let threshold = ThresholdRates {
above_prompt_tokens: 100,
standard: rates(Rate::Value(5.0), Rate::Value(8.0)),
tiers: &[
TierRates {
tier: ServiceTier::Priority,
rates: rates(Rate::Value(7.0), Rate::Missing),
},
TierRates {
tier: ServiceTier::Flex,
rates: rates(Rate::Value(6.0), Rate::Missing),
},
],
};
let specification = Pricing {
standard: rates(Rate::Value(2.0), Rate::Value(4.0)),
tiers: &[priority],
thresholds: &[threshold],
off_peak: None,
};
let base = request();
let no_cache = Request {
usage: Usage {
cache_read_tokens: 0,
cache_write_tokens: 0,
..base.usage
},
..base
};
let fast = Request {
service_tier: ServiceTier::Fast,
..no_cache
};
let inclusive = Request {
threshold_policy: ThresholdPolicy::Inclusive,
..fast
};
let flex = Request {
service_tier: ServiceTier::Flex,
..inclusive
};
assert_eq!(calculate(&specification, &no_cache).unwrap().input(), 200.0);
assert_eq!(calculate(&specification, &fast).unwrap().input(), 300.0);
assert_eq!(
calculate(&specification, &inclusive).unwrap().input(),
700.0
);
assert_eq!(calculate(&specification, &flex).unwrap().input(), 600.0);
}
#[test]
fn compile_rejects_ambiguous_rates() {
let duplicate = ThresholdRates {
above_prompt_tokens: 100,
standard: Rates::EMPTY,
tiers: &[],
};
let specification = Pricing {
standard: rates(Rate::Value(1.0), Rate::Value(1.0)),
tiers: &[],
thresholds: &[duplicate, duplicate],
off_peak: None,
};
assert_eq!(
compile(&specification).err(),
Some(PricingError::DuplicateThreshold)
);
let invalid = pricing(rates(Rate::Value(f64::NAN), Rate::Value(1.0)));
assert_eq!(compile(&invalid).err(), Some(PricingError::InvalidRate));
}
#[test]
fn off_peak_is_one_non_wrapping_utc_window() {
let specification = Pricing {
standard: rates(Rate::Value(2.0), Rate::Value(4.0)),
tiers: &[],
thresholds: &[],
off_peak: Some(OffPeakRates {
start_utc_minute: 60,
end_utc_minute: 120,
rates: rates(Rate::Value(1.0), Rate::Value(2.0)),
}),
};
let base = request();
let start = Request {
billed_at_utc_minute: Some(60),
..base
};
let end = Request {
billed_at_utc_minute: Some(120),
..base
};
assert_eq!(
calculate(&specification, &base),
Err(PricingError::InvalidBillingTime)
);
assert_eq!(calculate(&specification, &start).unwrap().input(), 100.0);
assert_eq!(calculate(&specification, &end).unwrap().input(), 200.0);
}
#[test]
fn missing_rates_and_free_rates_remain_distinct() {
let base = request();
let empty = Request {
usage: Usage {
prompt_tokens: 0,
completion_tokens: 0,
cache_read_tokens: 0,
cache_write_tokens: 0,
..base.usage
},
..base
};
assert_eq!(
calculate(&pricing(Rates::EMPTY), &empty),
Err(PricingError::MissingInputRate)
);
assert_eq!(
calculate(&pricing(rates(Rate::Value(0.0), Rate::Missing)), &empty),
Err(PricingError::MissingOutputRate)
);
assert_eq!(
calculate(&pricing(rates(Rate::Value(0.0), Rate::Value(0.0))), &empty)
.unwrap()
.total(),
0.0
);
}
#[test]
fn matches_executed_python_reference_cases() {
for row in include_str!("python_reference.tsv")
.lines()
.filter(|line| !line.starts_with('#'))
{
let fields: Vec<_> = row.split('\t').collect();
let count = |index: usize| fields[index].parse::<u64>().unwrap();
let number = |index: usize| fields[index].parse::<f64>().unwrap();
let optional_rate = |index: usize| {
if fields[index].is_empty() {
Rate::Missing
} else {
Rate::Value(number(index))
}
};
let threshold = ThresholdRates {
above_prompt_tokens: if fields[9].is_empty() { 0 } else { count(9) },
standard: rates(optional_rate(10), optional_rate(11)),
tiers: &[],
};
let thresholds = if fields[9].is_empty() {
&[][..]
} else {
std::slice::from_ref(&threshold)
};
let specification = Pricing {
standard: Rates {
cache_read: optional_rate(7),
cache_write: optional_rate(8),
..rates(Rate::Value(number(5)), Rate::Value(number(6)))
},
tiers: &[],
thresholds,
off_peak: None,
};
let base = request();
let input = Request {
usage: Usage {
prompt_tokens: count(1),
completion_tokens: count(2),
cache_read_tokens: count(3),
cache_write_tokens: count(4),
..base.usage
},
..base
};
let actual = calculate(&specification, &input).unwrap();
assert_eq!(actual.input(), number(12), "{}", fields[0]);
assert_eq!(actual.output(), number(13), "{}", fields[0]);
}
}
proptest::proptest! {
#[test]
fn equivalent_usage_conventions_and_breakdown_agree(
regular in 0_u64..1000,
read in 0_u64..1000,
write in 0_u64..1000,
output in 0_u64..1000,
) {
let threshold = ThresholdRates {
above_prompt_tokens: 1000,
standard: rates(Rate::Value(5.0), Rate::Value(8.0)),
tiers: &[],
};
let specification = Pricing {
standard: rates(Rate::Value(2.0), Rate::Value(4.0)),
tiers: &[],
thresholds: &[threshold],
off_peak: None,
};
let base = request();
let included = Request {
usage: Usage {
prompt_tokens: regular + read + write,
completion_tokens: output,
cache_read_tokens: read,
cache_write_tokens: write,
..base.usage
},
..base
};
let excluded = Request {
usage: Usage {
prompt_tokens: regular,
prompt_convention: PromptConvention::ExcludesCache,
..included.usage
},
..included
};
let plan = compile(&specification).unwrap();
let left = plan.calculate(&included).unwrap();
let right = plan.calculate(&excluded).unwrap();
proptest::prop_assert_eq!(left, right);
proptest::prop_assert_eq!(left.total(), left.input() + left.output());
}
}
#[test]
fn regional_multiplier_applies_after_input_components_are_summed() {
let standard = Rates {
cache_read: Rate::Value(0.5),
cache_write: Rate::Value(3.0),
..rates(Rate::Value(2.0), Rate::Value(4.0))
};
let base = request();
let regional = Request {
region_multiplier: Some(1.1),
..base
};
let result = calculate(&pricing(standard), &regional).unwrap();
assert_eq!(result.input(), (65.0 * 2.0 + 25.0 * 0.5 + 10.0 * 3.0) * 1.1);
assert_eq!(result.output(), 20.0 * 4.0 * 1.1);
let invalid = Request {
region_multiplier: Some(f64::NAN),
..base
};
assert_eq!(
calculate(&pricing(standard), &invalid),
Err(PricingError::InvalidRegionMultiplier)
);
}
#[test]
fn compilation_sorts_thresholds_and_rejects_duplicate_tiers() {
let high = ThresholdRates {
above_prompt_tokens: 200,
standard: rates(Rate::Value(7.0), Rate::Missing),
tiers: &[],
};
let low = ThresholdRates {
above_prompt_tokens: 100,
standard: rates(Rate::Value(5.0), Rate::Missing),
tiers: &[],
};
let specification = Pricing {
standard: rates(Rate::Value(2.0), Rate::Value(4.0)),
tiers: &[],
thresholds: &[high, low],
off_peak: None,
};
let base = request();
let above_both = Request {
usage: Usage {
prompt_tokens: 201,
cache_read_tokens: 0,
cache_write_tokens: 0,
..base.usage
},
..base
};
assert_eq!(
compile(&specification)
.unwrap()
.calculate(&above_both)
.unwrap()
.rates
.input,
7.0
);
let duplicate = TierRates {
tier: ServiceTier::Flex,
rates: Rates::EMPTY,
};
let invalid = Pricing {
tiers: &[duplicate, duplicate],
thresholds: &[],
..specification
};
assert_eq!(compile(&invalid).err(), Some(PricingError::DuplicateTier));
}

View file

@ -0,0 +1,96 @@
import subprocess
from dataclasses import dataclass
from pathlib import Path
from litellm.litellm_core_utils.llm_cost_calc.utils import generic_cost_per_token
from litellm.types.utils import Usage
@dataclass(frozen=True, slots=True)
class Case:
name: str
prompt: int
completion: int
cache_read: int
cache_write: int
input_rate: float
output_rate: float
cache_read_rate: float | None = None
cache_write_rate: float | None = None
threshold: int | None = None
threshold_input_rate: float | None = None
threshold_output_rate: float | None = None
CASES = (
Case("ordinary", 100, 20, 0, 0, 2.0, 4.0),
Case("cache_fallback", 100, 20, 25, 10, 2.0, 4.0),
Case("cache_specific", 100, 20, 25, 10, 2.0, 4.0, 0.5, 3.0),
Case("free_cache", 100, 20, 25, 10, 2.0, 4.0, 0.0, 0.0),
Case("threshold_below", 99, 20, 0, 0, 2.0, 4.0, threshold=100, threshold_input_rate=5.0, threshold_output_rate=8.0),
Case("threshold_at", 100, 20, 0, 0, 2.0, 4.0, threshold=100, threshold_input_rate=5.0, threshold_output_rate=8.0),
Case(
"threshold_above", 101, 20, 0, 0, 2.0, 4.0, threshold=100, threshold_input_rate=5.0, threshold_output_rate=8.0
),
Case(
"cache_threshold_above",
101,
20,
25,
10,
2.0,
4.0,
threshold=100,
threshold_input_rate=5.0,
threshold_output_rate=8.0,
),
)
def reference(case: Case) -> tuple[float, float]:
info = {"input_cost_per_token": case.input_rate, "output_cost_per_token": case.output_rate}
if case.cache_read_rate is not None:
info["cache_read_input_token_cost"] = case.cache_read_rate
if case.cache_write_rate is not None:
info["cache_creation_input_token_cost"] = case.cache_write_rate
if case.threshold is not None:
info[f"input_cost_per_token_above_{case.threshold}_tokens"] = case.threshold_input_rate
info[f"output_cost_per_token_above_{case.threshold}_tokens"] = case.threshold_output_rate
details = {"cached_tokens": case.cache_read, "cache_write_tokens": case.cache_write}
usage = Usage(prompt_tokens=case.prompt, completion_tokens=case.completion, prompt_tokens_details=details)
return generic_cost_per_token(
model="synthetic",
usage=usage,
custom_llm_provider="openai",
model_info=info,
)
def main() -> None:
revision = subprocess.check_output(("git", "rev-parse", "HEAD"), text=True).strip()
rows = ("# Python reference commit: " + revision,) + tuple(
"\t".join(
str(value) if value is not None else ""
for value in (
case.name,
case.prompt,
case.completion,
case.cache_read,
case.cache_write,
case.input_rate,
case.output_rate,
case.cache_read_rate,
case.cache_write_rate,
case.threshold,
case.threshold_input_rate,
case.threshold_output_rate,
*reference(case),
)
)
for case in CASES
)
Path(__file__).with_name("python_reference.tsv").write_text("\n".join(rows) + "\n")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,9 @@
# Python reference commit: dc4be2fd987c993aefcf16444e34f960c12c8627
ordinary 100 20 0 0 2.0 4.0 200.0 80.0
cache_fallback 100 20 25 10 2.0 4.0 200.0 80.0
cache_specific 100 20 25 10 2.0 4.0 0.5 3.0 172.5 80.0
free_cache 100 20 25 10 2.0 4.0 0.0 0.0 130.0 80.0
threshold_below 99 20 0 0 2.0 4.0 100 5.0 8.0 198.0 80.0
threshold_at 100 20 0 0 2.0 4.0 100 5.0 8.0 200.0 80.0
threshold_above 101 20 0 0 2.0 4.0 100 5.0 8.0 505.0 160.0
cache_threshold_above 101 20 25 10 2.0 4.0 100 5.0 8.0 505.0 160.0
Can't render this file because it has a wrong number of fields in line 2.