mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
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:
parent
58a05a9eae
commit
7177d3b6d1
9 changed files with 1109 additions and 0 deletions
8
litellm-rust/Cargo.lock
generated
8
litellm-rust/Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
14
litellm-rust/crates/cost/Cargo.toml
Normal file
14
litellm-rust/crates/cost/Cargo.toml
Normal 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
|
||||
13
litellm-rust/crates/cost/README.md
Normal file
13
litellm-rust/crates/cost/README.md
Normal 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
|
||||
66
litellm-rust/crates/cost/benches/calculate.rs
Normal file
66
litellm-rust/crates/cost/benches/calculate.rs
Normal 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);
|
||||
40
litellm-rust/crates/cost/examples/charge.rs
Normal file
40
litellm-rust/crates/cost/examples/charge.rs
Normal 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()
|
||||
);
|
||||
}
|
||||
405
litellm-rust/crates/cost/src/lib.rs
Normal file
405
litellm-rust/crates/cost/src/lib.rs
Normal 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)
|
||||
}
|
||||
458
litellm-rust/crates/cost/tests/calculation.rs
Normal file
458
litellm-rust/crates/cost/tests/calculation.rs
Normal 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), ®ional).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));
|
||||
}
|
||||
96
litellm-rust/crates/cost/tests/generate_python_reference.py
Normal file
96
litellm-rust/crates/cost/tests/generate_python_reference.py
Normal 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()
|
||||
9
litellm-rust/crates/cost/tests/python_reference.tsv
Normal file
9
litellm-rust/crates/cost/tests/python_reference.tsv
Normal 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.
|
Loading…
Add table
Reference in a new issue