mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-03 07:23:33 +00:00
Merge c5db9162e0 into dddbaa803d
This commit is contained in:
commit
609bb56ca8
37 changed files with 4373 additions and 1932 deletions
15
litellm-rust/Cargo.lock
generated
15
litellm-rust/Cargo.lock
generated
|
|
@ -3138,6 +3138,7 @@ dependencies = [
|
|||
"defmt",
|
||||
"jiff-core",
|
||||
"jiff-static",
|
||||
"jiff-tzdb",
|
||||
"log",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
|
|
@ -3166,6 +3167,12 @@ dependencies = [
|
|||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-tzdb"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e"
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.22.4"
|
||||
|
|
@ -3689,6 +3696,9 @@ dependencies = [
|
|||
"criterion",
|
||||
"proptest",
|
||||
"rstest",
|
||||
"schemars 1.2.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4195,12 +4205,15 @@ name = "litellm-model-catalog"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"indexmap 2.14.0",
|
||||
"jiff",
|
||||
"jsonschema",
|
||||
"litellm-llms-types",
|
||||
"litellm-cost",
|
||||
"macro_rules_attribute",
|
||||
"rstest",
|
||||
"schemars 1.2.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"thiserror 2.0.19",
|
||||
"time",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ litellm-token-counter-huggingface = { path = "crates/token-counter-huggingface"
|
|||
litellm-token-counter-tiktoken = { path = "crates/token-counter-tiktoken" }
|
||||
litellm-host-python = { path = "crates/host-python" }
|
||||
litellm-python-compat = { path = "crates/python-compat" }
|
||||
litellm-cost = { path = "crates/cost" }
|
||||
|
||||
askama = { version = "0.16.1", default-features = false, features = ["derive", "std"] }
|
||||
tracing = "0.1"
|
||||
|
|
|
|||
|
|
@ -5,10 +5,19 @@ edition.workspace = true
|
|||
license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[features]
|
||||
serde = ["dep:serde"]
|
||||
schema = ["serde", "dep:schemars"]
|
||||
|
||||
[dependencies]
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion.workspace = true
|
||||
proptest.workspace = true
|
||||
rstest.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
[[bench]]
|
||||
name = "calculate"
|
||||
|
|
|
|||
|
|
@ -11,3 +11,5 @@ The caller states whether `prompt_tokens` includes cache tokens. Threshold selec
|
|||
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
|
||||
|
||||
`money`, `price` and `schedule` hold the typed price model that `litellm-model-catalog` lowers each catalog entry into. `UnitPrice` is USD per unit and `Usd` is an amount, and only the operations that make sense between them exist, so a price is never added to an amount. `Prices::band` and `Prices::rate` resolve a token rate the way Python's `_get_token_base_cost` does. `calculate` does not read this model yet
|
||||
|
|
|
|||
405
litellm-rust/crates/cost/src/calculate.rs
Normal file
405
litellm-rust/crates/cost/src/calculate.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)
|
||||
}
|
||||
22
litellm-rust/crates/cost/src/error.rs
Normal file
22
litellm-rust/crates/cost/src/error.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
use std::fmt;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct InvalidUtcWindow;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct InvalidWeekday;
|
||||
|
||||
impl fmt::Display for InvalidUtcWindow {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("expected a UTC window HH:MM-HH:MM")
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for InvalidWeekday {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("expected an ISO weekday number 1-7 or an English day name")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for InvalidUtcWindow {}
|
||||
impl std::error::Error for InvalidWeekday {}
|
||||
|
|
@ -1,405 +1,8 @@
|
|||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Rate {
|
||||
Missing,
|
||||
Null,
|
||||
Value(f64),
|
||||
}
|
||||
mod calculate;
|
||||
mod error;
|
||||
pub mod money;
|
||||
pub mod price;
|
||||
pub mod schedule;
|
||||
|
||||
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)
|
||||
}
|
||||
pub use calculate::*;
|
||||
pub use error::*;
|
||||
|
|
|
|||
238
litellm-rust/crates/cost/src/money.rs
Normal file
238
litellm-rust/crates/cost/src/money.rs
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
use std::iter::Sum;
|
||||
use std::ops::{Add, Mul};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
|
||||
pub struct UnitPrice(f64);
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
|
||||
pub struct Usd(f64);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
|
||||
pub struct Multiplier(f64);
|
||||
|
||||
impl UnitPrice {
|
||||
pub fn new(usd_per_unit: f64) -> Option<Self> {
|
||||
(usd_per_unit.is_finite() && usd_per_unit >= 0.0).then_some(Self(usd_per_unit))
|
||||
}
|
||||
pub fn get(self) -> f64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Usd {
|
||||
pub const ZERO: Self = Self(0.0);
|
||||
|
||||
pub fn new(amount: f64) -> Option<Self> {
|
||||
(amount.is_finite() && amount >= 0.0).then_some(Self(amount))
|
||||
}
|
||||
pub fn get(self) -> f64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Multiplier {
|
||||
pub fn new(factor: f64) -> Option<Self> {
|
||||
(factor.is_finite() && factor >= 1.0).then_some(Self(factor))
|
||||
}
|
||||
pub fn get(self) -> f64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<u64> for UnitPrice {
|
||||
type Output = Usd;
|
||||
|
||||
fn mul(self, units: u64) -> Usd {
|
||||
Usd(self.0 * units as f64)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Multiplier> for UnitPrice {
|
||||
type Output = UnitPrice;
|
||||
|
||||
fn mul(self, factor: Multiplier) -> UnitPrice {
|
||||
UnitPrice(self.0 * factor.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Multiplier> for Usd {
|
||||
type Output = Usd;
|
||||
|
||||
fn mul(self, factor: Multiplier) -> Usd {
|
||||
Usd(self.0 * factor.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for Usd {
|
||||
type Output = Usd;
|
||||
|
||||
fn add(self, other: Usd) -> Usd {
|
||||
Usd(self.0 + other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sum for Usd {
|
||||
fn sum<I: Iterator<Item = Usd>>(amounts: I) -> Usd {
|
||||
amounts.fold(Usd::ZERO, Add::add)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde_impls {
|
||||
use std::fmt;
|
||||
|
||||
use serde::de::{self, Deserialize, Deserializer, Visitor};
|
||||
|
||||
use super::{Multiplier, UnitPrice};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Number(&'static str);
|
||||
|
||||
impl Visitor<'_> for Number {
|
||||
type Value = f64;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str(self.0)
|
||||
}
|
||||
|
||||
fn visit_i64<E: de::Error>(self, value: i64) -> Result<f64, E> {
|
||||
Ok(value as f64)
|
||||
}
|
||||
|
||||
fn visit_u64<E: de::Error>(self, value: u64) -> Result<f64, E> {
|
||||
Ok(value as f64)
|
||||
}
|
||||
|
||||
fn visit_f64<E: de::Error>(self, value: f64) -> Result<f64, E> {
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn visit_str<E: de::Error>(self, value: &str) -> Result<f64, E> {
|
||||
value
|
||||
.trim()
|
||||
.parse()
|
||||
.map_err(|_| E::invalid_value(de::Unexpected::Str(value), &self))
|
||||
}
|
||||
}
|
||||
|
||||
fn checked<'de, D: Deserializer<'de>, T>(
|
||||
deserializer: D,
|
||||
expected: Number,
|
||||
new: fn(f64) -> Option<T>,
|
||||
) -> Result<T, D::Error> {
|
||||
let number = deserializer.deserialize_any(expected)?;
|
||||
new(number)
|
||||
.ok_or_else(|| de::Error::invalid_value(de::Unexpected::Float(number), &expected))
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for UnitPrice {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
checked(deserializer, Number("a non-negative USD amount"), Self::new)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Multiplier {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
checked(
|
||||
deserializer,
|
||||
Number("a multiplier of at least 1"),
|
||||
Self::new,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
mod schema {
|
||||
use std::borrow::Cow;
|
||||
|
||||
use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
|
||||
|
||||
use super::{Multiplier, UnitPrice};
|
||||
|
||||
impl JsonSchema for UnitPrice {
|
||||
fn inline_schema() -> bool {
|
||||
true
|
||||
}
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"UnitPrice".into()
|
||||
}
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({"type": "number", "minimum": 0})
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for Multiplier {
|
||||
fn inline_schema() -> bool {
|
||||
true
|
||||
}
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"Multiplier".into()
|
||||
}
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({"type": "number", "minimum": 1})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
#[case::price(0.5)]
|
||||
#[case::free(0.0)]
|
||||
fn unit_price_keeps_finite_non_negative_values(#[case] value: f64) {
|
||||
assert_eq!(UnitPrice::new(value).map(UnitPrice::get), Some(value));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::negative(-1e-9)]
|
||||
#[case::nan(f64::NAN)]
|
||||
#[case::infinite(f64::INFINITY)]
|
||||
fn unit_price_rejects_what_no_price_can_be(#[case] value: f64) {
|
||||
assert_eq!(UnitPrice::new(value), None);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::uplift(1.1, true)]
|
||||
#[case::neutral(1.0, true)]
|
||||
#[case::discount(0.5, false)]
|
||||
#[case::nan(f64::NAN, false)]
|
||||
fn multiplier_is_never_a_discount(#[case] value: f64, #[case] accepted: bool) {
|
||||
assert_eq!(Multiplier::new(value).is_some(), accepted);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn amounts_follow_units_times_price_then_uplift() {
|
||||
let price = UnitPrice::new(0.25).unwrap();
|
||||
let uplift = Multiplier::new(2.0).unwrap();
|
||||
|
||||
let total: Usd = [price * 4, (price * uplift) * 2].into_iter().sum();
|
||||
|
||||
assert_eq!(total.get(), 2.0);
|
||||
assert_eq!((total * uplift).get(), 4.0);
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[rstest]
|
||||
#[case::number(serde_json::json!(1.5e-6), Some(1.5e-6))]
|
||||
#[case::integer(serde_json::json!(2), Some(2.0))]
|
||||
#[case::numeric_string(serde_json::json!(" 0.000002 "), Some(2e-6))]
|
||||
#[case::negative(serde_json::json!(-1), None)]
|
||||
#[case::word(serde_json::json!("free"), None)]
|
||||
#[case::boolean(serde_json::json!(true), None)]
|
||||
fn unit_price_reads_numbers_and_numeric_strings(
|
||||
#[case] written: serde_json::Value,
|
||||
#[case] expected: Option<f64>,
|
||||
) {
|
||||
use serde::Deserialize;
|
||||
|
||||
assert_eq!(
|
||||
UnitPrice::deserialize(&written).ok().map(UnitPrice::get),
|
||||
expected
|
||||
);
|
||||
}
|
||||
}
|
||||
407
litellm-rust/crates/cost/src/price.rs
Normal file
407
litellm-rust/crates/cost/src/price.rs
Normal file
|
|
@ -0,0 +1,407 @@
|
|||
mod meter;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::num::NonZeroU64;
|
||||
|
||||
pub use meter::*;
|
||||
|
||||
use crate::money::{Multiplier, UnitPrice};
|
||||
use crate::schedule::Schedule;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ServiceTier {
|
||||
#[default]
|
||||
Standard,
|
||||
Priority,
|
||||
Flex,
|
||||
Ultrafast,
|
||||
Balanced,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum Band {
|
||||
#[default]
|
||||
Base,
|
||||
OverPromptTokens(NonZeroU64),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct When {
|
||||
pub tier: ServiceTier,
|
||||
pub band: Band,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum Channel {
|
||||
#[default]
|
||||
Online,
|
||||
Batch,
|
||||
Decisions,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Price {
|
||||
pub meter: Meter,
|
||||
pub channel: Channel,
|
||||
pub when: When,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Prices {
|
||||
rates: BTreeMap<Price, UnitPrice>,
|
||||
off_peak: Option<OffPeak>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct OffPeak {
|
||||
schedule: Schedule,
|
||||
rates: OffPeakRates,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct OffPeakRates {
|
||||
pub input: Option<UnitPrice>,
|
||||
pub output: Option<UnitPrice>,
|
||||
pub cache_read: Option<UnitPrice>,
|
||||
pub explicit_cache_read: Option<UnitPrice>,
|
||||
pub cache_write: Option<UnitPrice>,
|
||||
pub reasoning: Option<UnitPrice>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Adjustments(BTreeMap<Uplift, Multiplier>);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum Uplift {
|
||||
NonGlobalEndpoint,
|
||||
Residency(Residency),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum Residency {
|
||||
Eu,
|
||||
Us,
|
||||
}
|
||||
|
||||
impl Band {
|
||||
pub fn over_prompt_tokens(tokens: u64) -> Self {
|
||||
NonZeroU64::new(tokens).map_or(Self::Base, Self::OverPromptTokens)
|
||||
}
|
||||
}
|
||||
|
||||
impl Price {
|
||||
pub fn online(meter: Meter, when: When) -> Self {
|
||||
Self {
|
||||
meter,
|
||||
channel: Channel::Online,
|
||||
when,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Prices {
|
||||
pub fn new(rates: BTreeMap<Price, UnitPrice>, off_peak: Option<OffPeak>) -> Self {
|
||||
Self { rates, off_peak }
|
||||
}
|
||||
|
||||
pub fn get(&self, price: &Price) -> Option<UnitPrice> {
|
||||
self.rates.get(price).copied()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&Price, UnitPrice)> {
|
||||
self.rates.iter().map(|(price, rate)| (price, *rate))
|
||||
}
|
||||
|
||||
pub fn off_peak(&self) -> Option<&OffPeak> {
|
||||
self.off_peak.as_ref()
|
||||
}
|
||||
|
||||
/// The online prompt band a request falls in. Every token meter's `_above_` prices count,
|
||||
/// and the highest band the prompt passes wins.
|
||||
pub fn band(&self, tier: ServiceTier, prompt_tokens: u64, inclusive: bool) -> Band {
|
||||
self.rates
|
||||
.keys()
|
||||
.filter(|price| {
|
||||
price.channel == Channel::Online
|
||||
&& price.meter.sets_prompt_band()
|
||||
&& (price.when.tier == ServiceTier::Standard || price.when.tier == tier)
|
||||
})
|
||||
.filter_map(|price| match price.when.band {
|
||||
Band::OverPromptTokens(floor) => Some(floor.get()),
|
||||
Band::Base => None,
|
||||
})
|
||||
.filter(|floor| prompt_tokens > *floor || (inclusive && prompt_tokens == *floor))
|
||||
.max()
|
||||
.map_or(Band::Base, Band::over_prompt_tokens)
|
||||
}
|
||||
|
||||
/// The online rate for `meter`, tried at `(tier, band)`, `(standard, band)`, `(tier, base)`
|
||||
/// and `(standard, base)` in that order. A missing cache price falls back to the input price
|
||||
/// resolved the same way.
|
||||
pub fn rate(&self, meter: &Meter, tier: ServiceTier, band: Band) -> Option<UnitPrice> {
|
||||
[
|
||||
When { tier, band },
|
||||
When {
|
||||
tier: ServiceTier::Standard,
|
||||
band,
|
||||
},
|
||||
When {
|
||||
tier,
|
||||
band: Band::Base,
|
||||
},
|
||||
When::default(),
|
||||
]
|
||||
.into_iter()
|
||||
.find_map(|when| self.get(&Price::online(meter.clone(), when)))
|
||||
.or_else(|| self.rate(&meter.fallback()?, tier, band))
|
||||
}
|
||||
}
|
||||
|
||||
impl OffPeak {
|
||||
pub fn new(schedule: Schedule, rates: OffPeakRates) -> Option<Self> {
|
||||
(rates != OffPeakRates::default()).then_some(Self { schedule, rates })
|
||||
}
|
||||
|
||||
pub fn schedule(&self) -> &Schedule {
|
||||
&self.schedule
|
||||
}
|
||||
|
||||
pub fn rates(&self) -> &OffPeakRates {
|
||||
&self.rates
|
||||
}
|
||||
}
|
||||
|
||||
impl Adjustments {
|
||||
pub fn new(uplifts: impl IntoIterator<Item = (Uplift, Multiplier)>) -> Self {
|
||||
Self(uplifts.into_iter().collect())
|
||||
}
|
||||
|
||||
pub fn get(&self, uplift: Uplift) -> Option<Multiplier> {
|
||||
self.0.get(&uplift).copied()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use super::*;
|
||||
|
||||
const INPUT: Meter = Meter::TEXT_INPUT;
|
||||
const OUTPUT: Meter = Meter::Output(OutputUnit::Token(TokenKind::Text));
|
||||
const CACHE_READ: Meter = Meter::CacheRead {
|
||||
kind: TokenKind::Text,
|
||||
mode: CacheReadMode::Implicit,
|
||||
};
|
||||
const CACHE_WRITE_HOUR: Meter = Meter::CacheWrite {
|
||||
kind: TokenKind::Text,
|
||||
ttl: CacheTtl::OneHour,
|
||||
};
|
||||
|
||||
fn over(tokens: u64) -> Band {
|
||||
Band::over_prompt_tokens(tokens)
|
||||
}
|
||||
|
||||
fn prices(rates: &[(Meter, Channel, ServiceTier, Band, f64)]) -> Prices {
|
||||
Prices::new(
|
||||
rates
|
||||
.iter()
|
||||
.map(|(meter, channel, tier, band, rate)| {
|
||||
(
|
||||
Price {
|
||||
meter: meter.clone(),
|
||||
channel: *channel,
|
||||
when: When {
|
||||
tier: *tier,
|
||||
band: *band,
|
||||
},
|
||||
},
|
||||
UnitPrice::new(*rate).unwrap(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn priority_with_a_standard_band() -> Prices {
|
||||
use Channel::Online;
|
||||
use ServiceTier::{Priority, Standard};
|
||||
prices(&[
|
||||
(INPUT, Online, Standard, Band::Base, 1.0),
|
||||
(INPUT, Online, Priority, Band::Base, 2.0),
|
||||
(INPUT, Online, Standard, over(200_000), 10.0),
|
||||
(OUTPUT, Online, Standard, Band::Base, 3.0),
|
||||
(OUTPUT, Online, Priority, over(200_000), 30.0),
|
||||
])
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::below(1_000, false, Band::Base)]
|
||||
#[case::at_floor_exclusive(200_000, false, Band::Base)]
|
||||
#[case::at_floor_inclusive(200_000, true, over(200_000))]
|
||||
#[case::above(200_001, false, over(200_000))]
|
||||
fn the_band_starts_strictly_above_its_floor_unless_inclusive(
|
||||
priority_with_a_standard_band: Prices,
|
||||
#[case] prompt_tokens: u64,
|
||||
#[case] inclusive: bool,
|
||||
#[case] expected: Band,
|
||||
) {
|
||||
assert_eq!(
|
||||
priority_with_a_standard_band.band(ServiceTier::Standard, prompt_tokens, inclusive),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn a_band_only_another_tier_prices_does_not_apply() {
|
||||
let prices = prices(&[(
|
||||
OUTPUT,
|
||||
Channel::Online,
|
||||
ServiceTier::Flex,
|
||||
over(100_000),
|
||||
1.0,
|
||||
)]);
|
||||
assert_eq!(
|
||||
prices.band(ServiceTier::Standard, 300_000, false),
|
||||
Band::Base
|
||||
);
|
||||
assert_eq!(
|
||||
prices.band(ServiceTier::Flex, 300_000, false),
|
||||
over(100_000)
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn batch_bands_never_pick_the_online_band() {
|
||||
let prices = prices(&[(
|
||||
INPUT,
|
||||
Channel::Batch,
|
||||
ServiceTier::Standard,
|
||||
over(100_000),
|
||||
1.0,
|
||||
)]);
|
||||
assert_eq!(
|
||||
prices.band(ServiceTier::Standard, 300_000, false),
|
||||
Band::Base
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn the_highest_band_passed_wins() {
|
||||
let prices = prices(&[
|
||||
(
|
||||
INPUT,
|
||||
Channel::Online,
|
||||
ServiceTier::Standard,
|
||||
over(100_000),
|
||||
1.0,
|
||||
),
|
||||
(
|
||||
OUTPUT,
|
||||
Channel::Online,
|
||||
ServiceTier::Standard,
|
||||
over(200_000),
|
||||
1.0,
|
||||
),
|
||||
]);
|
||||
assert_eq!(
|
||||
prices.band(ServiceTier::Standard, 300_000, false),
|
||||
over(200_000)
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::tier_base(INPUT, ServiceTier::Priority, Band::Base, Some(2.0))]
|
||||
#[case::standard_band_beats_tier_base(INPUT, ServiceTier::Priority, over(200_000), Some(10.0))]
|
||||
#[case::tier_band(OUTPUT, ServiceTier::Priority, over(200_000), Some(30.0))]
|
||||
#[case::standard_base_when_nothing_else(OUTPUT, ServiceTier::Flex, over(200_000), Some(3.0))]
|
||||
#[case::cache_read_falls_back_to_the_resolved_input(
|
||||
CACHE_READ,
|
||||
ServiceTier::Priority,
|
||||
over(200_000),
|
||||
Some(10.0)
|
||||
)]
|
||||
#[case::one_hour_write_falls_back_through_write_to_input(
|
||||
CACHE_WRITE_HOUR,
|
||||
ServiceTier::Priority,
|
||||
Band::Base,
|
||||
Some(2.0)
|
||||
)]
|
||||
#[case::no_fallback_for_other_meters(
|
||||
Meter::Input(InputUnit::Token(TokenKind::Audio)),
|
||||
ServiceTier::Standard,
|
||||
Band::Base,
|
||||
None
|
||||
)]
|
||||
fn rate_walks_tier_then_band(
|
||||
priority_with_a_standard_band: Prices,
|
||||
#[case] meter: Meter,
|
||||
#[case] tier: ServiceTier,
|
||||
#[case] band: Band,
|
||||
#[case] expected: Option<f64>,
|
||||
) {
|
||||
assert_eq!(
|
||||
priority_with_a_standard_band
|
||||
.rate(&meter, tier, band)
|
||||
.map(UnitPrice::get),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn an_explicit_cache_price_wins_over_the_input_fallback() {
|
||||
let prices = prices(&[
|
||||
(
|
||||
INPUT,
|
||||
Channel::Online,
|
||||
ServiceTier::Standard,
|
||||
Band::Base,
|
||||
1.0,
|
||||
),
|
||||
(
|
||||
CACHE_READ,
|
||||
Channel::Online,
|
||||
ServiceTier::Standard,
|
||||
Band::Base,
|
||||
0.0,
|
||||
),
|
||||
]);
|
||||
assert_eq!(
|
||||
prices
|
||||
.rate(&CACHE_READ, ServiceTier::Standard, Band::Base)
|
||||
.map(UnitPrice::get),
|
||||
Some(0.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn off_peak_needs_a_price() {
|
||||
let schedule = Schedule {
|
||||
windows: Vec::new(),
|
||||
weekday_timezone: None,
|
||||
};
|
||||
assert_eq!(
|
||||
OffPeak::new(schedule.clone(), OffPeakRates::default()),
|
||||
None
|
||||
);
|
||||
assert!(
|
||||
OffPeak::new(
|
||||
schedule,
|
||||
OffPeakRates {
|
||||
output: UnitPrice::new(1.0),
|
||||
..OffPeakRates::default()
|
||||
}
|
||||
)
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::zero(0, Band::Base)]
|
||||
#[case::positive(272_000, Band::OverPromptTokens(NonZeroU64::new(272_000).unwrap()))]
|
||||
fn a_zero_floor_is_the_base_band(#[case] tokens: u64, #[case] expected: Band) {
|
||||
assert_eq!(Band::over_prompt_tokens(tokens), expected);
|
||||
}
|
||||
}
|
||||
192
litellm-rust/crates/cost/src/price/meter.rs
Normal file
192
litellm-rust/crates/cost/src/price/meter.rs
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
use std::num::NonZeroU64;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum Meter {
|
||||
Input(InputUnit),
|
||||
Output(OutputUnit),
|
||||
CacheRead {
|
||||
kind: TokenKind,
|
||||
mode: CacheReadMode,
|
||||
},
|
||||
CacheWrite {
|
||||
kind: TokenKind,
|
||||
ttl: CacheTtl,
|
||||
},
|
||||
Feature(Feature),
|
||||
WallClockSecond,
|
||||
WebSearch(SearchContextSize),
|
||||
Guardrail(GuardrailUnit),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum TokenKind {
|
||||
Text,
|
||||
Audio,
|
||||
Image,
|
||||
Video,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum InputUnit {
|
||||
Token(TokenKind),
|
||||
Character,
|
||||
Image,
|
||||
Pixel,
|
||||
Query { over_results: Option<NonZeroU64> },
|
||||
Request,
|
||||
Second,
|
||||
TextMessage,
|
||||
AudioSecond,
|
||||
VideoSecond { over_seconds: Option<NonZeroU64> },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum OutputUnit {
|
||||
Token(TokenKind),
|
||||
ReasoningToken,
|
||||
Character,
|
||||
Image(Option<ImageSize>),
|
||||
Pixel,
|
||||
Second(Option<Resolution>),
|
||||
VideoSecond,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum CacheReadMode {
|
||||
Implicit,
|
||||
Explicit,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum CacheTtl {
|
||||
Default,
|
||||
OneHour,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum Feature {
|
||||
AnnotationPage,
|
||||
CitationToken,
|
||||
CodeInterpreterSession,
|
||||
GoogleMapsGroundingQuery,
|
||||
OcrCredit,
|
||||
OcrPage,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum SearchContextSize {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct GuardrailUnit(Box<str>);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ImageSize(Box<str>);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Resolution(Box<str>);
|
||||
|
||||
impl Meter {
|
||||
pub(crate) const TEXT_INPUT: Self = Self::Input(InputUnit::Token(TokenKind::Text));
|
||||
|
||||
pub(crate) fn sets_prompt_band(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Input(InputUnit::Token(TokenKind::Text))
|
||||
| Self::Output(OutputUnit::Token(TokenKind::Text))
|
||||
| Self::CacheRead {
|
||||
kind: TokenKind::Text,
|
||||
mode: CacheReadMode::Implicit,
|
||||
}
|
||||
| Self::CacheWrite {
|
||||
kind: TokenKind::Text,
|
||||
..
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn fallback(&self) -> Option<Self> {
|
||||
match self {
|
||||
Self::CacheRead {
|
||||
kind: TokenKind::Text,
|
||||
mode: CacheReadMode::Implicit,
|
||||
}
|
||||
| Self::CacheWrite {
|
||||
kind: TokenKind::Text,
|
||||
ttl: CacheTtl::Default,
|
||||
} => Some(Self::TEXT_INPUT),
|
||||
Self::CacheWrite {
|
||||
kind: TokenKind::Text,
|
||||
ttl: CacheTtl::OneHour,
|
||||
} => Some(Self::CacheWrite {
|
||||
kind: TokenKind::Text,
|
||||
ttl: CacheTtl::Default,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GuardrailUnit {
|
||||
pub fn new(counter: &str) -> Self {
|
||||
Self(counter.into())
|
||||
}
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ImageSize {
|
||||
pub fn new(label: &str) -> Option<Self> {
|
||||
let number = label.strip_suffix('K').unwrap_or(label);
|
||||
let (whole, fraction) = number.split_once('.').unwrap_or((number, "0"));
|
||||
(is_digits(whole) && is_digits(fraction)).then(|| Self(label.into()))
|
||||
}
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolution {
|
||||
pub fn new(label: &str) -> Option<Self> {
|
||||
let digits = label.strip_suffix(['p', 'k'])?;
|
||||
is_digits(digits).then(|| Self(label.into()))
|
||||
}
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
fn is_digits(value: &str) -> bool {
|
||||
!value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
#[case::kilo("1K", true)]
|
||||
#[case::half_kilo("0.5K", true)]
|
||||
#[case::pixels("1024", true)]
|
||||
#[case::word("big", false)]
|
||||
#[case::bare_suffix("K", false)]
|
||||
#[case::trailing_dot("1.K", false)]
|
||||
fn image_size_labels_are_numbers_with_an_optional_k(#[case] label: &str, #[case] valid: bool) {
|
||||
assert_eq!(ImageSize::new(label).is_some(), valid);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::progressive("720p", true)]
|
||||
#[case::kilo("4k", true)]
|
||||
#[case::bare("720", false)]
|
||||
#[case::word("hd", false)]
|
||||
fn resolution_labels_are_numbers_with_p_or_k(#[case] label: &str, #[case] valid: bool) {
|
||||
assert_eq!(Resolution::new(label).is_some(), valid);
|
||||
}
|
||||
}
|
||||
241
litellm-rust/crates/cost/src/schedule.rs
Normal file
241
litellm-rust/crates/cost/src/schedule.rs
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use crate::error::{InvalidUtcWindow, InvalidWeekday};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Schedule {
|
||||
pub windows: Vec<Window>,
|
||||
pub weekday_timezone: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Window {
|
||||
pub hours: Vec<UtcWindow>,
|
||||
pub weekdays: Option<Vec<Weekday>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct UtcWindow {
|
||||
pub start_minute: u16,
|
||||
pub end_minute: u16,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum Weekday {
|
||||
Monday,
|
||||
Tuesday,
|
||||
Wednesday,
|
||||
Thursday,
|
||||
Friday,
|
||||
Saturday,
|
||||
Sunday,
|
||||
}
|
||||
|
||||
impl FromStr for UtcWindow {
|
||||
type Err = InvalidUtcWindow;
|
||||
|
||||
fn from_str(window: &str) -> Result<Self, Self::Err> {
|
||||
let (start, end) = window.split_once('-').ok_or(InvalidUtcWindow)?;
|
||||
Ok(Self {
|
||||
start_minute: minute_of_day(start).ok_or(InvalidUtcWindow)?,
|
||||
end_minute: minute_of_day(end).ok_or(InvalidUtcWindow)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn minute_of_day(clock: &str) -> Option<u16> {
|
||||
let (hour, minute) = clock.split_once(':')?;
|
||||
if hour.len() != 2 || minute.len() != 2 {
|
||||
return None;
|
||||
}
|
||||
let hour: u16 = hour.parse().ok().filter(|hour| *hour < 24)?;
|
||||
let minute: u16 = minute.parse().ok().filter(|minute| *minute < 60)?;
|
||||
Some(hour * 60 + minute)
|
||||
}
|
||||
|
||||
impl Weekday {
|
||||
pub fn from_iso(number: u8) -> Option<Self> {
|
||||
match number {
|
||||
1 => Some(Self::Monday),
|
||||
2 => Some(Self::Tuesday),
|
||||
3 => Some(Self::Wednesday),
|
||||
4 => Some(Self::Thursday),
|
||||
5 => Some(Self::Friday),
|
||||
6 => Some(Self::Saturday),
|
||||
7 => Some(Self::Sunday),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Weekday {
|
||||
type Err = InvalidWeekday;
|
||||
|
||||
fn from_str(name: &str) -> Result<Self, Self::Err> {
|
||||
match name.trim().to_ascii_lowercase().as_str() {
|
||||
"mon" | "monday" => Ok(Self::Monday),
|
||||
"tue" | "tues" | "tuesday" => Ok(Self::Tuesday),
|
||||
"wed" | "wednesday" => Ok(Self::Wednesday),
|
||||
"thu" | "thur" | "thurs" | "thursday" => Ok(Self::Thursday),
|
||||
"fri" | "friday" => Ok(Self::Friday),
|
||||
"sat" | "saturday" => Ok(Self::Saturday),
|
||||
"sun" | "sunday" => Ok(Self::Sunday),
|
||||
_ => Err(InvalidWeekday),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde_impls {
|
||||
use std::fmt;
|
||||
|
||||
use serde::de::{self, Deserialize, Deserializer, Visitor};
|
||||
|
||||
use super::{UtcWindow, Weekday};
|
||||
|
||||
impl<'de> Deserialize<'de> for UtcWindow {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
let window = String::deserialize(deserializer)?;
|
||||
window.parse().map_err(de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
struct WrittenWeekday;
|
||||
|
||||
impl Visitor<'_> for WrittenWeekday {
|
||||
type Value = Weekday;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("an ISO weekday number 1-7 or an English day name")
|
||||
}
|
||||
|
||||
fn visit_u64<E: de::Error>(self, number: u64) -> Result<Weekday, E> {
|
||||
u8::try_from(number)
|
||||
.ok()
|
||||
.and_then(Weekday::from_iso)
|
||||
.ok_or_else(|| E::invalid_value(de::Unexpected::Unsigned(number), &self))
|
||||
}
|
||||
|
||||
fn visit_i64<E: de::Error>(self, number: i64) -> Result<Weekday, E> {
|
||||
u64::try_from(number)
|
||||
.map_err(|_| E::invalid_value(de::Unexpected::Signed(number), &self))
|
||||
.and_then(|number| self.visit_u64(number))
|
||||
}
|
||||
|
||||
fn visit_str<E: de::Error>(self, name: &str) -> Result<Weekday, E> {
|
||||
name.parse()
|
||||
.map_err(|_| E::invalid_value(de::Unexpected::Str(name), &self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Weekday {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
deserializer.deserialize_any(WrittenWeekday)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
mod schema {
|
||||
use std::borrow::Cow;
|
||||
|
||||
use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
|
||||
|
||||
use super::{UtcWindow, Weekday};
|
||||
|
||||
impl JsonSchema for UtcWindow {
|
||||
fn inline_schema() -> bool {
|
||||
true
|
||||
}
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"UtcWindow".into()
|
||||
}
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({
|
||||
"type": "string",
|
||||
"pattern": r"^([01]\d|2[0-3]):[0-5]\d-([01]\d|2[0-3]):[0-5]\d$"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for Weekday {
|
||||
fn inline_schema() -> bool {
|
||||
true
|
||||
}
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"Weekday".into()
|
||||
}
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({
|
||||
"anyOf": [
|
||||
{"type": "integer", "minimum": 1, "maximum": 7},
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": r"(?i)^\s*(mon|monday|tue|tues|tuesday|wed|wednesday|thu|thur|thurs|thursday|fri|friday|sat|saturday|sun|sunday)\s*$"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
#[case::same_day("10:00-18:30", Some((600, 1110)))]
|
||||
#[case::wraps_midnight("22:00-02:00", Some((1320, 120)))]
|
||||
#[case::whole_day("00:00-00:00", Some((0, 0)))]
|
||||
#[case::hour_out_of_range("25:00-01:00", None)]
|
||||
#[case::minute_out_of_range("10:60-11:00", None)]
|
||||
#[case::one_digit_hour("9:00-10:00", None)]
|
||||
#[case::no_separator("10:00", None)]
|
||||
fn utc_window_reads_hh_mm_ranges(#[case] text: &str, #[case] expected: Option<(u16, u16)>) {
|
||||
assert_eq!(
|
||||
text.parse::<UtcWindow>()
|
||||
.ok()
|
||||
.map(|window| (window.start_minute, window.end_minute)),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::short("thurs", Some(Weekday::Thursday))]
|
||||
#[case::upper("SATURDAY", Some(Weekday::Saturday))]
|
||||
#[case::padded(" mon ", Some(Weekday::Monday))]
|
||||
#[case::unknown("someday", None)]
|
||||
#[case::empty("", None)]
|
||||
fn weekday_names_match_python_after_trimming(
|
||||
#[case] name: &str,
|
||||
#[case] expected: Option<Weekday>,
|
||||
) {
|
||||
assert_eq!(name.parse().ok(), expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::monday(1, Some(Weekday::Monday))]
|
||||
#[case::sunday(7, Some(Weekday::Sunday))]
|
||||
#[case::zero(0, None)]
|
||||
#[case::eight(8, None)]
|
||||
fn weekday_numbers_are_iso(#[case] number: u8, #[case] expected: Option<Weekday>) {
|
||||
assert_eq!(Weekday::from_iso(number), expected);
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
#[rstest]
|
||||
#[case::number(serde_json::json!(6), Some(Weekday::Saturday))]
|
||||
#[case::name(serde_json::json!("Sun"), Some(Weekday::Sunday))]
|
||||
#[case::negative(serde_json::json!(-1), None)]
|
||||
#[case::boolean(serde_json::json!(true), None)]
|
||||
fn weekday_reads_numbers_and_names(
|
||||
#[case] written: serde_json::Value,
|
||||
#[case] expected: Option<Weekday>,
|
||||
) {
|
||||
use serde::Deserialize;
|
||||
|
||||
assert_eq!(Weekday::deserialize(&written).ok(), expected);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,19 @@
|
|||
## Validation
|
||||
## Layout
|
||||
|
||||
For `model_prices_and_context_window.json` validation, we should eventually:
|
||||
`wire/` reads one entry as written. `EntryWire::read` makes one pass over the entry's keys and puts every non-null key in exactly one place, which is an attribute, a flat price parsed by `FlatKey::parse`, a price block, a `supports_*` capability or `unknown`. A known key with a bad value becomes an `EntryProblem` that names the key, and the rest of the entry is still read
|
||||
|
||||
- Stop skipping this crate's tests
|
||||
`lower.rs` is the only code that knows both the wire types and `litellm-cost`. It lowers an `EntryWire` into `Prices`, `Adjustments` and `Model` and checks the rules that span keys, such as one price per meter and condition, contiguous tiers, no suffix inside a block, and off-peak needing hours and prices. Lowering only goes one way. Nothing turns `Prices` back into JSON, since the raw fields are kept
|
||||
|
||||
`catalog.rs` reads the document, indexes aliases and runs the checks across entries. A bad entry keeps its raw fields and its problems are listed in `Catalog::problems`. Deciding what to do with them is the caller's job
|
||||
|
||||
## Prices
|
||||
|
||||
The price vocabulary lives in `litellm-cost`, and only this crate knows JSON key names. Every price is keyed by `Meter`, `Channel` and `When`. A variation only one meter has stays in the meter (cache TTL, image size, resolution, `_above_Ns_interval`, `max_results_range`). Service tier and prompt band make up `When`. Batch and decisions are a `Channel` because Python bills them with their own rules. Off-peak is an overlay on `Prices`, and regional uplifts are `Adjustments`, never prices
|
||||
|
||||
A new `N`, image size or resolution in an existing key shape parses with no Rust change. A new unit, token kind, feature or tier needs a variant in `litellm-cost` plus its spelling in `wire/flat_key.rs` and `FLAT_KEY_PATTERN` in `schema.rs`
|
||||
|
||||
`Prices::band` and `Prices::rate` are pinned to Python's `_get_token_base_cost` by `tests/price_lookup_reference.json`. After a billing change in Python, regenerate it from the repo root with `PYTHONPATH=. python litellm-rust/crates/model-catalog/tests/generate_price_lookup_reference.py`
|
||||
|
||||
## Tests
|
||||
|
||||
Tests that read the checked-in `model_prices_and_context_window.json` are `#[ignore]` until a host serves the catalog. Fixture tests always run
|
||||
|
|
|
|||
|
|
@ -6,15 +6,18 @@ license.workspace = true
|
|||
repository.workspace = true
|
||||
|
||||
[features]
|
||||
schema = ["dep:schemars", "litellm-llms-types/schema"]
|
||||
schema = ["dep:schemars", "litellm-cost/schema"]
|
||||
|
||||
[dependencies]
|
||||
litellm-llms-types.workspace = true
|
||||
litellm-cost = { workspace = true, features = ["serde"] }
|
||||
|
||||
indexmap = { version = "2.14.0", features = ["serde"] }
|
||||
jiff = { version = "0.2.37", default-features = false, features = ["std", "tzdb-bundle-always"] }
|
||||
macro_rules_attribute.workspace = true
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_with.workspace = true
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
|
||||
|
|
|
|||
|
|
@ -1,67 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Primary API surface / task type of the model.
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Mode {
|
||||
AudioSpeech,
|
||||
AudioTranscription,
|
||||
Chat,
|
||||
Completion,
|
||||
Decisions,
|
||||
Embedding,
|
||||
Evaluation,
|
||||
Guardrail,
|
||||
ImageEdit,
|
||||
ImageGeneration,
|
||||
Moderation,
|
||||
Ocr,
|
||||
Realtime,
|
||||
Rerank,
|
||||
Responses,
|
||||
Search,
|
||||
VectorStore,
|
||||
VideoGeneration,
|
||||
}
|
||||
|
||||
/// Gemini audio generation API the model is served through.
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum VertexAiAudioApi {
|
||||
LyriaPredict,
|
||||
LyriaInteractions,
|
||||
}
|
||||
|
||||
/// Audio container format the model can return.
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AudioFormat {
|
||||
Mp3,
|
||||
Wav,
|
||||
}
|
||||
|
||||
/// Input modality the model accepts.
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum InputModality {
|
||||
Text,
|
||||
Image,
|
||||
Audio,
|
||||
Video,
|
||||
}
|
||||
|
||||
/// Output modality the model can produce.
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum OutputModality {
|
||||
Text,
|
||||
Image,
|
||||
Audio,
|
||||
Video,
|
||||
Code,
|
||||
}
|
||||
|
|
@ -1,50 +1,24 @@
|
|||
use crate::error::Error;
|
||||
use crate::fallback::{FallbackGeneralizations, FallbackRule};
|
||||
use crate::model_info::ModelInfo;
|
||||
use indexmap::IndexMap;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Map, Value};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Provenance {
|
||||
pub source: Option<String>,
|
||||
pub revision: Option<String>,
|
||||
pub etag: Option<String>,
|
||||
}
|
||||
use indexmap::IndexMap;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::error::{IntegrityViolation, InvalidDocument, Problem};
|
||||
use crate::fallback::{FallbackGeneralizations, FallbackRule};
|
||||
use crate::model::Model;
|
||||
use crate::wire::{EntryWire, Fields};
|
||||
|
||||
/// How far a registry may shrink against the one it replaces.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct IntegrityLimits {
|
||||
pub reference_model_count: usize,
|
||||
pub min_model_count: usize,
|
||||
pub min_reference_ratio: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum AliasIssue {
|
||||
InvalidList { model: String },
|
||||
InvalidName { model: String },
|
||||
CanonicalCollision { model: String, alias: String },
|
||||
AliasCollision { model: String, alias: String },
|
||||
min_model_count: usize,
|
||||
min_reference_ratio: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ModelEntry {
|
||||
fields: Map<String, Value>,
|
||||
info: ModelInfo,
|
||||
}
|
||||
|
||||
impl ModelEntry {
|
||||
pub fn field(&self, name: &str) -> Option<&Value> {
|
||||
self.fields.get(name)
|
||||
}
|
||||
pub fn fields(&self) -> &Map<String, Value> {
|
||||
&self.fields
|
||||
}
|
||||
/// The entry deserialized into the typed mirror of the catalog schema.
|
||||
pub fn info(&self) -> &ModelInfo {
|
||||
&self.info
|
||||
}
|
||||
fields: Fields,
|
||||
model: Option<Model>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
|
|
@ -54,6 +28,8 @@ pub struct ModelMatch<'a> {
|
|||
pub entry: &'a ModelEntry,
|
||||
}
|
||||
|
||||
/// A registry document read entry by entry. A bad entry keeps its raw fields and its problems are
|
||||
/// listed, so the caller decides what to serve.
|
||||
#[derive(Debug)]
|
||||
pub struct Catalog {
|
||||
entries: IndexMap<String, ModelEntry>,
|
||||
|
|
@ -61,67 +37,82 @@ pub struct Catalog {
|
|||
lowercase_keys: HashMap<String, String>,
|
||||
sample_spec: Option<Value>,
|
||||
fallback_generalizations: Option<FallbackGeneralizations>,
|
||||
provenance: Provenance,
|
||||
alias_issues: Vec<AliasIssue>,
|
||||
problems: Vec<Problem>,
|
||||
}
|
||||
|
||||
impl IntegrityLimits {
|
||||
pub fn new(min_model_count: usize, min_reference_ratio: f64) -> Option<Self> {
|
||||
(0.0..=1.0).contains(&min_reference_ratio).then_some(Self {
|
||||
min_model_count,
|
||||
min_reference_ratio,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ModelEntry {
|
||||
pub fn field(&self, name: &str) -> Option<&Value> {
|
||||
self.fields.get(name)
|
||||
}
|
||||
pub fn fields(&self) -> &Fields {
|
||||
&self.fields
|
||||
}
|
||||
pub fn model(&self) -> Option<&Model> {
|
||||
self.model.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl Catalog {
|
||||
pub fn parse(body: &[u8], provenance: Provenance) -> Result<Self, Error> {
|
||||
let root: IndexMap<String, Value> = serde_json::from_slice(body)?;
|
||||
if root.is_empty() {
|
||||
return Err(Error::Empty);
|
||||
pub fn parse(body: &[u8]) -> Result<Self, InvalidDocument> {
|
||||
let mut document: IndexMap<String, Value> = serde_json::from_slice(body)?;
|
||||
let sample_spec = document.shift_remove("sample_spec");
|
||||
let fallback_generalizations = document
|
||||
.shift_remove("fallback_generalizations")
|
||||
.map(serde_json::from_value)
|
||||
.transpose()?;
|
||||
if document.is_empty() {
|
||||
return Err(InvalidDocument::Empty);
|
||||
}
|
||||
|
||||
let mut entries = IndexMap::with_capacity(root.len());
|
||||
let mut problems = Vec::new();
|
||||
let mut entries = IndexMap::with_capacity(document.len());
|
||||
let mut alias_lists = Vec::new();
|
||||
let mut alias_issues = Vec::new();
|
||||
let mut sample_spec = None;
|
||||
let mut fallback_generalizations = None;
|
||||
for (name, value) in root {
|
||||
match name.as_str() {
|
||||
"sample_spec" => {
|
||||
sample_spec = Some(value);
|
||||
continue;
|
||||
}
|
||||
"fallback_generalizations" => {
|
||||
fallback_generalizations =
|
||||
Some(serde_json::from_value::<FallbackGeneralizations>(value)?);
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let Value::Object(mut fields) = value else {
|
||||
return Err(Error::EntryNotObject { model: name });
|
||||
for (name, value) in document {
|
||||
let Value::Object(fields) = value else {
|
||||
problems.push(Problem::NotAnObject { model: name });
|
||||
continue;
|
||||
};
|
||||
let info = ModelInfo::deserialize(&fields)?;
|
||||
if let Some(aliases) = fields.remove("aliases") {
|
||||
match aliases {
|
||||
Value::Array(names) => alias_lists.push((name.clone(), names)),
|
||||
_ => alias_issues.push(AliasIssue::InvalidList {
|
||||
model: name.clone(),
|
||||
}),
|
||||
}
|
||||
let wire = EntryWire::read(&fields);
|
||||
if let Some(aliases) = wire.attributes.aliases.clone() {
|
||||
alias_lists.push((name.clone(), aliases));
|
||||
}
|
||||
entries.insert(name, ModelEntry { fields, info });
|
||||
let model = Model::try_from(wire)
|
||||
.map_err(|found| {
|
||||
problems.extend(found.into_iter().map(|problem| Problem::Entry {
|
||||
model: name.clone(),
|
||||
problem,
|
||||
}))
|
||||
})
|
||||
.ok();
|
||||
problems.extend(
|
||||
model
|
||||
.as_ref()
|
||||
.and_then(|model| unknown_timezone(&name, model)),
|
||||
);
|
||||
entries.insert(name, ModelEntry { fields, model });
|
||||
}
|
||||
|
||||
let mut aliases = IndexMap::new();
|
||||
let mut aliases: IndexMap<String, String> = IndexMap::new();
|
||||
for (model, names) in alias_lists {
|
||||
for name in names {
|
||||
let Value::String(alias) = name else {
|
||||
alias_issues.push(AliasIssue::InvalidName {
|
||||
model: model.clone(),
|
||||
});
|
||||
continue;
|
||||
};
|
||||
for alias in names {
|
||||
if entries.contains_key(&alias) {
|
||||
alias_issues.push(AliasIssue::CanonicalCollision {
|
||||
problems.push(Problem::AliasShadowsModel {
|
||||
model: model.clone(),
|
||||
alias,
|
||||
});
|
||||
} else if aliases.contains_key(&alias) {
|
||||
alias_issues.push(AliasIssue::AliasCollision {
|
||||
} else if let Some(taken_by) = aliases.get(&alias) {
|
||||
problems.push(Problem::AliasTaken {
|
||||
model: model.clone(),
|
||||
taken_by: taken_by.clone(),
|
||||
alias,
|
||||
});
|
||||
} else {
|
||||
|
|
@ -141,36 +132,48 @@ impl Catalog {
|
|||
lowercase_keys,
|
||||
sample_spec,
|
||||
fallback_generalizations,
|
||||
provenance,
|
||||
alias_issues,
|
||||
problems,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn validate(&self, limits: IntegrityLimits) -> Result<(), Error> {
|
||||
if !limits.min_reference_ratio.is_finite()
|
||||
|| !(0.0..=1.0).contains(&limits.min_reference_ratio)
|
||||
{
|
||||
return Err(Error::InvalidRatio);
|
||||
}
|
||||
let actual = self.entries.len();
|
||||
/// Whether this catalog is big enough to replace one of `reference` models.
|
||||
pub fn check_size(
|
||||
&self,
|
||||
reference: usize,
|
||||
limits: IntegrityLimits,
|
||||
) -> Result<(), IntegrityViolation> {
|
||||
let actual = self.model_count();
|
||||
if actual < limits.min_model_count {
|
||||
return Err(Error::BelowMinimum {
|
||||
return Err(IntegrityViolation::BelowMinimum {
|
||||
actual,
|
||||
minimum: limits.min_model_count,
|
||||
});
|
||||
}
|
||||
if limits.reference_model_count > 0
|
||||
&& (actual as f64) < (limits.reference_model_count as f64) * limits.min_reference_ratio
|
||||
{
|
||||
return Err(Error::Shrunk {
|
||||
if (actual as f64) < (reference as f64) * limits.min_reference_ratio {
|
||||
return Err(IntegrityViolation::Shrunk {
|
||||
actual,
|
||||
reference: limits.reference_model_count,
|
||||
reference,
|
||||
ratio: limits.min_reference_ratio,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn problems(&self) -> &[Problem] {
|
||||
&self.problems
|
||||
}
|
||||
|
||||
/// Every `(model, key)` no type of this build reads.
|
||||
pub fn unrecognized(&self) -> impl Iterator<Item = (&str, &str)> {
|
||||
self.entries.iter().flat_map(|(name, entry)| {
|
||||
entry
|
||||
.model
|
||||
.iter()
|
||||
.flat_map(|model| &model.unrecognized)
|
||||
.map(move |key| (name.as_str(), key.as_str()))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn lookup(&self, key: &str) -> Option<ModelMatch<'_>> {
|
||||
let matched_key = if self.entries.contains_key(key) || self.aliases.contains_key(key) {
|
||||
key
|
||||
|
|
@ -205,25 +208,27 @@ impl Catalog {
|
|||
pub fn model_names(&self) -> impl Iterator<Item = &str> {
|
||||
self.entries.keys().map(String::as_str)
|
||||
}
|
||||
pub fn alias_count(&self) -> usize {
|
||||
self.aliases.len()
|
||||
}
|
||||
pub fn aliases(&self) -> &IndexMap<String, String> {
|
||||
&self.aliases
|
||||
}
|
||||
pub fn alias_issues(&self) -> &[AliasIssue] {
|
||||
&self.alias_issues
|
||||
}
|
||||
pub fn sample_spec(&self) -> Option<&Value> {
|
||||
self.sample_spec.as_ref()
|
||||
}
|
||||
pub fn fallback_generalizations(&self) -> Option<&FallbackGeneralizations> {
|
||||
self.fallback_generalizations.as_ref()
|
||||
}
|
||||
pub fn fallback_rules(&self) -> Option<&[FallbackRule]> {
|
||||
Some(self.fallback_generalizations.as_ref()?.rules.as_slice())
|
||||
}
|
||||
pub fn provenance(&self) -> &Provenance {
|
||||
&self.provenance
|
||||
}
|
||||
}
|
||||
|
||||
fn unknown_timezone(name: &str, model: &Model) -> Option<Problem> {
|
||||
let zone = model
|
||||
.prices
|
||||
.off_peak()?
|
||||
.schedule()
|
||||
.weekday_timezone
|
||||
.as_deref()?
|
||||
.trim();
|
||||
(!zone.is_empty() && jiff::tz::TimeZone::get(zone).is_err()).then(|| Problem::UnknownTimezone {
|
||||
model: name.to_owned(),
|
||||
zone: zone.to_owned(),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,95 @@
|
|||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
/// The body is not valid JSON, or a model entry fails typed deserialization.
|
||||
pub enum InvalidDocument {
|
||||
#[error("invalid JSON: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
/// The catalog has no entries at all.
|
||||
#[error("catalog is empty")]
|
||||
#[error("catalog has no models")]
|
||||
Empty,
|
||||
/// A non-reserved top level value is not a JSON object.
|
||||
#[error("model {model:?} must be an object")]
|
||||
EntryNotObject { model: String },
|
||||
/// Canonical entry count is under the configured minimum.
|
||||
}
|
||||
|
||||
#[derive(Debug, Error, PartialEq)]
|
||||
pub enum IntegrityViolation {
|
||||
#[error("catalog has {actual} models, below minimum {minimum}")]
|
||||
BelowMinimum { actual: usize, minimum: usize },
|
||||
/// Canonical entry count is under the configured reference ratio.
|
||||
#[error("catalog has {actual} models, below {ratio} of reference count {reference}")]
|
||||
Shrunk {
|
||||
actual: usize,
|
||||
reference: usize,
|
||||
ratio: f64,
|
||||
},
|
||||
/// The configured minimum reference ratio is not finite or outside `[0, 1]`.
|
||||
#[error("minimum reference ratio must be finite and between zero and one")]
|
||||
InvalidRatio,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Problem {
|
||||
#[error("{model} must be a JSON object")]
|
||||
NotAnObject { model: String },
|
||||
#[error("{model}.{problem}")]
|
||||
Entry {
|
||||
model: String,
|
||||
#[source]
|
||||
problem: EntryProblem,
|
||||
},
|
||||
#[error("{model}.off_peak_pricing.weekday_timezone: {zone:?} is not an IANA time zone")]
|
||||
UnknownTimezone { model: String, zone: String },
|
||||
#[error("{model}: alias {alias:?} is already a model name")]
|
||||
AliasShadowsModel { model: String, alias: String },
|
||||
#[error("{model}: alias {alias:?} already belongs to {taken_by}")]
|
||||
AliasTaken {
|
||||
model: String,
|
||||
alias: String,
|
||||
taken_by: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum EntryProblem {
|
||||
#[error("{key}: {error}")]
|
||||
Invalid {
|
||||
key: String,
|
||||
#[source]
|
||||
error: serde_json::Error,
|
||||
},
|
||||
#[error("{key} is not a field this block reads")]
|
||||
UnknownBlockKey { key: String },
|
||||
#[error("litellm_provider is required")]
|
||||
MissingProvider,
|
||||
#[error(transparent)]
|
||||
Price(#[from] PriceProblem),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error, PartialEq, Eq)]
|
||||
pub enum PriceProblem {
|
||||
#[error("{first} and {second} price the same meter under the same conditions")]
|
||||
Duplicate { first: String, second: String },
|
||||
#[error("{key}: a price inside {block} takes no band or tier suffix")]
|
||||
SuffixInsideBlock { key: String, block: &'static str },
|
||||
#[error("{row}: a tier needs exactly one of range and max_results_range")]
|
||||
TierRange { row: String },
|
||||
#[error("{row}: the range ends below its start")]
|
||||
TierReversed { row: String },
|
||||
#[error("{row}: starts at {start}, but the tiers before it end at {expected}")]
|
||||
TierGap {
|
||||
row: String,
|
||||
start: u64,
|
||||
expected: u64,
|
||||
},
|
||||
#[error("tiered_pricing mixes token ranges and result ranges")]
|
||||
MixedTierBasis,
|
||||
#[error("{key}: a tier by result count only prices input_cost_per_query")]
|
||||
NotPricedByResults { key: String },
|
||||
#[error("tiered_pricing and {key} both set prompt token bands")]
|
||||
BandsFromTwoSources { key: String },
|
||||
#[error("{key} has no off-peak rate")]
|
||||
NotAnOffPeakRate { key: String },
|
||||
#[error("off_peak_pricing has no prices")]
|
||||
NoOffPeakPrices,
|
||||
#[error("off_peak_pricing needs hours_utc or windows")]
|
||||
NoOffPeakHours,
|
||||
#[error("{key} must list at least one entry")]
|
||||
EmptyList { key: String },
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Error, PartialEq, Eq, Hash)]
|
||||
#[error("expected a calendar date YYYY-MM-DD")]
|
||||
pub struct InvalidDate;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// One regex rule generalizing unknown model ids to known families.
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[macro_rules_attribute::apply(crate::wire_type)]
|
||||
#[derive(Eq, Hash)]
|
||||
pub struct FallbackRule {
|
||||
pub name: String,
|
||||
pub pattern: String,
|
||||
|
|
@ -15,8 +14,8 @@ pub struct FallbackRule {
|
|||
}
|
||||
|
||||
/// Regex rules that generalize unknown model ids to known families; not a model entry.
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[macro_rules_attribute::apply(crate::wire_type)]
|
||||
#[derive(Eq, Hash)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct FallbackGeneralizations {
|
||||
pub rules: Vec<FallbackRule>,
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
mod capabilities;
|
||||
macro_rules_attribute::attribute_alias! {
|
||||
#[apply(wire_type)] =
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))];
|
||||
}
|
||||
|
||||
mod catalog;
|
||||
mod error;
|
||||
mod fallback;
|
||||
mod model_info;
|
||||
mod pricing;
|
||||
mod validation;
|
||||
mod lower;
|
||||
mod model;
|
||||
mod vocabulary;
|
||||
pub mod wire;
|
||||
|
||||
pub use capabilities::*;
|
||||
pub use catalog::*;
|
||||
pub use error::*;
|
||||
pub use fallback::*;
|
||||
pub use model_info::*;
|
||||
pub use pricing::*;
|
||||
pub use validation::*;
|
||||
pub use model::*;
|
||||
pub use vocabulary::*;
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
mod schema;
|
||||
|
|
|
|||
700
litellm-rust/crates/model-catalog/src/lower.rs
Normal file
700
litellm-rust/crates/model-catalog/src/lower.rs
Normal file
|
|
@ -0,0 +1,700 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::collections::btree_map::Entry;
|
||||
use std::num::NonZeroU64;
|
||||
|
||||
use litellm_cost::money::UnitPrice;
|
||||
use litellm_cost::price::{
|
||||
Adjustments, Band, CacheReadMode, CacheTtl, GuardrailUnit, InputUnit, Meter, OffPeak,
|
||||
OffPeakRates, OutputUnit, Price, Prices, Residency, SearchContextSize, TokenKind, Uplift, When,
|
||||
};
|
||||
use litellm_cost::schedule::{Schedule, Window};
|
||||
|
||||
use crate::error::{EntryProblem, PriceProblem};
|
||||
use crate::model::Model;
|
||||
use crate::wire::{EntryWire, FlatPrice, OffPeakBlock, TierRow};
|
||||
|
||||
impl TryFrom<&EntryWire> for Prices {
|
||||
type Error = Vec<PriceProblem>;
|
||||
|
||||
fn try_from(wire: &EntryWire) -> Result<Self, Self::Error> {
|
||||
let mut problems = Vec::new();
|
||||
let sourced = flat_prices(&wire.prices)
|
||||
.chain(tier_prices(wire, &mut problems))
|
||||
.chain(search_context_prices(wire))
|
||||
.chain(guardrail_prices(wire));
|
||||
let rates = dedupe(sourced, &mut problems);
|
||||
let off_peak = wire
|
||||
.off_peak
|
||||
.as_ref()
|
||||
.and_then(|block| off_peak(block, &mut problems));
|
||||
if problems.is_empty() {
|
||||
Ok(Prices::new(rates, off_peak))
|
||||
} else {
|
||||
Err(problems)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&EntryWire> for Adjustments {
|
||||
fn from(wire: &EntryWire) -> Self {
|
||||
Adjustments::new(
|
||||
[
|
||||
(Uplift::NonGlobalEndpoint, wire.regional_endpoint_uplift),
|
||||
(Uplift::Residency(Residency::Eu), wire.residency_uplift_eu),
|
||||
(Uplift::Residency(Residency::Us), wire.residency_uplift_us),
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|(uplift, factor)| Some((uplift, factor?))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<EntryWire> for Model {
|
||||
type Error = Vec<EntryProblem>;
|
||||
|
||||
fn try_from(wire: EntryWire) -> Result<Self, Self::Error> {
|
||||
let prices = Prices::try_from(&wire);
|
||||
let adjustments = Adjustments::from(&wire);
|
||||
let EntryWire {
|
||||
attributes,
|
||||
capabilities,
|
||||
unknown,
|
||||
problems,
|
||||
..
|
||||
} = wire;
|
||||
let missing_provider = attributes
|
||||
.litellm_provider
|
||||
.is_none()
|
||||
.then_some(EntryProblem::MissingProvider);
|
||||
match prices {
|
||||
Ok(prices) if problems.is_empty() && missing_provider.is_none() => Ok(Model {
|
||||
attributes,
|
||||
prices,
|
||||
adjustments,
|
||||
capabilities,
|
||||
unrecognized: unknown.into_iter().map(|(key, _)| key).collect(),
|
||||
}),
|
||||
prices => Err(problems
|
||||
.into_iter()
|
||||
.chain(missing_provider)
|
||||
.chain(prices.err().into_iter().flatten().map(EntryProblem::Price))
|
||||
.collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Sourced {
|
||||
price: Price,
|
||||
rate: UnitPrice,
|
||||
source: String,
|
||||
}
|
||||
|
||||
fn flat_prices(prices: &[FlatPrice]) -> impl Iterator<Item = Sourced> + '_ {
|
||||
prices.iter().map(|flat| {
|
||||
let (channel, when) = flat.flat.channel_and_when();
|
||||
Sourced {
|
||||
price: Price {
|
||||
meter: flat.flat.meter.clone(),
|
||||
channel,
|
||||
when,
|
||||
},
|
||||
rate: flat.price,
|
||||
source: flat.key.clone(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn dedupe(
|
||||
sourced: impl Iterator<Item = Sourced>,
|
||||
problems: &mut Vec<PriceProblem>,
|
||||
) -> BTreeMap<Price, UnitPrice> {
|
||||
let mut seen: BTreeMap<Price, (UnitPrice, String)> = BTreeMap::new();
|
||||
for Sourced {
|
||||
price,
|
||||
rate,
|
||||
source,
|
||||
} in sourced
|
||||
{
|
||||
match seen.entry(price) {
|
||||
Entry::Occupied(first) => problems.push(PriceProblem::Duplicate {
|
||||
first: first.get().1.clone(),
|
||||
second: source,
|
||||
}),
|
||||
Entry::Vacant(slot) => {
|
||||
slot.insert((rate, source));
|
||||
}
|
||||
}
|
||||
}
|
||||
seen.into_iter()
|
||||
.map(|(price, (rate, _))| (price, rate))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum TierBasis {
|
||||
PromptTokens,
|
||||
Results,
|
||||
}
|
||||
|
||||
fn tier_basis(row: &TierRow) -> Result<(TierBasis, [u64; 2]), PriceProblem> {
|
||||
match (row.range, row.max_results_range) {
|
||||
(Some(range), None) => Ok((TierBasis::PromptTokens, range)),
|
||||
(None, Some(range)) => Ok((TierBasis::Results, range)),
|
||||
_ => Err(PriceProblem::TierRange {
|
||||
row: row.path.clone(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn tier_prices(wire: &EntryWire, problems: &mut Vec<PriceProblem>) -> Vec<Sourced> {
|
||||
let Some(rows) = wire.tier_rows.as_deref().filter(|rows| !rows.is_empty()) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let based: Vec<_> = rows
|
||||
.iter()
|
||||
.filter_map(|row| match tier_basis(row) {
|
||||
Ok((basis, range)) => Some((basis, range, row)),
|
||||
Err(problem) => {
|
||||
problems.push(problem);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let Some(&(basis, ..)) = based.first() else {
|
||||
return Vec::new();
|
||||
};
|
||||
if based.iter().any(|(other, ..)| *other != basis) {
|
||||
problems.push(PriceProblem::MixedTierBasis);
|
||||
return Vec::new();
|
||||
}
|
||||
if basis == TierBasis::PromptTokens {
|
||||
problems.extend(
|
||||
wire.prices
|
||||
.iter()
|
||||
.filter(|flat| flat.flat.band != Band::Base)
|
||||
.map(|flat| PriceProblem::BandsFromTwoSources {
|
||||
key: flat.key.clone(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
let mut ordered = based;
|
||||
ordered.sort_by_key(|(_, [start, _], _)| *start);
|
||||
problems.extend(ladder_problems(basis, &ordered));
|
||||
ordered
|
||||
.iter()
|
||||
.flat_map(|(basis, [start, _], row)| {
|
||||
row.prices.iter().map(move |flat| (*basis, *start, flat))
|
||||
})
|
||||
.filter_map(
|
||||
|(basis, start, flat)| match tier_price(basis, start, flat) {
|
||||
Ok(sourced) => Some(sourced),
|
||||
Err(problem) => {
|
||||
problems.push(problem);
|
||||
None
|
||||
}
|
||||
},
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn ladder_problems(
|
||||
basis: TierBasis,
|
||||
ordered: &[(TierBasis, [u64; 2], &TierRow)],
|
||||
) -> Vec<PriceProblem> {
|
||||
let step = match basis {
|
||||
TierBasis::PromptTokens => 0,
|
||||
TierBasis::Results => 1,
|
||||
};
|
||||
let reversed = ordered
|
||||
.iter()
|
||||
.filter(|(_, [start, end], _)| end < start)
|
||||
.map(|(.., row)| PriceProblem::TierReversed {
|
||||
row: row.path.clone(),
|
||||
});
|
||||
let first = ordered
|
||||
.first()
|
||||
.filter(|(_, [start, _], _)| *start > step)
|
||||
.map(|(_, [start, _], row)| PriceProblem::TierGap {
|
||||
row: row.path.clone(),
|
||||
start: *start,
|
||||
expected: 0,
|
||||
});
|
||||
let gaps = ordered.windows(2).filter_map(|pair| {
|
||||
let (_, [_, end], _) = pair[0];
|
||||
let (_, [start, _], row) = pair[1];
|
||||
(end.checked_add(step) != Some(start)).then(|| PriceProblem::TierGap {
|
||||
row: row.path.clone(),
|
||||
start,
|
||||
expected: end,
|
||||
})
|
||||
});
|
||||
reversed.chain(first).chain(gaps).collect()
|
||||
}
|
||||
|
||||
fn tier_price(basis: TierBasis, start: u64, flat: &FlatPrice) -> Result<Sourced, PriceProblem> {
|
||||
if flat.flat.has_suffix() {
|
||||
return Err(PriceProblem::SuffixInsideBlock {
|
||||
key: flat.key.clone(),
|
||||
block: "tiered_pricing",
|
||||
});
|
||||
}
|
||||
let (meter, band) = match (basis, &flat.flat.meter) {
|
||||
(TierBasis::PromptTokens, meter) => (meter.clone(), Band::over_prompt_tokens(start)),
|
||||
(TierBasis::Results, Meter::Input(InputUnit::Query { over_results: None })) => (
|
||||
Meter::Input(InputUnit::Query {
|
||||
over_results: NonZeroU64::new(start.saturating_sub(1)),
|
||||
}),
|
||||
Band::Base,
|
||||
),
|
||||
(TierBasis::Results, _) => {
|
||||
return Err(PriceProblem::NotPricedByResults {
|
||||
key: flat.key.clone(),
|
||||
});
|
||||
}
|
||||
};
|
||||
Ok(Sourced {
|
||||
price: Price::online(
|
||||
meter,
|
||||
When {
|
||||
band,
|
||||
..When::default()
|
||||
},
|
||||
),
|
||||
rate: flat.price,
|
||||
source: flat.key.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn search_context_prices(wire: &EntryWire) -> impl Iterator<Item = Sourced> + '_ {
|
||||
wire.search_context.iter().flat_map(|costs| {
|
||||
[
|
||||
(SearchContextSize::Low, "low", costs.search_context_size_low),
|
||||
(
|
||||
SearchContextSize::Medium,
|
||||
"medium",
|
||||
costs.search_context_size_medium,
|
||||
),
|
||||
(
|
||||
SearchContextSize::High,
|
||||
"high",
|
||||
costs.search_context_size_high,
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|(size, name, rate)| {
|
||||
Some(Sourced {
|
||||
price: Price::online(Meter::WebSearch(size), When::default()),
|
||||
rate: rate?,
|
||||
source: format!("search_context_cost_per_query.search_context_size_{name}"),
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn guardrail_prices(wire: &EntryWire) -> impl Iterator<Item = Sourced> + '_ {
|
||||
wire.guardrail.iter().flatten().map(|(unit, rate)| Sourced {
|
||||
price: Price::online(Meter::Guardrail(GuardrailUnit::new(unit)), When::default()),
|
||||
rate: *rate,
|
||||
source: format!("guardrail_cost_per_unit.{unit}"),
|
||||
})
|
||||
}
|
||||
|
||||
fn off_peak(block: &OffPeakBlock, problems: &mut Vec<PriceProblem>) -> Option<OffPeak> {
|
||||
let rates = off_peak_rates(&block.prices, problems);
|
||||
let schedule = schedule(block, problems)?;
|
||||
let off_peak = OffPeak::new(schedule, rates);
|
||||
if off_peak.is_none() {
|
||||
problems.push(PriceProblem::NoOffPeakPrices);
|
||||
}
|
||||
off_peak
|
||||
}
|
||||
|
||||
fn off_peak_rates(prices: &[FlatPrice], problems: &mut Vec<PriceProblem>) -> OffPeakRates {
|
||||
let mut rates = OffPeakRates::default();
|
||||
for flat in prices {
|
||||
if flat.flat.has_suffix() {
|
||||
problems.push(PriceProblem::SuffixInsideBlock {
|
||||
key: flat.key.clone(),
|
||||
block: "off_peak_pricing",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
let slot = match flat.flat.meter {
|
||||
Meter::Input(InputUnit::Token(TokenKind::Text)) => &mut rates.input,
|
||||
Meter::Output(OutputUnit::Token(TokenKind::Text)) => &mut rates.output,
|
||||
Meter::Output(OutputUnit::ReasoningToken) => &mut rates.reasoning,
|
||||
Meter::CacheRead {
|
||||
kind: TokenKind::Text,
|
||||
mode: CacheReadMode::Implicit,
|
||||
} => &mut rates.cache_read,
|
||||
Meter::CacheRead {
|
||||
kind: TokenKind::Text,
|
||||
mode: CacheReadMode::Explicit,
|
||||
} => &mut rates.explicit_cache_read,
|
||||
Meter::CacheWrite {
|
||||
kind: TokenKind::Text,
|
||||
ttl: CacheTtl::Default,
|
||||
} => &mut rates.cache_write,
|
||||
_ => {
|
||||
problems.push(PriceProblem::NotAnOffPeakRate {
|
||||
key: flat.key.clone(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
};
|
||||
*slot = Some(flat.price);
|
||||
}
|
||||
rates
|
||||
}
|
||||
|
||||
fn schedule(block: &OffPeakBlock, problems: &mut Vec<PriceProblem>) -> Option<Schedule> {
|
||||
let empty = |key: String| PriceProblem::EmptyList { key };
|
||||
let every_day = block
|
||||
.hours_utc
|
||||
.iter()
|
||||
.map(|hours| ("off_peak_pricing.hours_utc".to_owned(), hours, None));
|
||||
let windows = block
|
||||
.windows
|
||||
.iter()
|
||||
.flatten()
|
||||
.enumerate()
|
||||
.map(|(index, window)| {
|
||||
(
|
||||
format!("off_peak_pricing.windows[{index}].hours_utc"),
|
||||
&window.hours_utc,
|
||||
Some((index, window.weekdays.as_ref())),
|
||||
)
|
||||
});
|
||||
let windows: Vec<Window> = every_day
|
||||
.chain(windows)
|
||||
.map(|(key, hours, weekdays)| {
|
||||
if hours.windows().is_empty() {
|
||||
problems.push(empty(key));
|
||||
}
|
||||
if let Some((index, Some(days))) = weekdays
|
||||
&& days.is_empty()
|
||||
{
|
||||
problems.push(empty(format!("off_peak_pricing.windows[{index}].weekdays")));
|
||||
}
|
||||
Window {
|
||||
hours: hours.windows().to_vec(),
|
||||
weekdays: weekdays.and_then(|(_, days)| days.cloned()),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if block.windows.as_ref().is_some_and(Vec::is_empty) {
|
||||
problems.push(empty("off_peak_pricing.windows".to_owned()));
|
||||
}
|
||||
if block.hours_utc.is_none() && block.windows.is_none() {
|
||||
problems.push(PriceProblem::NoOffPeakHours);
|
||||
return None;
|
||||
}
|
||||
Some(Schedule {
|
||||
windows,
|
||||
weekday_timezone: block.weekday_timezone.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use litellm_cost::money::Multiplier;
|
||||
use litellm_cost::price::{Channel, ServiceTier};
|
||||
use litellm_cost::schedule::{UtcWindow, Weekday};
|
||||
use rstest::rstest;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::*;
|
||||
|
||||
const TEXT_INPUT: Meter = Meter::Input(InputUnit::Token(TokenKind::Text));
|
||||
|
||||
fn wire(entry: Value) -> EntryWire {
|
||||
let Value::Object(fields) = entry else {
|
||||
panic!("entry must be an object")
|
||||
};
|
||||
let wire = EntryWire::read(&fields);
|
||||
assert!(wire.problems.is_empty(), "{:?}", wire.problems);
|
||||
wire
|
||||
}
|
||||
|
||||
fn prices(entry: Value) -> Result<Prices, Vec<PriceProblem>> {
|
||||
Prices::try_from(&wire(entry))
|
||||
}
|
||||
|
||||
fn online(meter: Meter, tier: ServiceTier, band: Band) -> Price {
|
||||
Price::online(meter, When { tier, band })
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::flat_key(json!({"input_cost_per_token": 1e-6}), online(TEXT_INPUT, ServiceTier::Standard, Band::Base))]
|
||||
#[case::band_and_tier(
|
||||
json!({"input_cost_per_token_above_272k_tokens_priority": 1e-6}),
|
||||
online(TEXT_INPUT, ServiceTier::Priority, Band::over_prompt_tokens(272_000))
|
||||
)]
|
||||
#[case::batch(
|
||||
json!({"input_cost_per_token_batches": 1e-6}),
|
||||
Price { meter: TEXT_INPUT, channel: Channel::Batch, when: When::default() }
|
||||
)]
|
||||
#[case::first_token_tier_is_the_base_band(
|
||||
json!({"tiered_pricing": [{"range": [0.0, 256000.0], "input_cost_per_token": 1e-6}]}),
|
||||
online(TEXT_INPUT, ServiceTier::Standard, Band::Base)
|
||||
)]
|
||||
#[case::result_tier_stays_in_its_meter(
|
||||
json!({"tiered_pricing": [
|
||||
{"max_results_range": [0, 25], "input_cost_per_query": 2e-6},
|
||||
{"max_results_range": [26, 100], "input_cost_per_query": 1e-6}
|
||||
]}),
|
||||
online(
|
||||
Meter::Input(InputUnit::Query { over_results: NonZeroU64::new(25) }),
|
||||
ServiceTier::Standard,
|
||||
Band::Base
|
||||
)
|
||||
)]
|
||||
#[case::search_context(
|
||||
json!({"search_context_cost_per_query": {"search_context_size_low": 1e-6}}),
|
||||
online(Meter::WebSearch(SearchContextSize::Low), ServiceTier::Standard, Band::Base)
|
||||
)]
|
||||
#[case::guardrail(
|
||||
json!({"guardrail_cost_per_unit": {"contentPolicyUnits": 1e-6}}),
|
||||
online(Meter::Guardrail(GuardrailUnit::new("contentPolicyUnits")), ServiceTier::Standard, Band::Base)
|
||||
)]
|
||||
fn every_spelling_lowers_to_one_price(#[case] entry: Value, #[case] expected: Price) {
|
||||
assert_eq!(prices(entry).unwrap().get(&expected), UnitPrice::new(1e-6));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn token_tiers_lower_to_prompt_bands_at_each_start() {
|
||||
let prices = prices(json!({"tiered_pricing": [
|
||||
{"range": [0, 256000], "input_cost_per_token": 1e-6},
|
||||
{"range": [256000, 1000000], "input_cost_per_token": 2e-6}
|
||||
]}))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
prices.band(ServiceTier::Standard, 256_001, false),
|
||||
Band::over_prompt_tokens(256_000)
|
||||
);
|
||||
assert_eq!(
|
||||
prices
|
||||
.rate(
|
||||
&TEXT_INPUT,
|
||||
ServiceTier::Standard,
|
||||
Band::over_prompt_tokens(256_000)
|
||||
)
|
||||
.map(UnitPrice::get),
|
||||
Some(2e-6)
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::two_spellings_of_one_price(
|
||||
json!({
|
||||
"input_cost_per_token": 1e-6,
|
||||
"tiered_pricing": [{"range": [0, 10], "input_cost_per_token": 2e-6}]
|
||||
}),
|
||||
PriceProblem::Duplicate {
|
||||
first: "input_cost_per_token".into(),
|
||||
second: "tiered_pricing[0].input_cost_per_token".into(),
|
||||
}
|
||||
)]
|
||||
#[case::zero_band_is_the_base_price(
|
||||
json!({"input_cost_per_token": 1e-6, "input_cost_per_token_above_0k_tokens": 2e-6}),
|
||||
PriceProblem::Duplicate {
|
||||
first: "input_cost_per_token".into(),
|
||||
second: "input_cost_per_token_above_0k_tokens".into(),
|
||||
}
|
||||
)]
|
||||
#[case::tier_without_range(
|
||||
json!({"tiered_pricing": [{"input_cost_per_token": 1e-6}]}),
|
||||
PriceProblem::TierRange { row: "tiered_pricing[0]".into() }
|
||||
)]
|
||||
#[case::tier_with_suffix(
|
||||
json!({"tiered_pricing": [{"range": [0, 10], "input_cost_per_token_priority": 1e-6}]}),
|
||||
PriceProblem::SuffixInsideBlock {
|
||||
key: "tiered_pricing[0].input_cost_per_token_priority".into(),
|
||||
block: "tiered_pricing",
|
||||
}
|
||||
)]
|
||||
#[case::reversed_range(
|
||||
json!({"tiered_pricing": [{"range": [0, 10], "input_cost_per_token": 1e-6}, {"range": [10, 5], "input_cost_per_token": 1e-6}]}),
|
||||
PriceProblem::TierReversed { row: "tiered_pricing[1]".into() }
|
||||
)]
|
||||
#[case::gap_between_tiers(
|
||||
json!({"tiered_pricing": [
|
||||
{"range": [0, 10], "input_cost_per_token": 1e-6},
|
||||
{"range": [20, 30], "input_cost_per_token": 2e-6}
|
||||
]}),
|
||||
PriceProblem::TierGap { row: "tiered_pricing[1]".into(), start: 20, expected: 10 }
|
||||
)]
|
||||
#[case::ladder_not_from_zero(
|
||||
json!({"tiered_pricing": [{"range": [5, 10], "input_cost_per_token": 1e-6}]}),
|
||||
PriceProblem::TierGap { row: "tiered_pricing[0]".into(), start: 5, expected: 0 }
|
||||
)]
|
||||
#[case::result_ladder_overlaps(
|
||||
json!({"tiered_pricing": [
|
||||
{"max_results_range": [1, 10], "input_cost_per_query": 1e-6},
|
||||
{"max_results_range": [10, 20], "input_cost_per_query": 2e-6}
|
||||
]}),
|
||||
PriceProblem::TierGap { row: "tiered_pricing[1]".into(), start: 10, expected: 10 }
|
||||
)]
|
||||
#[case::mixed_basis(
|
||||
json!({"tiered_pricing": [
|
||||
{"range": [0, 10], "input_cost_per_token": 1e-6},
|
||||
{"max_results_range": [11, 20], "input_cost_per_query": 2e-6}
|
||||
]}),
|
||||
PriceProblem::MixedTierBasis
|
||||
)]
|
||||
#[case::result_tier_prices_a_token(
|
||||
json!({"tiered_pricing": [{"max_results_range": [0, 10], "input_cost_per_token": 1e-6}]}),
|
||||
PriceProblem::NotPricedByResults { key: "tiered_pricing[0].input_cost_per_token".into() }
|
||||
)]
|
||||
#[case::two_band_sources(
|
||||
json!({
|
||||
"output_cost_per_token_above_200k_tokens": 1e-6,
|
||||
"tiered_pricing": [{"range": [0, 10], "input_cost_per_token": 1e-6}]
|
||||
}),
|
||||
PriceProblem::BandsFromTwoSources { key: "output_cost_per_token_above_200k_tokens".into() }
|
||||
)]
|
||||
#[case::off_peak_without_hours(
|
||||
json!({"off_peak_pricing": {"input_cost_per_token": 1e-6}}),
|
||||
PriceProblem::NoOffPeakHours
|
||||
)]
|
||||
#[case::off_peak_without_prices(
|
||||
json!({"off_peak_pricing": {"hours_utc": "00:00-01:00"}}),
|
||||
PriceProblem::NoOffPeakPrices
|
||||
)]
|
||||
#[case::off_peak_empty_windows(
|
||||
json!({"off_peak_pricing": {"windows": [], "input_cost_per_token": 1e-6}}),
|
||||
PriceProblem::EmptyList { key: "off_peak_pricing.windows".into() }
|
||||
)]
|
||||
#[case::off_peak_empty_hours(
|
||||
json!({"off_peak_pricing": {"hours_utc": [], "input_cost_per_token": 1e-6}}),
|
||||
PriceProblem::EmptyList { key: "off_peak_pricing.hours_utc".into() }
|
||||
)]
|
||||
#[case::off_peak_empty_weekdays(
|
||||
json!({"off_peak_pricing": {
|
||||
"windows": [{"hours_utc": "00:00-01:00", "weekdays": []}],
|
||||
"input_cost_per_token": 1e-6
|
||||
}}),
|
||||
PriceProblem::EmptyList { key: "off_peak_pricing.windows[0].weekdays".into() }
|
||||
)]
|
||||
#[case::off_peak_image_price(
|
||||
json!({"off_peak_pricing": {"hours_utc": "00:00-01:00", "output_cost_per_image": 1}}),
|
||||
PriceProblem::NotAnOffPeakRate { key: "off_peak_pricing.output_cost_per_image".into() }
|
||||
)]
|
||||
#[case::off_peak_band(
|
||||
json!({"off_peak_pricing": {"hours_utc": "00:00-01:00", "input_cost_per_token_above_200k_tokens": 1}}),
|
||||
PriceProblem::SuffixInsideBlock {
|
||||
key: "off_peak_pricing.input_cost_per_token_above_200k_tokens".into(),
|
||||
block: "off_peak_pricing",
|
||||
}
|
||||
)]
|
||||
fn rules_across_keys_name_where_each_price_came_from(
|
||||
#[case] entry: Value,
|
||||
#[case] expected: PriceProblem,
|
||||
) {
|
||||
let problems = prices(entry).unwrap_err();
|
||||
assert!(problems.contains(&expected), "{problems:?}");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn off_peak_hours_and_windows_form_one_schedule_with_its_rates() {
|
||||
let prices = prices(json!({"off_peak_pricing": {
|
||||
"hours_utc": ["00:00-01:00"],
|
||||
"windows": [{"hours_utc": "10:00-12:00", "weekdays": [6, " sun "]}],
|
||||
"weekday_timezone": "Asia/Shanghai",
|
||||
"output_cost_per_token": 1e-6,
|
||||
"explicit_cache_read_input_token_cost": 2e-6
|
||||
}}))
|
||||
.unwrap();
|
||||
let off_peak = prices.off_peak().unwrap();
|
||||
let window = |text: &str| text.parse::<UtcWindow>().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
off_peak.schedule(),
|
||||
&Schedule {
|
||||
windows: vec![
|
||||
Window {
|
||||
hours: vec![window("00:00-01:00")],
|
||||
weekdays: None,
|
||||
},
|
||||
Window {
|
||||
hours: vec![window("10:00-12:00")],
|
||||
weekdays: Some(vec![Weekday::Saturday, Weekday::Sunday]),
|
||||
},
|
||||
],
|
||||
weekday_timezone: Some("Asia/Shanghai".into()),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
off_peak.rates(),
|
||||
&OffPeakRates {
|
||||
output: UnitPrice::new(1e-6),
|
||||
explicit_cache_read: UnitPrice::new(2e-6),
|
||||
..OffPeakRates::default()
|
||||
}
|
||||
);
|
||||
assert_eq!(prices.iter().count(), 0);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn uplifts_are_adjustments_not_prices() {
|
||||
let wire = wire(json!({
|
||||
"regional_endpoint_uplift_multiplier": 1.1,
|
||||
"regional_processing_uplift_multiplier_eu": "1.2"
|
||||
}));
|
||||
let adjustments = Adjustments::from(&wire);
|
||||
|
||||
assert_eq!(Prices::try_from(&wire).unwrap().iter().count(), 0);
|
||||
assert_eq!(
|
||||
adjustments
|
||||
.get(Uplift::NonGlobalEndpoint)
|
||||
.map(Multiplier::get),
|
||||
Some(1.1)
|
||||
);
|
||||
assert_eq!(
|
||||
adjustments
|
||||
.get(Uplift::Residency(Residency::Eu))
|
||||
.map(Multiplier::get),
|
||||
Some(1.2)
|
||||
);
|
||||
assert_eq!(adjustments.get(Uplift::Residency(Residency::Us)), None);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn a_model_reports_field_provider_and_price_problems_together() {
|
||||
let Value::Object(fields) = json!({
|
||||
"mode": "chatt",
|
||||
"tiered_pricing": [{"input_cost_per_token": 1e-6}]
|
||||
}) else {
|
||||
unreachable!()
|
||||
};
|
||||
let problems: Vec<String> = Model::from_fields(&fields)
|
||||
.unwrap_err()
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect();
|
||||
|
||||
assert_eq!(problems.len(), 3, "{problems:?}");
|
||||
assert!(problems[0].starts_with("mode: unknown variant `chatt`"));
|
||||
assert_eq!(problems[1], "litellm_provider is required");
|
||||
assert_eq!(
|
||||
problems[2],
|
||||
"tiered_pricing[0]: a tier needs exactly one of range and max_results_range"
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn a_null_value_is_absent() {
|
||||
let Value::Object(fields) = json!({
|
||||
"litellm_provider": "openai",
|
||||
"input_cost_per_token": null,
|
||||
"mode": null
|
||||
}) else {
|
||||
unreachable!()
|
||||
};
|
||||
let model = Model::from_fields(&fields).unwrap();
|
||||
|
||||
assert_eq!(model.prices.iter().count(), 0);
|
||||
assert_eq!(model.attributes.mode, None);
|
||||
assert!(model.unrecognized.is_empty());
|
||||
}
|
||||
}
|
||||
22
litellm-rust/crates/model-catalog/src/model.rs
Normal file
22
litellm-rust/crates/model-catalog/src/model.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use litellm_cost::price::{Adjustments, Prices};
|
||||
|
||||
use crate::error::EntryProblem;
|
||||
use crate::wire::{Attributes, EntryWire, Fields};
|
||||
|
||||
/// One catalog entry read into types.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Model {
|
||||
pub attributes: Attributes,
|
||||
pub prices: Prices,
|
||||
pub adjustments: Adjustments,
|
||||
pub capabilities: BTreeMap<String, bool>,
|
||||
pub unrecognized: Vec<String>,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn from_fields(fields: &Fields) -> Result<Self, Vec<EntryProblem>> {
|
||||
Self::try_from(EntryWire::read(fields))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,582 +0,0 @@
|
|||
use crate::capabilities::{AudioFormat, InputModality, Mode, OutputModality, VertexAiAudioApi};
|
||||
use crate::pricing::{OffPeakPricing, SearchContextCostPerQuery, TieredRate, WebSearchBillingUnit};
|
||||
use litellm_llms_types::formats::chat_completions::ReasoningEffort;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// Typed mirror of one catalog model entry.
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct ModelInfo {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub annotation_cost_per_page: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub annotation_cost_per_page_batches: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub audio_transcription_config: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bedrock_converse_supports_strict_tools: Option<bool>,
|
||||
/// Highest reasoning effort the Bedrock output_config accepts for this model.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bedrock_output_config_effort_ceiling: Option<ReasoningEffort>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_creation_input_audio_token_cost: Option<f64>,
|
||||
/// USD per token written to the provider's prompt cache.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_creation_input_token_cost: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_creation_input_token_cost_above_32k_tokens: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_creation_input_token_cost_above_100k_tokens: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_creation_input_token_cost_above_100k_tokens_batches: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_creation_input_token_cost_above_128k_tokens: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_creation_input_token_cost_above_1hr: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_creation_input_token_cost_above_1hr_above_100k_tokens: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_creation_input_token_cost_above_1hr_above_200k_tokens: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_creation_input_token_cost_above_200k_tokens: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_creation_input_token_cost_above_200k_tokens_batches: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_creation_input_token_cost_above_256k_tokens: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_creation_input_token_cost_above_272k_tokens: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_creation_input_token_cost_above_272k_tokens_batches: Option<f64>,
|
||||
/// Flex service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_creation_input_token_cost_above_272k_tokens_flex: Option<f64>,
|
||||
/// Priority service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_creation_input_token_cost_above_272k_tokens_priority: Option<f64>,
|
||||
/// Ultrafast service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_creation_input_token_cost_above_272k_tokens_ultrafast: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_creation_input_token_cost_batches: Option<f64>,
|
||||
/// Flex service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_creation_input_token_cost_flex: Option<f64>,
|
||||
/// Priority service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_creation_input_token_cost_priority: Option<f64>,
|
||||
/// Ultrafast service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_creation_input_token_cost_ultrafast: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_input_audio_token_cost: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_input_image_token_cost: Option<f64>,
|
||||
/// USD per prompt token served from the provider's prompt cache.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_input_token_cost: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_input_token_cost_above_32k_tokens: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_input_token_cost_above_100k_tokens: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_input_token_cost_above_100k_tokens_batches: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_input_token_cost_above_128k_tokens: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_input_token_cost_above_200k_tokens: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_input_token_cost_above_200k_tokens_batches: Option<f64>,
|
||||
/// Priority service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_input_token_cost_above_200k_tokens_priority: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_input_token_cost_above_256k_tokens: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_input_token_cost_above_272k_tokens: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_input_token_cost_above_272k_tokens_batches: Option<f64>,
|
||||
/// Flex service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_input_token_cost_above_272k_tokens_flex: Option<f64>,
|
||||
/// Priority service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_input_token_cost_above_272k_tokens_priority: Option<f64>,
|
||||
/// Ultrafast service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_input_token_cost_above_272k_tokens_ultrafast: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_input_token_cost_above_512k_tokens: Option<f64>,
|
||||
/// Balanced service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_input_token_cost_balanced: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_input_token_cost_batches: Option<f64>,
|
||||
/// Flex service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_input_token_cost_flex: Option<f64>,
|
||||
/// Priority service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_input_token_cost_priority: Option<f64>,
|
||||
/// Ultrafast service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_input_token_cost_ultrafast: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub citation_cost_per_token: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub code_interpreter_cost_per_session: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub comment: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub computer_use_input_cost_per_1k_tokens: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub computer_use_output_cost_per_1k_tokens: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cost_per_second: Option<f64>,
|
||||
/// Reasoning effort the provider applies when the request omits reasoning_effort. Gates whether a non-default temperature or the top_p/logprobs sampling params are accepted, which hold only when the effort resolves to 'none'.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub default_reasoning_effort: Option<ReasoningEffort>,
|
||||
/// Date the provider deprecates the model, YYYY-MM-DD.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub deprecation_date: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub file_search_cost_per_1k_calls: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub file_search_cost_per_gb_per_day: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub gemini_audio_only_live: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub gemini_native_audio: Option<bool>,
|
||||
/// USD per Grounding with Google Maps request; billed per query or per prompt per web_search_billing_unit.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub google_maps_grounding_cost_per_query: Option<f64>,
|
||||
/// USD cost per billable guardrail unit, keyed by the provider's usage counter name (e.g. Bedrock's contentPolicyUnits).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub guardrail_cost_per_unit: Option<BTreeMap<String, f64>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_audio_per_second: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_audio_per_second_above_128k_tokens: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_audio_token: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_audio_token_batches: Option<f64>,
|
||||
/// Priority service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_audio_token_priority: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_character: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_character_above_128k_tokens: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_image: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_image_above_128k_tokens: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_image_token: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_image_token_batches: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_pixel: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_query: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_request: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_second: Option<f64>,
|
||||
/// USD per prompt token.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_token: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_token_above_32k_tokens: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_token_above_100k_tokens: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_token_above_100k_tokens_batches: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_token_above_128k_tokens: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_token_above_200k_tokens: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_token_above_200k_tokens_batches: Option<f64>,
|
||||
/// Priority service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_token_above_200k_tokens_priority: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_token_above_256k_tokens: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_token_above_272k_tokens: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_token_above_272k_tokens_batches: Option<f64>,
|
||||
/// Flex service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_token_above_272k_tokens_flex: Option<f64>,
|
||||
/// Priority service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_token_above_272k_tokens_priority: Option<f64>,
|
||||
/// Ultrafast service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_token_above_272k_tokens_ultrafast: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_token_above_512k_tokens: Option<f64>,
|
||||
/// Balanced service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_token_balanced: Option<f64>,
|
||||
/// USD per prompt token via the provider's batch API.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_token_batches: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_token_cache_hit: Option<f64>,
|
||||
/// Flex service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_token_flex: Option<f64>,
|
||||
/// Priority service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_token_priority: Option<f64>,
|
||||
/// Ultrafast service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_token_ultrafast: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_video_per_second: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_video_per_second_above_128k_tokens: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_video_per_second_above_15s_interval: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_video_per_second_above_8s_interval: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_video_token: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_video_token_batches: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_dbu_cost_per_token: Option<f64>,
|
||||
/// LiteLLM provider slug; one of https://docs.litellm.ai/docs/providers.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub litellm_provider: Option<String>,
|
||||
/// Maximum prompt/context tokens the model accepts.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_input_tokens: Option<u64>,
|
||||
/// Maximum tokens the model can generate in one response.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_output_tokens: Option<u64>,
|
||||
/// Legacy field: max output tokens if the provider specifies it, else max input tokens.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_tokens: Option<u64>,
|
||||
/// Free-form notes about the entry (e.g. pricing derivation).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub metadata: Option<BTreeMap<String, Value>>,
|
||||
/// Primary API surface / task type of the model.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mode: Option<Mode>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ocr_cost_per_credit: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ocr_cost_per_page: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ocr_cost_per_page_batches: Option<f64>,
|
||||
/// Rates that replace the same-named base fields while the request falls inside the stated UTC windows.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub off_peak_pricing: Option<OffPeakPricing>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_audio_token: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_character: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_character_above_128k_tokens: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_image: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_image_1024: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_image_1536: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_image_512: Option<f64>,
|
||||
#[serde(
|
||||
rename = "output_cost_per_image_0.5K",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub output_cost_per_image_0_5k: Option<f64>,
|
||||
#[serde(
|
||||
rename = "output_cost_per_image_1K",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub output_cost_per_image_1k: Option<f64>,
|
||||
#[serde(
|
||||
rename = "output_cost_per_image_2K",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub output_cost_per_image_2k: Option<f64>,
|
||||
#[serde(
|
||||
rename = "output_cost_per_image_4K",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub output_cost_per_image_4k: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_image_token: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_pixel: Option<f64>,
|
||||
/// USD per reasoning/thinking token, when billed separately.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_reasoning_token: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_second: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_second_1080p: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_second_2k: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_second_480p: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_second_4k: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_second_720p: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_second_768p: Option<f64>,
|
||||
/// USD per generated token.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_token: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_token_above_32k_tokens: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_token_above_100k_tokens: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_token_above_100k_tokens_batches: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_token_above_128k_tokens: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_token_above_200k_tokens: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_token_above_200k_tokens_batches: Option<f64>,
|
||||
/// Priority service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_token_above_200k_tokens_priority: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_token_above_256k_tokens: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_token_above_272k_tokens: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_token_above_272k_tokens_batches: Option<f64>,
|
||||
/// Flex service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_token_above_272k_tokens_flex: Option<f64>,
|
||||
/// Priority service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_token_above_272k_tokens_priority: Option<f64>,
|
||||
/// Ultrafast service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_token_above_272k_tokens_ultrafast: Option<f64>,
|
||||
/// Rate applied once the prompt exceeds the token threshold in the field name.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_token_above_512k_tokens: Option<f64>,
|
||||
/// Balanced service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_token_balanced: Option<f64>,
|
||||
/// USD per generated token via the provider's batch API.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_token_batches: Option<f64>,
|
||||
/// Flex service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_token_flex: Option<f64>,
|
||||
/// Priority service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_token_priority: Option<f64>,
|
||||
/// Ultrafast service-tier rate for the same-named base field.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_token_ultrafast: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_video_per_second: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_video_token: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_dbu_cost_per_token: Option<f64>,
|
||||
/// Embedding dimension for embedding models.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_vector_size: Option<u64>,
|
||||
/// Smallest prefix the provider will actually cache; absent means the provider default applies.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub prompt_cache_min_tokens: Option<u64>,
|
||||
/// Provider-internal routing hints (e.g. bedrock_invocation_schema).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub provider_specific_entry: Option<BTreeMap<String, Value>>,
|
||||
/// Exact reasoning_effort levels this deployment accepts; wins over supports_* flags.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reasoning_effort_levels: Option<Vec<ReasoningEffort>>,
|
||||
/// Multiplier applied to all token costs when served from a non-global Vertex AI endpoint (e.g. 1.10 = +10%).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub regional_endpoint_uplift_multiplier: Option<f64>,
|
||||
/// Multiplier applied to all token costs for EU data residency (e.g. 1.10 = +10%).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub regional_processing_uplift_multiplier_eu: Option<f64>,
|
||||
/// Multiplier applied to all token costs for US data residency (e.g. 1.10 = +10%).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub regional_processing_uplift_multiplier_us: Option<f64>,
|
||||
/// Provider default requests-per-minute limit.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rpm: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rules: Option<Vec<Value>>,
|
||||
/// USD cost per web search query, keyed by search context size.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub search_context_cost_per_query: Option<SearchContextCostPerQuery>,
|
||||
/// URL of the provider pricing/model page this entry was taken from.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub source: Option<String>,
|
||||
/// Audio container formats the model can return.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supported_audio_formats: Option<Vec<AudioFormat>>,
|
||||
/// OpenAI-style API routes this model can be called through, e.g. /v1/chat/completions.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supported_endpoints: Option<Vec<String>>,
|
||||
/// Input modalities the model accepts.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supported_modalities: Option<Vec<InputModality>>,
|
||||
/// Output modalities the model can produce.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supported_output_modalities: Option<Vec<OutputModality>>,
|
||||
/// Cloud regions the model is available in ('global' or region ids).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supported_regions: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_adaptive_thinking: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_anthropic_compaction: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_anthropic_thinking_payload: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_assistant_prefill: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_audio_input: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_audio_output: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_bedrock_runtime_chat_completions_response_format: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_bedrock_runtime_chat_completions_tools_with_reasoning: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_computer_use: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_embedding_image_input: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_fast_mode: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_forced_tool_use: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_function_calling: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_image_input: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_image_size: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_legacy_thinking: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_low_reasoning_effort: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_max_reasoning_effort: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_mid_conversation_system: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_minimal_reasoning_effort: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_multimodal: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_native_streaming: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_native_structured_output: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_none_reasoning_effort: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_nova_canvas_image_edit: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_output_config: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_parallel_function_calling: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_parallel_tool_use_config: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_pdf_input: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_prompt_cache_breakpoint: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_prompt_caching: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_reasoning: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_response_schema: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_sampling_params: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_speed: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_system_messages: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_thinking_cache_preservation: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_tool_choice: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_tool_search: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_url_context: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_video_input: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_vision: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_web_search: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_xhigh_reasoning_effort: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub thinking_always_on: Option<bool>,
|
||||
/// Context-length or result-count tiered rates; each tier's costs apply within its range.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tiered_pricing: Option<Vec<TieredRate>>,
|
||||
/// Provider default tokens-per-minute limit.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tpm: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub use_openai_responses_path: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub uses_embed_content: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub vector_store_cost_per_gb_per_day: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub vertex_ai_audio_api: Option<VertexAiAudioApi>,
|
||||
/// Whether web search is billed per query or per prompt.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub web_search_billing_unit: Option<WebSearchBillingUnit>,
|
||||
}
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Whether web search is billed per query or per prompt.
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WebSearchBillingUnit {
|
||||
PerQuery,
|
||||
PerPrompt,
|
||||
}
|
||||
|
||||
/// UTC "HH:MM-HH:MM" window, or a list of them; a window may wrap past midnight.
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum UtcHours {
|
||||
Single(String),
|
||||
Multiple(Vec<String>),
|
||||
}
|
||||
|
||||
/// ISO-8601 weekday number (1 = Monday .. 7 = Sunday) or English day name.
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum Weekday {
|
||||
Number(u8),
|
||||
Name(String),
|
||||
}
|
||||
|
||||
/// One off-peak window entry inside `windows`.
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct OffPeakWindow {
|
||||
pub hours_utc: UtcHours,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub weekdays: Option<Vec<Weekday>>,
|
||||
}
|
||||
|
||||
/// Rates that replace the same-named base fields inside the stated UTC windows.
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct OffPeakPricing {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub hours_utc: Option<UtcHours>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub windows: Option<Vec<OffPeakWindow>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub weekday_timezone: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_token: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_token: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_reasoning_token: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_input_token_cost: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_creation_input_token_cost: Option<f64>,
|
||||
}
|
||||
|
||||
/// USD cost per web search query, keyed by search context size.
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct SearchContextCostPerQuery {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub search_context_size_low: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub search_context_size_medium: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub search_context_size_high: Option<f64>,
|
||||
}
|
||||
|
||||
/// One tier of a context-length or result-count tiered rate.
|
||||
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct TieredRate {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub range: Option<[f64; 2]>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_results_range: Option<[f64; 2]>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_token: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_token: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub output_cost_per_reasoning_token: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_read_input_token_cost: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cache_creation_input_token_cost: Option<f64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub input_cost_per_query: Option<f64>,
|
||||
}
|
||||
|
|
@ -1,9 +1,79 @@
|
|||
use schemars::Schema;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use litellm_cost::money::{Multiplier, UnitPrice};
|
||||
use schemars::{JsonSchema, Schema};
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
use crate::fallback::FallbackGeneralizations;
|
||||
use crate::wire::{Attributes, OffPeakWindow, SearchContextCostPerQuery, UtcHours};
|
||||
|
||||
#[derive(JsonSchema)]
|
||||
#[schemars(rename = "ModelEntry")]
|
||||
struct ModelEntry {
|
||||
#[serde(flatten)]
|
||||
_attributes: Attributes,
|
||||
#[serde(flatten)]
|
||||
_blocks: PriceBlocks,
|
||||
}
|
||||
|
||||
#[derive(JsonSchema)]
|
||||
#[allow(dead_code)]
|
||||
struct PriceBlocks {
|
||||
/// USD cost per billable guardrail unit, keyed by the provider's usage counter name (e.g. Bedrock's contentPolicyUnits).
|
||||
guardrail_cost_per_unit: Option<BTreeMap<String, UnitPrice>>,
|
||||
/// Rates that replace the same-named base fields while the request falls inside the stated UTC windows.
|
||||
off_peak_pricing: Option<OffPeakPricing>,
|
||||
/// Multiplier applied to all token costs when served from a non-global Vertex AI endpoint (e.g. 1.10 = +10%).
|
||||
regional_endpoint_uplift_multiplier: Option<Multiplier>,
|
||||
/// Multiplier applied to all token costs for EU data residency (e.g. 1.10 = +10%).
|
||||
regional_processing_uplift_multiplier_eu: Option<Multiplier>,
|
||||
/// Multiplier applied to all token costs for US data residency (e.g. 1.10 = +10%).
|
||||
regional_processing_uplift_multiplier_us: Option<Multiplier>,
|
||||
/// USD cost per web search query, keyed by search context size.
|
||||
search_context_cost_per_query: Option<SearchContextCostPerQuery>,
|
||||
/// Context-length or result-count tiered rates; each tier's costs apply within its range.
|
||||
tiered_pricing: Option<Vec<TieredRate>>,
|
||||
}
|
||||
|
||||
/// Rates that replace the same-named base fields inside the stated UTC windows.
|
||||
#[derive(JsonSchema)]
|
||||
#[allow(dead_code)]
|
||||
struct OffPeakPricing {
|
||||
hours_utc: Option<UtcHours>,
|
||||
windows: Option<Vec<OffPeakWindow>>,
|
||||
weekday_timezone: Option<String>,
|
||||
}
|
||||
|
||||
/// One tier of a context-length or result-count tiered rate.
|
||||
#[derive(JsonSchema)]
|
||||
#[allow(dead_code)]
|
||||
struct TieredRate {
|
||||
range: Option<[u64; 2]>,
|
||||
max_results_range: Option<[u64; 2]>,
|
||||
}
|
||||
|
||||
#[derive(JsonSchema)]
|
||||
#[allow(dead_code)]
|
||||
struct RegistryDocument {
|
||||
sample_spec: Option<Map<String, Value>>,
|
||||
fallback_generalizations: Option<FallbackGeneralizations>,
|
||||
}
|
||||
|
||||
pub(crate) const FLAT_KEY_PATTERN: &str = concat!(
|
||||
r"^(",
|
||||
r"input_cost_per_((audio_|image_|video_)?token|character|image|pixel|query|request|second|text_message|audio_per_second|video_per_second(_above_\d+s_interval)?)",
|
||||
r"|output_cost_per_((audio_|image_|video_|reasoning_)?token|character|image(_\d+(\.\d+)?K?)?|pixel|second(_\d+[pk])?|video_per_second)",
|
||||
r"|cache_read_input_(audio_|image_|video_)?token_cost",
|
||||
r"|explicit_cache_read_input_token_cost",
|
||||
r"|cache_creation_input_(audio_|image_|video_)?token_cost(_above_1hr)?",
|
||||
r"|annotation_cost_per_page|citation_cost_per_token|code_interpreter_cost_per_session",
|
||||
r"|google_maps_grounding_cost_per_query|ocr_cost_per_credit|ocr_cost_per_page|cost_per_second",
|
||||
r")(_above_\d+k?_tokens)?(_(priority|flex|ultrafast|balanced|batches|decisions))?$",
|
||||
);
|
||||
|
||||
/// JSON Schema for one model entry, including registry validation constraints.
|
||||
pub fn model_entry_json_schema() -> Schema {
|
||||
let mut schema = serde_json::to_value(schemars::schema_for!(crate::ModelInfo))
|
||||
let mut schema = serde_json::to_value(schemars::schema_for!(ModelEntry))
|
||||
.expect("derived model schema serializes");
|
||||
remove_nullable_optional_fields(&mut schema);
|
||||
decorate_model_entry(&mut schema);
|
||||
|
|
@ -22,24 +92,21 @@ pub fn registry_json_schema() -> Schema {
|
|||
entry.as_object_mut().unwrap().remove("$schema");
|
||||
definitions.insert("modelEntry".into(), entry);
|
||||
|
||||
let mut fallback = serde_json::to_value(schemars::schema_for!(crate::FallbackGeneralizations))
|
||||
.expect("derived fallback schema serializes");
|
||||
remove_nullable_optional_fields(&mut fallback);
|
||||
definitions.extend(take_definitions(&mut fallback));
|
||||
fallback.as_object_mut().unwrap().remove("$schema");
|
||||
|
||||
let root = json!({
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "LiteLLM model prices and context window registry",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sample_spec": {"type": "object"},
|
||||
"fallback_generalizations": fallback
|
||||
},
|
||||
"additionalProperties": {"$ref": "#/$defs/modelEntry"},
|
||||
"$defs": definitions
|
||||
});
|
||||
Schema::from(root.as_object().unwrap().clone())
|
||||
let mut registry = serde_json::to_value(schemars::schema_for!(RegistryDocument))
|
||||
.expect("derived registry schema serializes");
|
||||
remove_nullable_optional_fields(&mut registry);
|
||||
definitions.extend(take_definitions(&mut registry));
|
||||
let root = registry.as_object_mut().unwrap();
|
||||
root.insert(
|
||||
"title".into(),
|
||||
"LiteLLM model prices and context window registry".into(),
|
||||
);
|
||||
root.insert(
|
||||
"additionalProperties".into(),
|
||||
json!({"$ref": "#/$defs/modelEntry"}),
|
||||
);
|
||||
root.insert("$defs".into(), Value::Object(definitions));
|
||||
Schema::from(root.clone())
|
||||
}
|
||||
|
||||
fn take_definitions(schema: &mut Value) -> Map<String, Value> {
|
||||
|
|
@ -80,42 +147,21 @@ fn remove_nullable_optional_fields(value: &mut Value) {
|
|||
}
|
||||
|
||||
fn decorate_model_entry(schema: &mut Value) {
|
||||
let rates = json!({FLAT_KEY_PATTERN: {"type": "number", "minimum": 0}});
|
||||
let object = schema.as_object_mut().unwrap();
|
||||
object.insert("required".into(), json!(["litellm_provider"]));
|
||||
object.insert("additionalProperties".into(), Value::Bool(true));
|
||||
let properties = object
|
||||
.get_mut("properties")
|
||||
.unwrap()
|
||||
.as_object_mut()
|
||||
.unwrap();
|
||||
properties.insert(
|
||||
"aliases".into(),
|
||||
json!({"type": "array", "items": {"type": "string"}}),
|
||||
object.insert(
|
||||
"patternProperties".into(),
|
||||
json!({
|
||||
FLAT_KEY_PATTERN: {"type": "number", "minimum": 0},
|
||||
"^supports_": {"type": "boolean"},
|
||||
}),
|
||||
);
|
||||
properties.get_mut("deprecation_date").unwrap()["format"] = json!("date");
|
||||
properties.get_mut("deprecation_date").unwrap()["pattern"] =
|
||||
json!(r"^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$");
|
||||
|
||||
properties.iter_mut().for_each(|(name, property)| {
|
||||
if name.contains("cost") {
|
||||
property["minimum"] = json!(0);
|
||||
} else if name.contains("uplift_multiplier") {
|
||||
property["minimum"] = json!(1);
|
||||
}
|
||||
});
|
||||
properties.get_mut("guardrail_cost_per_unit").unwrap()["additionalProperties"]["minimum"] =
|
||||
json!(0);
|
||||
|
||||
let definitions = object.get_mut("$defs").unwrap().as_object_mut().unwrap();
|
||||
for definition in ["OffPeakPricing", "TieredRate", "SearchContextCostPerQuery"] {
|
||||
let properties = definitions[definition]["properties"]
|
||||
.as_object_mut()
|
||||
.unwrap();
|
||||
properties.iter_mut().for_each(|(name, property)| {
|
||||
if name.contains("cost") || definition == "SearchContextCostPerQuery" {
|
||||
property["minimum"] = json!(0);
|
||||
}
|
||||
});
|
||||
for definition in ["OffPeakPricing", "TieredRate"] {
|
||||
definitions[definition]["patternProperties"] = rates.clone();
|
||||
definitions[definition]["additionalProperties"] = Value::Bool(false);
|
||||
}
|
||||
definitions["OffPeakPricing"]["anyOf"] = json!([
|
||||
{"required": ["hours_utc"]},
|
||||
|
|
@ -123,15 +169,43 @@ fn decorate_model_entry(schema: &mut Value) {
|
|||
]);
|
||||
definitions["OffPeakPricing"]["properties"]["windows"]["minItems"] = json!(1);
|
||||
definitions["OffPeakWindow"]["properties"]["weekdays"]["minItems"] = json!(1);
|
||||
definitions["TieredRate"]["properties"]["range"]["items"]["minimum"] = json!(0);
|
||||
definitions["TieredRate"]["properties"]["max_results_range"]["items"]["minimum"] = json!(0);
|
||||
definitions["Weekday"]["anyOf"][0]["minimum"] = json!(1);
|
||||
definitions["Weekday"]["anyOf"][0]["maximum"] = json!(7);
|
||||
definitions["Weekday"]["anyOf"][1]["pattern"] = json!(
|
||||
r"(?i)^(mon|monday|tue|tues|tuesday|wed|wednesday|thu|thur|thurs|thursday|fri|friday|sat|saturday|sun|sunday)$"
|
||||
);
|
||||
let window_pattern = json!(r"^([01]\d|2[0-3]):[0-5]\d-([01]\d|2[0-3]):[0-5]\d$");
|
||||
definitions["UtcHours"]["anyOf"][0]["pattern"] = window_pattern.clone();
|
||||
definitions["UtcHours"]["anyOf"][1]["items"]["pattern"] = window_pattern;
|
||||
definitions["UtcHours"]["anyOf"][1]["minItems"] = json!(1);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::FLAT_KEY_PATTERN;
|
||||
use crate::wire::FlatKey;
|
||||
|
||||
#[rstest]
|
||||
#[case::input_token("input_cost_per_token")]
|
||||
#[case::cache_write_one_hour_band(
|
||||
"cache_creation_input_token_cost_above_1hr_above_200k_tokens"
|
||||
)]
|
||||
#[case::image_label("output_cost_per_image_0.5K")]
|
||||
#[case::resolution_tier("output_cost_per_second_4k_flex")]
|
||||
#[case::feature_batch("annotation_cost_per_page_batches")]
|
||||
#[case::video_interval("input_cost_per_video_per_second_above_8s_interval")]
|
||||
#[case::explicit_cache_read("explicit_cache_read_input_token_cost")]
|
||||
#[case::wall_clock("cost_per_second")]
|
||||
#[case::unused_meter("cost_per_token")]
|
||||
#[case::other_currency("input_dbu_cost_per_token")]
|
||||
#[case::typo("input_cost_per_tokne")]
|
||||
#[case::read_has_no_ttl("cache_read_input_token_cost_above_1hr")]
|
||||
#[case::interval_on_a_token_meter("input_cost_per_token_above_8s_interval")]
|
||||
fn flat_key_pattern_agrees_with_the_parser(#[case] key: &str) {
|
||||
let schema = serde_json::json!({
|
||||
"type": "object",
|
||||
"patternProperties": {FLAT_KEY_PATTERN: {}},
|
||||
"additionalProperties": false,
|
||||
});
|
||||
let validator = jsonschema::validator_for(&schema).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
validator.is_valid(&serde_json::json!({key: 1})),
|
||||
FlatKey::parse(key).is_some()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,218 +0,0 @@
|
|||
use std::collections::BTreeSet;
|
||||
|
||||
use serde_json::{Map, Value};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{AliasIssue, Catalog, ModelInfo, UtcHours, Weekday};
|
||||
|
||||
/// A registry entry violates the checked-in catalog contract.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RegistryValidationError {
|
||||
#[error("{reason}")]
|
||||
Entry { model: String, reason: String },
|
||||
#[error("alias issue: {0:?}")]
|
||||
Alias(AliasIssue),
|
||||
}
|
||||
|
||||
/// Validate one registry entry without restricting the tolerant catalog reader.
|
||||
pub fn validate_model_entry(model: &str, value: &Value) -> Result<(), RegistryValidationError> {
|
||||
validate_entry_inner(model, value).map_err(|reason| RegistryValidationError::Entry {
|
||||
model: model.to_owned(),
|
||||
reason,
|
||||
})
|
||||
}
|
||||
|
||||
/// Check every model and alias in a parsed catalog against registry rules.
|
||||
pub fn validate_registry(catalog: &Catalog) -> Result<(), RegistryValidationError> {
|
||||
if let Some(issue) = catalog.alias_issues().first() {
|
||||
return Err(RegistryValidationError::Alias(issue.clone()));
|
||||
}
|
||||
catalog.model_names().try_for_each(|name| {
|
||||
let entry = catalog.lookup(name).expect("catalog name must resolve");
|
||||
validate_model_entry(name, &Value::Object(entry.entry.fields().clone()))
|
||||
})
|
||||
}
|
||||
|
||||
fn json_eq(left: &Value, right: &Value) -> bool {
|
||||
match (left, right) {
|
||||
(Value::Number(left), Value::Number(right)) => left.as_f64() == right.as_f64(),
|
||||
(Value::Array(left), Value::Array(right)) => {
|
||||
left.len() == right.len() && left.iter().zip(right).all(|(a, b)| json_eq(a, b))
|
||||
}
|
||||
(Value::Object(left), Value::Object(right)) => {
|
||||
left.len() == right.len()
|
||||
&& left
|
||||
.iter()
|
||||
.all(|(key, value)| right.get(key).is_some_and(|other| json_eq(value, other)))
|
||||
}
|
||||
_ => left == right,
|
||||
}
|
||||
}
|
||||
|
||||
fn keys(value: &Map<String, Value>) -> BTreeSet<String> {
|
||||
value.keys().cloned().collect()
|
||||
}
|
||||
|
||||
fn symmetric_difference(left: &BTreeSet<String>, right: &BTreeSet<String>) -> BTreeSet<String> {
|
||||
left.symmetric_difference(right).cloned().collect()
|
||||
}
|
||||
|
||||
fn validate_entry_inner(model_name: &str, value: &Value) -> Result<(), String> {
|
||||
let object = value
|
||||
.as_object()
|
||||
.ok_or_else(|| format!("{model_name} must be an object"))?;
|
||||
if let Some(aliases) = object.get("aliases") {
|
||||
let names = aliases
|
||||
.as_array()
|
||||
.ok_or_else(|| format!("{model_name}.aliases must be an array"))?;
|
||||
if names.iter().any(|name| !name.is_string()) {
|
||||
return Err(format!("{model_name}.aliases must contain strings"));
|
||||
}
|
||||
}
|
||||
let info: ModelInfo =
|
||||
serde_json::from_value(value.clone()).map_err(|error| format!("{model_name}: {error}"))?;
|
||||
if info.litellm_provider.is_none() {
|
||||
return Err(format!("{model_name}.litellm_provider is required"));
|
||||
}
|
||||
validate_dates_and_windows(model_name, &info)?;
|
||||
let serialized = serde_json::to_value(info).map_err(|error| error.to_string())?;
|
||||
let mut expected = object.clone();
|
||||
expected.remove("aliases");
|
||||
if !json_eq(&Value::Object(expected.clone()), &serialized) {
|
||||
let actual = serialized
|
||||
.as_object()
|
||||
.expect("ModelInfo serializes as an object");
|
||||
return Err(format!(
|
||||
"{model_name} has an unknown field, null, or changed value: {:?}",
|
||||
symmetric_difference(&keys(&expected), &keys(actual))
|
||||
));
|
||||
}
|
||||
check_prices(model_name, value)
|
||||
}
|
||||
|
||||
fn validate_dates_and_windows(model_name: &str, info: &ModelInfo) -> Result<(), String> {
|
||||
if let Some(date) = &info.deprecation_date {
|
||||
let format = time::format_description::parse_borrowed::<2>("[year]-[month]-[day]").unwrap();
|
||||
time::Date::parse(date, &format)
|
||||
.map_err(|error| format!("{model_name}.deprecation_date: {error}"))?;
|
||||
}
|
||||
let Some(pricing) = &info.off_peak_pricing else {
|
||||
return Ok(());
|
||||
};
|
||||
if pricing.hours_utc.is_none() && pricing.windows.is_none() {
|
||||
return Err(format!(
|
||||
"{model_name}.off_peak_pricing needs hours or windows"
|
||||
));
|
||||
}
|
||||
if let Some(hours) = &pricing.hours_utc {
|
||||
validate_hours(hours)?;
|
||||
}
|
||||
if let Some(windows) = &pricing.windows {
|
||||
if windows.is_empty() {
|
||||
return Err(format!("{model_name}.off_peak_pricing.windows is empty"));
|
||||
}
|
||||
windows.iter().try_for_each(|window| {
|
||||
validate_hours(&window.hours_utc)?;
|
||||
if let Some(days) = &window.weekdays
|
||||
&& (days.is_empty() || days.iter().any(|day| !valid_weekday(day)))
|
||||
{
|
||||
return Err(format!("{model_name}.off_peak_pricing.weekdays is invalid"));
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_hours(hours: &UtcHours) -> Result<(), String> {
|
||||
let values = match hours {
|
||||
UtcHours::Single(value) => std::slice::from_ref(value),
|
||||
UtcHours::Multiple(values) => values.as_slice(),
|
||||
};
|
||||
if values.is_empty() || values.iter().any(|value| !valid_utc_window(value)) {
|
||||
return Err("off_peak_pricing.hours_utc is invalid".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn valid_utc_window(value: &str) -> bool {
|
||||
let Some((start, end)) = value.split_once('-') else {
|
||||
return false;
|
||||
};
|
||||
[start, end].into_iter().all(|clock| {
|
||||
let Some((hour, minute)) = clock.split_once(':') else {
|
||||
return false;
|
||||
};
|
||||
hour.len() == 2
|
||||
&& minute.len() == 2
|
||||
&& hour.parse::<u8>().is_ok_and(|hour| hour < 24)
|
||||
&& minute.parse::<u8>().is_ok_and(|minute| minute < 60)
|
||||
})
|
||||
}
|
||||
|
||||
fn valid_weekday(day: &Weekday) -> bool {
|
||||
match day {
|
||||
Weekday::Number(number) => (1..=7).contains(number),
|
||||
Weekday::Name(name) => matches!(
|
||||
name.to_ascii_lowercase().as_str(),
|
||||
"mon"
|
||||
| "monday"
|
||||
| "tue"
|
||||
| "tues"
|
||||
| "tuesday"
|
||||
| "wed"
|
||||
| "wednesday"
|
||||
| "thu"
|
||||
| "thur"
|
||||
| "thurs"
|
||||
| "thursday"
|
||||
| "fri"
|
||||
| "friday"
|
||||
| "sat"
|
||||
| "saturday"
|
||||
| "sun"
|
||||
| "sunday"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn check_prices(path: &str, value: &Value) -> Result<(), String> {
|
||||
let Some(object) = value.as_object() else {
|
||||
return Ok(());
|
||||
};
|
||||
object.iter().try_for_each(|(key, field)| {
|
||||
let field_path = format!("{path}.{key}");
|
||||
if (key.contains("cost")
|
||||
|| path.ends_with(".guardrail_cost_per_unit")
|
||||
|| path.ends_with(".search_context_cost_per_query"))
|
||||
&& let Some(number) = field.as_f64()
|
||||
&& number < 0.0
|
||||
{
|
||||
return Err(format!("{field_path} must be nonnegative"));
|
||||
}
|
||||
if key.contains("uplift_multiplier")
|
||||
&& let Some(number) = field.as_f64()
|
||||
&& number < 1.0
|
||||
{
|
||||
return Err(format!("{field_path} must be at least one"));
|
||||
}
|
||||
if matches!(key.as_str(), "range" | "max_results_range")
|
||||
&& field.as_array().is_some_and(|values| {
|
||||
values
|
||||
.iter()
|
||||
.any(|value| value.as_f64().is_some_and(|n| n < 0.0))
|
||||
})
|
||||
{
|
||||
return Err(format!("{field_path} must be nonnegative"));
|
||||
}
|
||||
if matches!(key.as_str(), "metadata" | "provider_specific_entry") {
|
||||
return Ok(());
|
||||
}
|
||||
match field.as_array() {
|
||||
Some(items) => items.iter().enumerate().try_for_each(|(index, item)| {
|
||||
check_prices(&format!("{field_path}[{index}]"), item)
|
||||
}),
|
||||
None => check_prices(&field_path, field),
|
||||
}
|
||||
})
|
||||
}
|
||||
124
litellm-rust/crates/model-catalog/src/vocabulary.rs
Normal file
124
litellm-rust/crates/model-catalog/src/vocabulary.rs
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
/// Primary API surface / task type of the model.
|
||||
#[macro_rules_attribute::apply(crate::wire_type)]
|
||||
#[derive(Copy, Eq, Hash)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Mode {
|
||||
AudioSpeech,
|
||||
AudioTranscription,
|
||||
Chat,
|
||||
Completion,
|
||||
Decisions,
|
||||
Embedding,
|
||||
Evaluation,
|
||||
Extraction,
|
||||
Guardrail,
|
||||
ImageEdit,
|
||||
ImageGeneration,
|
||||
Moderation,
|
||||
Ocr,
|
||||
Realtime,
|
||||
Rerank,
|
||||
Responses,
|
||||
Search,
|
||||
VectorStore,
|
||||
VideoGeneration,
|
||||
}
|
||||
|
||||
/// Gemini audio generation API the model is served through.
|
||||
#[macro_rules_attribute::apply(crate::wire_type)]
|
||||
#[derive(Copy, Eq, Hash)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum VertexAiAudioApi {
|
||||
LyriaPredict,
|
||||
LyriaInteractions,
|
||||
}
|
||||
|
||||
/// Audio container format the model can return.
|
||||
#[macro_rules_attribute::apply(crate::wire_type)]
|
||||
#[derive(Copy, Eq, Hash)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AudioFormat {
|
||||
Mp3,
|
||||
Wav,
|
||||
}
|
||||
|
||||
/// Input modality the model accepts.
|
||||
#[macro_rules_attribute::apply(crate::wire_type)]
|
||||
#[derive(Copy, Eq, Hash)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum InputModality {
|
||||
Text,
|
||||
Image,
|
||||
Audio,
|
||||
Video,
|
||||
}
|
||||
|
||||
/// Output modality the model can produce.
|
||||
#[macro_rules_attribute::apply(crate::wire_type)]
|
||||
#[derive(Copy, Eq, Hash)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum OutputModality {
|
||||
Text,
|
||||
Image,
|
||||
Audio,
|
||||
Video,
|
||||
Code,
|
||||
}
|
||||
|
||||
/// API route a model can be called through.
|
||||
#[macro_rules_attribute::apply(crate::wire_type)]
|
||||
#[derive(Copy, Eq, Hash)]
|
||||
pub enum Endpoint {
|
||||
#[serde(rename = "/v1/chat/completions")]
|
||||
ChatCompletions,
|
||||
#[serde(rename = "/v1/completions")]
|
||||
Completions,
|
||||
#[serde(rename = "/v1/responses")]
|
||||
Responses,
|
||||
#[serde(rename = "/v1/messages")]
|
||||
Messages,
|
||||
#[serde(rename = "/v1/batch")]
|
||||
Batch,
|
||||
#[serde(rename = "/v1/embeddings")]
|
||||
Embeddings,
|
||||
#[serde(rename = "/v1/images/generations")]
|
||||
ImageGenerations,
|
||||
#[serde(rename = "/v1/images/edits")]
|
||||
ImageEdits,
|
||||
#[serde(rename = "/v1/audio/speech")]
|
||||
AudioSpeech,
|
||||
#[serde(rename = "/v1/audio/transcriptions")]
|
||||
AudioTranscriptions,
|
||||
#[serde(rename = "/v1/realtime")]
|
||||
Realtime,
|
||||
#[serde(rename = "/v1/realtime/transcription_sessions")]
|
||||
RealtimeTranscriptionSessions,
|
||||
#[serde(rename = "/v1/videos")]
|
||||
Videos,
|
||||
#[serde(rename = "/v1/ocr")]
|
||||
Ocr,
|
||||
#[serde(rename = "/v1/listen")]
|
||||
Listen,
|
||||
#[serde(rename = "/v1/systemone")]
|
||||
SystemOne,
|
||||
#[serde(rename = "/v1/decisions")]
|
||||
Decisions,
|
||||
#[serde(rename = "/v1beta/interactions")]
|
||||
Interactions,
|
||||
#[serde(rename = "/vertex_ai/live")]
|
||||
VertexAiLive,
|
||||
}
|
||||
|
||||
/// Reasoning effort level a model accepts or applies.
|
||||
#[macro_rules_attribute::apply(crate::wire_type)]
|
||||
#[derive(Copy, Eq, Hash)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum EffortLevel {
|
||||
None,
|
||||
Minimal,
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
Xhigh,
|
||||
Max,
|
||||
}
|
||||
10
litellm-rust/crates/model-catalog/src/wire.rs
Normal file
10
litellm-rust/crates/model-catalog/src/wire.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
mod attributes;
|
||||
mod blocks;
|
||||
mod entry;
|
||||
mod flat_key;
|
||||
mod lax;
|
||||
|
||||
pub use attributes::*;
|
||||
pub use blocks::*;
|
||||
pub use entry::*;
|
||||
pub use flat_key::*;
|
||||
319
litellm-rust/crates/model-catalog/src/wire/attributes.rs
Normal file
319
litellm-rust/crates/model-catalog/src/wire/attributes.rs
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use litellm_cost::money::UnitPrice;
|
||||
use serde::de::{self, Deserializer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::error::InvalidDate;
|
||||
use crate::vocabulary::{
|
||||
AudioFormat, EffortLevel, Endpoint, InputModality, Mode, OutputModality, VertexAiAudioApi,
|
||||
};
|
||||
use crate::wire::lax::Count;
|
||||
|
||||
macro_rules! attributes {
|
||||
($($(#[$doc:meta])* $name:ident: $ty:ty $(as $via:ty)?),* $(,)?) => {
|
||||
/// The named, non-price fields of a catalog entry.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
pub struct Attributes {
|
||||
$($(#[$doc])* pub $name: Option<$ty>,)*
|
||||
}
|
||||
|
||||
impl Attributes {
|
||||
pub(crate) fn read(
|
||||
&mut self,
|
||||
key: &str,
|
||||
value: &Value,
|
||||
) -> Option<Result<(), serde_json::Error>> {
|
||||
match key {
|
||||
$(stringify!($name) => Some(
|
||||
attributes!(@read value, $ty $(, $via)?).map(|read| self.$name = Some(read)),
|
||||
),)*
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
(@read $value:ident, $ty:ty) => {
|
||||
<$ty as Deserialize>::deserialize($value)
|
||||
};
|
||||
(@read $value:ident, $ty:ty, $via:ty) => {
|
||||
serde_with::de::DeserializeAsWrap::<$ty, $via>::deserialize($value)
|
||||
.map(serde_with::de::DeserializeAsWrap::into_inner)
|
||||
};
|
||||
}
|
||||
|
||||
attributes! {
|
||||
/// Other names this entry is served under.
|
||||
aliases: Vec<String>,
|
||||
audio_transcription_config: String,
|
||||
bedrock_converse_supports_strict_tools: bool,
|
||||
/// Highest reasoning effort the Bedrock output_config accepts for this model.
|
||||
bedrock_output_config_effort_ceiling: EffortLevel,
|
||||
comment: String,
|
||||
/// Reasoning effort the provider applies when the request omits reasoning_effort. Gates whether a non-default temperature or the top_p/logprobs sampling params are accepted, which hold only when the effort resolves to 'none'.
|
||||
default_reasoning_effort: EffortLevel,
|
||||
/// Date the provider deprecates the model, YYYY-MM-DD.
|
||||
deprecation_date: IsoDate,
|
||||
gemini_audio_only_live: bool,
|
||||
gemini_native_audio: bool,
|
||||
input_cost_per_token_cache_hit: UnitPrice,
|
||||
/// Databricks units, not USD.
|
||||
input_dbu_cost_per_token: Dbu,
|
||||
/// LiteLLM provider slug; one of https://docs.litellm.ai/docs/providers.
|
||||
litellm_provider: String,
|
||||
/// Maximum prompt/context tokens the model accepts.
|
||||
max_input_tokens: u64 as Count,
|
||||
/// Maximum tokens the model can generate in one response.
|
||||
max_output_tokens: u64 as Count,
|
||||
/// Legacy field: max output tokens if the provider specifies it, else max input tokens.
|
||||
max_tokens: u64 as Count,
|
||||
/// Free-form notes about the entry (e.g. pricing derivation).
|
||||
metadata: BTreeMap<String, Value>,
|
||||
/// Primary API surface / task type of the model.
|
||||
mode: Mode,
|
||||
/// Databricks units, not USD.
|
||||
output_dbu_cost_per_token: Dbu,
|
||||
/// Embedding dimension for embedding models.
|
||||
output_vector_size: u64 as Count,
|
||||
/// Smallest prefix the provider will actually cache; absent means the provider default applies.
|
||||
prompt_cache_min_tokens: u64 as Count,
|
||||
/// Provider-internal routing hints (e.g. bedrock_invocation_schema).
|
||||
provider_specific_entry: BTreeMap<String, Value>,
|
||||
/// Exact reasoning_effort levels this deployment accepts; wins over supports_* flags.
|
||||
reasoning_effort_levels: Vec<EffortLevel>,
|
||||
/// Provider default requests-per-minute limit.
|
||||
rpm: u64 as Count,
|
||||
/// Grounded search requests the provider includes for free before search is billed.
|
||||
search_free_requests: SearchFreeRequests,
|
||||
/// URL of the provider pricing/model page this entry was taken from.
|
||||
source: String,
|
||||
/// Audio container formats the model can return.
|
||||
supported_audio_formats: Vec<AudioFormat>,
|
||||
/// OpenAI-style API routes this model can be called through, e.g. /v1/chat/completions.
|
||||
supported_endpoints: Vec<Endpoint>,
|
||||
/// Input modalities the model accepts.
|
||||
supported_modalities: Vec<InputModality>,
|
||||
/// Output modalities the model can produce.
|
||||
supported_output_modalities: Vec<OutputModality>,
|
||||
/// Cloud regions the model is available in ('global' or region ids).
|
||||
supported_regions: Vec<String>,
|
||||
thinking_always_on: bool,
|
||||
token_count_framing: u64 as Count,
|
||||
token_count_model: TokenCountModel,
|
||||
token_count_tokenizer: TokenCountTokenizer,
|
||||
/// Provider default tokens-per-minute limit.
|
||||
tpm: u64 as Count,
|
||||
use_openai_responses_path: bool,
|
||||
uses_embed_content: bool,
|
||||
vertex_ai_audio_api: VertexAiAudioApi,
|
||||
/// Whether web search is billed per query or per prompt.
|
||||
web_search_billing_unit: WebSearchBillingUnit,
|
||||
}
|
||||
|
||||
/// The model whose provider token counter counts tokens for this entry.
|
||||
#[macro_rules_attribute::apply(crate::wire_type)]
|
||||
#[derive(Eq, Hash)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct TokenCountModel {
|
||||
pub model: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub litellm_provider: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub region: Option<String>,
|
||||
}
|
||||
|
||||
/// Free grounded search requests per period, shared across the models of one group.
|
||||
#[macro_rules_attribute::apply(crate::wire_type)]
|
||||
#[derive(Eq, Hash)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct SearchFreeRequests {
|
||||
pub count: u64,
|
||||
pub period: FreeRequestPeriod,
|
||||
pub shared_group: String,
|
||||
}
|
||||
|
||||
#[macro_rules_attribute::apply(crate::wire_type)]
|
||||
#[derive(Copy, Eq, Hash)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FreeRequestPeriod {
|
||||
Day,
|
||||
Month,
|
||||
}
|
||||
|
||||
/// Local tokenizer that counts tokens for this entry.
|
||||
#[macro_rules_attribute::apply(crate::wire_type)]
|
||||
#[derive(Copy, Eq, Hash)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TokenCountTokenizer {
|
||||
KimiK3,
|
||||
}
|
||||
|
||||
/// Whether web search is billed per query or per prompt.
|
||||
#[macro_rules_attribute::apply(crate::wire_type)]
|
||||
#[derive(Copy, Eq, Hash)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WebSearchBillingUnit {
|
||||
PerQuery,
|
||||
PerPrompt,
|
||||
}
|
||||
|
||||
/// A non-negative amount of Databricks units.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct Dbu(f64);
|
||||
|
||||
impl Dbu {
|
||||
pub fn new(units: f64) -> Option<Self> {
|
||||
(units.is_finite() && units >= 0.0).then_some(Self(units))
|
||||
}
|
||||
pub fn get(self) -> f64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Dbu {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
let units = f64::deserialize(deserializer)?;
|
||||
Self::new(units).ok_or_else(|| {
|
||||
de::Error::invalid_value(
|
||||
de::Unexpected::Float(units),
|
||||
&"a non-negative number of Databricks units",
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A calendar date written YYYY-MM-DD.
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Hash,
|
||||
serde_with::DeserializeFromStr,
|
||||
serde_with::SerializeDisplay,
|
||||
)]
|
||||
pub struct IsoDate(pub time::Date);
|
||||
|
||||
impl FromStr for IsoDate {
|
||||
type Err = InvalidDate;
|
||||
|
||||
fn from_str(date: &str) -> Result<Self, Self::Err> {
|
||||
let mut parts = date.split('-');
|
||||
let (Some(year), Some(month), Some(day), None) =
|
||||
(parts.next(), parts.next(), parts.next(), parts.next())
|
||||
else {
|
||||
return Err(InvalidDate);
|
||||
};
|
||||
if year.len() != 4 || month.len() != 2 || day.len() != 2 {
|
||||
return Err(InvalidDate);
|
||||
}
|
||||
let number = |part: &str| part.parse::<u8>().map_err(|_| InvalidDate);
|
||||
let month = time::Month::try_from(number(month)?).map_err(|_| InvalidDate)?;
|
||||
time::Date::from_calendar_date(year.parse().map_err(|_| InvalidDate)?, month, number(day)?)
|
||||
.map(Self)
|
||||
.map_err(|_| InvalidDate)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for IsoDate {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{:04}-{:02}-{:02}",
|
||||
self.0.year(),
|
||||
u8::from(self.0.month()),
|
||||
self.0.day()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
mod schema {
|
||||
use std::borrow::Cow;
|
||||
|
||||
use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
|
||||
|
||||
use super::{Dbu, IsoDate};
|
||||
|
||||
impl JsonSchema for IsoDate {
|
||||
fn inline_schema() -> bool {
|
||||
true
|
||||
}
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"IsoDate".into()
|
||||
}
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({
|
||||
"type": "string",
|
||||
"format": "date",
|
||||
"pattern": r"^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for Dbu {
|
||||
fn inline_schema() -> bool {
|
||||
true
|
||||
}
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"Dbu".into()
|
||||
}
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
json_schema!({"type": "number", "minimum": 0})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::rstest;
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
#[case::leap_day("2028-02-29", true)]
|
||||
#[case::not_a_leap_year("2026-02-29", false)]
|
||||
#[case::impossible_day("2026-02-31", false)]
|
||||
#[case::one_digit_month("2026-2-01", false)]
|
||||
#[case::extra_part("2026-02-01-01", false)]
|
||||
fn iso_dates_are_real_calendar_dates(#[case] text: &str, #[case] valid: bool) {
|
||||
assert_eq!(text.parse::<IsoDate>().is_ok(), valid);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::count_written_as_text("max_tokens", json!("8192"), true)]
|
||||
#[case::negative_count("max_tokens", json!(-1), false)]
|
||||
#[case::known_mode("mode", json!("chat"), true)]
|
||||
#[case::unknown_mode("mode", json!("chatt"), false)]
|
||||
#[case::negative_dbu("input_dbu_cost_per_token", json!(-1), false)]
|
||||
fn each_attribute_reads_its_own_type(
|
||||
#[case] key: &str,
|
||||
#[case] value: Value,
|
||||
#[case] valid: bool,
|
||||
) {
|
||||
assert_eq!(
|
||||
Attributes::default()
|
||||
.read(key, &value)
|
||||
.map(|read| read.is_ok()),
|
||||
Some(valid)
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn keys_that_are_not_attributes_are_left_to_the_caller() {
|
||||
assert!(
|
||||
Attributes::default()
|
||||
.read("input_cost_per_token", &json!(1))
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
}
|
||||
154
litellm-rust/crates/model-catalog/src/wire/blocks.rs
Normal file
154
litellm-rust/crates/model-catalog/src/wire/blocks.rs
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
use litellm_cost::money::UnitPrice;
|
||||
use litellm_cost::schedule::{UtcWindow, Weekday};
|
||||
use serde::Deserialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::{Map, Value};
|
||||
use serde_with::de::DeserializeAsWrap;
|
||||
|
||||
use crate::error::EntryProblem;
|
||||
use crate::wire::flat_key::FlatKey;
|
||||
use crate::wire::lax::Count;
|
||||
|
||||
/// A flat price key with its spelling, kept so problems can name it.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct FlatPrice {
|
||||
pub key: String,
|
||||
pub flat: FlatKey,
|
||||
pub price: UnitPrice,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct TierRow {
|
||||
pub path: String,
|
||||
pub range: Option<[u64; 2]>,
|
||||
pub max_results_range: Option<[u64; 2]>,
|
||||
pub prices: Vec<FlatPrice>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct OffPeakBlock {
|
||||
pub hours_utc: Option<UtcHours>,
|
||||
pub windows: Option<Vec<OffPeakWindow>>,
|
||||
pub weekday_timezone: Option<String>,
|
||||
pub prices: Vec<FlatPrice>,
|
||||
}
|
||||
|
||||
/// One UTC window or a list of them.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(untagged)]
|
||||
pub enum UtcHours {
|
||||
Single(UtcWindow),
|
||||
Multiple(Vec<UtcWindow>),
|
||||
}
|
||||
|
||||
/// One off-peak window entry inside `windows`.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct OffPeakWindow {
|
||||
pub hours_utc: UtcHours,
|
||||
pub weekdays: Option<Vec<Weekday>>,
|
||||
}
|
||||
|
||||
/// USD cost per web search query, keyed by search context size.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct SearchContextCostPerQuery {
|
||||
pub search_context_size_low: Option<UnitPrice>,
|
||||
pub search_context_size_medium: Option<UnitPrice>,
|
||||
pub search_context_size_high: Option<UnitPrice>,
|
||||
}
|
||||
|
||||
impl UtcHours {
|
||||
pub fn windows(&self) -> &[UtcWindow] {
|
||||
match self {
|
||||
Self::Single(window) => std::slice::from_ref(window),
|
||||
Self::Multiple(windows) => windows,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Reader<'a> {
|
||||
pub(crate) problems: &'a mut Vec<EntryProblem>,
|
||||
}
|
||||
|
||||
impl Reader<'_> {
|
||||
pub(crate) fn value<T: DeserializeOwned>(&mut self, key: String, value: &Value) -> Option<T> {
|
||||
T::deserialize(value)
|
||||
.map_err(|error| self.problems.push(EntryProblem::Invalid { key, error }))
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub(crate) fn price(&mut self, key: String, flat: FlatKey, value: &Value) -> Option<FlatPrice> {
|
||||
let price = self.value(key.clone(), value)?;
|
||||
Some(FlatPrice { key, flat, price })
|
||||
}
|
||||
|
||||
fn object<'v>(&mut self, key: &str, value: &'v Value) -> Option<&'v Map<String, Value>> {
|
||||
match value {
|
||||
Value::Object(fields) => Some(fields),
|
||||
other => self
|
||||
.value::<Map<String, Value>>(key.to_owned(), other)
|
||||
.and(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn block_price(&mut self, name: &str, key: String, value: &Value) -> Option<FlatPrice> {
|
||||
let Some(flat) = FlatKey::parse(name) else {
|
||||
self.problems.push(EntryProblem::UnknownBlockKey { key });
|
||||
return None;
|
||||
};
|
||||
self.price(key, flat, value)
|
||||
}
|
||||
|
||||
pub(crate) fn tier_rows(&mut self, value: &Value) -> Option<Vec<TierRow>> {
|
||||
let rows: Vec<Value> = self.value("tiered_pricing".to_owned(), value)?;
|
||||
Some(
|
||||
rows.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, row)| self.tier_row(format!("tiered_pricing[{index}]"), row))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn tier_row(&mut self, path: String, value: &Value) -> Option<TierRow> {
|
||||
let fields = self.object(&path, value)?;
|
||||
let mut row = TierRow {
|
||||
path: path.clone(),
|
||||
..TierRow::default()
|
||||
};
|
||||
for (name, value) in fields.iter().filter(|(_, value)| !value.is_null()) {
|
||||
let key = format!("{path}.{name}");
|
||||
match name.as_str() {
|
||||
"range" => row.range = self.bounds(key, value),
|
||||
"max_results_range" => row.max_results_range = self.bounds(key, value),
|
||||
_ => row.prices.extend(self.block_price(name, key, value)),
|
||||
}
|
||||
}
|
||||
Some(row)
|
||||
}
|
||||
|
||||
fn bounds(&mut self, key: String, value: &Value) -> Option<[u64; 2]> {
|
||||
DeserializeAsWrap::<[u64; 2], [Count; 2]>::deserialize(value)
|
||||
.map(DeserializeAsWrap::into_inner)
|
||||
.map_err(|error| self.problems.push(EntryProblem::Invalid { key, error }))
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub(crate) fn off_peak(&mut self, value: &Value) -> Option<OffPeakBlock> {
|
||||
let fields = self.object("off_peak_pricing", value)?;
|
||||
let mut block = OffPeakBlock::default();
|
||||
for (name, value) in fields.iter().filter(|(_, value)| !value.is_null()) {
|
||||
let key = format!("off_peak_pricing.{name}");
|
||||
match name.as_str() {
|
||||
"hours_utc" => block.hours_utc = self.value(key, value),
|
||||
"windows" => block.windows = self.value(key, value),
|
||||
"weekday_timezone" => block.weekday_timezone = self.value(key, value),
|
||||
_ => block.prices.extend(self.block_price(name, key, value)),
|
||||
}
|
||||
}
|
||||
Some(block)
|
||||
}
|
||||
}
|
||||
197
litellm-rust/crates/model-catalog/src/wire/entry.rs
Normal file
197
litellm-rust/crates/model-catalog/src/wire/entry.rs
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use litellm_cost::money::{Multiplier, UnitPrice};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use crate::error::EntryProblem;
|
||||
use crate::wire::attributes::Attributes;
|
||||
use crate::wire::blocks::{FlatPrice, OffPeakBlock, Reader, SearchContextCostPerQuery, TierRow};
|
||||
use crate::wire::flat_key::FlatKey;
|
||||
|
||||
pub type Fields = Map<String, Value>;
|
||||
|
||||
/// One catalog entry as written. Every non-null key lands in exactly one field, and a known key
|
||||
/// with a bad value is kept out and listed in `problems`.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct EntryWire {
|
||||
pub attributes: Attributes,
|
||||
pub prices: Vec<FlatPrice>,
|
||||
pub capabilities: BTreeMap<String, bool>,
|
||||
pub tier_rows: Option<Vec<TierRow>>,
|
||||
pub off_peak: Option<OffPeakBlock>,
|
||||
pub search_context: Option<SearchContextCostPerQuery>,
|
||||
pub guardrail: Option<BTreeMap<String, UnitPrice>>,
|
||||
pub regional_endpoint_uplift: Option<Multiplier>,
|
||||
pub residency_uplift_eu: Option<Multiplier>,
|
||||
pub residency_uplift_us: Option<Multiplier>,
|
||||
pub unknown: Fields,
|
||||
pub problems: Vec<EntryProblem>,
|
||||
}
|
||||
|
||||
impl EntryWire {
|
||||
pub fn read(fields: &Fields) -> Self {
|
||||
let mut problems = Vec::new();
|
||||
let mut wire = Self::default();
|
||||
let mut reader = Reader {
|
||||
problems: &mut problems,
|
||||
};
|
||||
for (key, value) in fields.iter().filter(|(_, value)| !value.is_null()) {
|
||||
wire.read_key(&mut reader, key, value);
|
||||
}
|
||||
Self { problems, ..wire }
|
||||
}
|
||||
|
||||
fn read_key(&mut self, reader: &mut Reader<'_>, key: &str, value: &Value) {
|
||||
let owned = || key.to_owned();
|
||||
match key {
|
||||
"tiered_pricing" => self.tier_rows = reader.tier_rows(value),
|
||||
"off_peak_pricing" => self.off_peak = reader.off_peak(value),
|
||||
"search_context_cost_per_query" => self.search_context = reader.value(owned(), value),
|
||||
"guardrail_cost_per_unit" => self.guardrail = reader.value(owned(), value),
|
||||
"regional_endpoint_uplift_multiplier" => {
|
||||
self.regional_endpoint_uplift = reader.value(owned(), value)
|
||||
}
|
||||
"regional_processing_uplift_multiplier_eu" => {
|
||||
self.residency_uplift_eu = reader.value(owned(), value)
|
||||
}
|
||||
"regional_processing_uplift_multiplier_us" => {
|
||||
self.residency_uplift_us = reader.value(owned(), value)
|
||||
}
|
||||
_ => self.read_open_key(reader, key, value),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_open_key(&mut self, reader: &mut Reader<'_>, key: &str, value: &Value) {
|
||||
if let Some(read) = self.attributes.read(key, value) {
|
||||
if let Err(error) = read {
|
||||
reader.problems.push(EntryProblem::Invalid {
|
||||
key: key.to_owned(),
|
||||
error,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if let Some(flat) = FlatKey::parse(key) {
|
||||
self.prices
|
||||
.extend(reader.price(key.to_owned(), flat, value));
|
||||
return;
|
||||
}
|
||||
if key.starts_with("supports_") {
|
||||
if let Some(flag) = reader.value(key.to_owned(), value) {
|
||||
self.capabilities.insert(key.to_owned(), flag);
|
||||
}
|
||||
return;
|
||||
}
|
||||
self.unknown.insert(key.to_owned(), value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::rstest;
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn read(entry: Value) -> EntryWire {
|
||||
let Value::Object(fields) = entry else {
|
||||
panic!("entry must be an object")
|
||||
};
|
||||
EntryWire::read(&fields)
|
||||
}
|
||||
|
||||
fn problem_keys(wire: &EntryWire) -> Vec<String> {
|
||||
wire.problems
|
||||
.iter()
|
||||
.map(|problem| match problem {
|
||||
EntryProblem::Invalid { key, .. } | EntryProblem::UnknownBlockKey { key } => {
|
||||
key.clone()
|
||||
}
|
||||
other => panic!("the wire only reports field problems, got {other}"),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn every_key_lands_in_exactly_one_place() {
|
||||
let wire = read(json!({
|
||||
"litellm_provider": "openai",
|
||||
"max_tokens": "8192",
|
||||
"input_cost_per_token": "0.000001",
|
||||
"tiered_pricing": [{"range": [0, 10], "output_cost_per_token": 2e-6}],
|
||||
"supports_vision": true,
|
||||
"id": "deployment-1",
|
||||
"mode": null
|
||||
}));
|
||||
|
||||
assert_eq!(wire.attributes.litellm_provider.as_deref(), Some("openai"));
|
||||
assert_eq!(wire.attributes.max_tokens, Some(8192));
|
||||
assert_eq!(wire.attributes.mode, None);
|
||||
assert_eq!(
|
||||
wire.prices
|
||||
.iter()
|
||||
.map(|price| price.key.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
["input_cost_per_token"]
|
||||
);
|
||||
assert_eq!(wire.tier_rows.unwrap()[0].prices.len(), 1);
|
||||
assert_eq!(
|
||||
wire.capabilities,
|
||||
BTreeMap::from([("supports_vision".to_owned(), true)])
|
||||
);
|
||||
assert_eq!(wire.unknown.keys().collect::<Vec<_>>(), ["id"]);
|
||||
assert!(wire.problems.is_empty());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn every_bad_value_is_reported_by_its_key_at_once() {
|
||||
let wire = read(json!({
|
||||
"mode": "chatt",
|
||||
"max_tokens": -1,
|
||||
"input_cost_per_token": "x",
|
||||
"supports_vision": "yes",
|
||||
"regional_endpoint_uplift_multiplier": 0.5,
|
||||
"tiered_pricing": [
|
||||
{"range": [0, 10], "input_cost_per_token": 1e-6},
|
||||
{"range": [10.5, 20], "typo_cost": 1, "output_cost_per_token": -1}
|
||||
],
|
||||
"off_peak_pricing": {"hours_utc": "25:00-01:00", "input_cost_per_token": 1e-6}
|
||||
}));
|
||||
|
||||
assert_eq!(
|
||||
problem_keys(&wire)
|
||||
.into_iter()
|
||||
.collect::<std::collections::BTreeSet<_>>(),
|
||||
[
|
||||
"input_cost_per_token",
|
||||
"max_tokens",
|
||||
"mode",
|
||||
"off_peak_pricing.hours_utc",
|
||||
"regional_endpoint_uplift_multiplier",
|
||||
"supports_vision",
|
||||
"tiered_pricing[1].output_cost_per_token",
|
||||
"tiered_pricing[1].range",
|
||||
"tiered_pricing[1].typo_cost",
|
||||
]
|
||||
.map(str::to_owned)
|
||||
.into_iter()
|
||||
.collect()
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::tiers_not_a_list(json!({"tiered_pricing": {"range": [0, 1]}}), "tiered_pricing")]
|
||||
#[case::row_not_an_object(json!({"tiered_pricing": [3]}), "tiered_pricing[0]")]
|
||||
#[case::off_peak_not_an_object(json!({"off_peak_pricing": true}), "off_peak_pricing")]
|
||||
#[case::unknown_search_size(
|
||||
json!({"search_context_cost_per_query": {"search_context_size_huge": 1}}),
|
||||
"search_context_cost_per_query"
|
||||
)]
|
||||
#[case::negative_guardrail(
|
||||
json!({"guardrail_cost_per_unit": {"contentPolicyUnits": -1}}),
|
||||
"guardrail_cost_per_unit"
|
||||
)]
|
||||
fn a_bad_block_is_reported_at_the_block(#[case] entry: Value, #[case] expected: &str) {
|
||||
assert_eq!(problem_keys(&read(entry)), [expected]);
|
||||
}
|
||||
}
|
||||
338
litellm-rust/crates/model-catalog/src/wire/flat_key.rs
Normal file
338
litellm-rust/crates/model-catalog/src/wire/flat_key.rs
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
use std::num::NonZeroU64;
|
||||
|
||||
use litellm_cost::price::{
|
||||
Band, CacheReadMode, CacheTtl, Channel, Feature, ImageSize, InputUnit, Meter, OutputUnit,
|
||||
Resolution, ServiceTier, TokenKind, When,
|
||||
};
|
||||
|
||||
/// A flat price key as the grammar reads it: `{meter}[_above_{N}k_tokens][_{tier}]`.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct FlatKey {
|
||||
pub meter: Meter,
|
||||
pub band: Band,
|
||||
pub tier: Option<TierSuffix>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum TierSuffix {
|
||||
Service(ServiceTier),
|
||||
Batches,
|
||||
Decisions,
|
||||
}
|
||||
|
||||
impl FlatKey {
|
||||
pub fn parse(key: &str) -> Option<Self> {
|
||||
let (rest, tier) = split_tier(key);
|
||||
let (base, suffix) = split_above(rest)?;
|
||||
let (meter, band) = match suffix {
|
||||
Above::None => (meter(base)?, Band::Base),
|
||||
Above::PromptTokens(tokens) => (meter(base)?, Band::over_prompt_tokens(tokens)),
|
||||
Above::Seconds(seconds) => match meter(base)? {
|
||||
Meter::Input(InputUnit::VideoSecond { over_seconds: None }) => (
|
||||
Meter::Input(InputUnit::VideoSecond {
|
||||
over_seconds: NonZeroU64::new(seconds),
|
||||
}),
|
||||
Band::Base,
|
||||
),
|
||||
_ => return None,
|
||||
},
|
||||
};
|
||||
Some(Self { meter, band, tier })
|
||||
}
|
||||
|
||||
pub fn has_suffix(&self) -> bool {
|
||||
self.band != Band::Base || self.tier.is_some()
|
||||
}
|
||||
|
||||
pub fn channel_and_when(&self) -> (Channel, When) {
|
||||
let (channel, tier) = match self.tier {
|
||||
None => (Channel::Online, ServiceTier::Standard),
|
||||
Some(TierSuffix::Service(tier)) => (Channel::Online, tier),
|
||||
Some(TierSuffix::Batches) => (Channel::Batch, ServiceTier::Standard),
|
||||
Some(TierSuffix::Decisions) => (Channel::Decisions, ServiceTier::Standard),
|
||||
};
|
||||
(
|
||||
channel,
|
||||
When {
|
||||
tier,
|
||||
band: self.band,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum Above {
|
||||
None,
|
||||
PromptTokens(u64),
|
||||
Seconds(u64),
|
||||
}
|
||||
|
||||
fn split_tier(key: &str) -> (&str, Option<TierSuffix>) {
|
||||
let Some((head, tail)) = key.rsplit_once('_') else {
|
||||
return (key, None);
|
||||
};
|
||||
let tier = match tail {
|
||||
"priority" => TierSuffix::Service(ServiceTier::Priority),
|
||||
"flex" => TierSuffix::Service(ServiceTier::Flex),
|
||||
"ultrafast" => TierSuffix::Service(ServiceTier::Ultrafast),
|
||||
"balanced" => TierSuffix::Service(ServiceTier::Balanced),
|
||||
"batches" => TierSuffix::Batches,
|
||||
"decisions" => TierSuffix::Decisions,
|
||||
_ => return (key, None),
|
||||
};
|
||||
(head, Some(tier))
|
||||
}
|
||||
|
||||
fn split_above(key: &str) -> Option<(&str, Above)> {
|
||||
let Some((head, tail)) = key.rsplit_once("_above_") else {
|
||||
return Some((key, Above::None));
|
||||
};
|
||||
if tail == "1hr" {
|
||||
return Some((key, Above::None));
|
||||
}
|
||||
if let Some(thousands) = tail.strip_suffix("k_tokens") {
|
||||
return Some((
|
||||
head,
|
||||
Above::PromptTokens(number(thousands)?.checked_mul(1000)?),
|
||||
));
|
||||
}
|
||||
if let Some(tokens) = tail.strip_suffix("_tokens") {
|
||||
return Some((head, Above::PromptTokens(number(tokens)?)));
|
||||
}
|
||||
let seconds = tail.strip_suffix("s_interval")?;
|
||||
Some((head, Above::Seconds(number(seconds)?)))
|
||||
}
|
||||
|
||||
fn number(digits: &str) -> Option<u64> {
|
||||
(!digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit()))
|
||||
.then(|| digits.parse().ok())
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn meter(base: &str) -> Option<Meter> {
|
||||
let fixed = match base {
|
||||
"cost_per_second" => Some(Meter::WallClockSecond),
|
||||
"explicit_cache_read_input_token_cost" => Some(Meter::CacheRead {
|
||||
kind: TokenKind::Text,
|
||||
mode: CacheReadMode::Explicit,
|
||||
}),
|
||||
"annotation_cost_per_page" => Some(Meter::Feature(Feature::AnnotationPage)),
|
||||
"citation_cost_per_token" => Some(Meter::Feature(Feature::CitationToken)),
|
||||
"code_interpreter_cost_per_session" => {
|
||||
Some(Meter::Feature(Feature::CodeInterpreterSession))
|
||||
}
|
||||
"google_maps_grounding_cost_per_query" => {
|
||||
Some(Meter::Feature(Feature::GoogleMapsGroundingQuery))
|
||||
}
|
||||
"ocr_cost_per_credit" => Some(Meter::Feature(Feature::OcrCredit)),
|
||||
"ocr_cost_per_page" => Some(Meter::Feature(Feature::OcrPage)),
|
||||
_ => None,
|
||||
};
|
||||
if fixed.is_some() {
|
||||
return fixed;
|
||||
}
|
||||
if let Some(unit) = base.strip_prefix("input_cost_per_") {
|
||||
return input_unit(unit).map(Meter::Input);
|
||||
}
|
||||
if let Some(unit) = base.strip_prefix("output_cost_per_") {
|
||||
return output_unit(unit).map(Meter::Output);
|
||||
}
|
||||
if let Some(kind) = base
|
||||
.strip_prefix("cache_read_input_")
|
||||
.and_then(|rest| rest.strip_suffix("_cost"))
|
||||
{
|
||||
return Some(Meter::CacheRead {
|
||||
kind: token_kind(kind)?,
|
||||
mode: CacheReadMode::Implicit,
|
||||
});
|
||||
}
|
||||
let rest = base.strip_prefix("cache_creation_input_")?;
|
||||
let (kind, ttl) = match rest.strip_suffix("_cost_above_1hr") {
|
||||
Some(kind) => (kind, CacheTtl::OneHour),
|
||||
None => (rest.strip_suffix("_cost")?, CacheTtl::Default),
|
||||
};
|
||||
Some(Meter::CacheWrite {
|
||||
kind: token_kind(kind)?,
|
||||
ttl,
|
||||
})
|
||||
}
|
||||
|
||||
fn token_kind(unit: &str) -> Option<TokenKind> {
|
||||
match unit {
|
||||
"token" => Some(TokenKind::Text),
|
||||
"audio_token" => Some(TokenKind::Audio),
|
||||
"image_token" => Some(TokenKind::Image),
|
||||
"video_token" => Some(TokenKind::Video),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn input_unit(unit: &str) -> Option<InputUnit> {
|
||||
if let Some(kind) = token_kind(unit) {
|
||||
return Some(InputUnit::Token(kind));
|
||||
}
|
||||
match unit {
|
||||
"character" => Some(InputUnit::Character),
|
||||
"image" => Some(InputUnit::Image),
|
||||
"pixel" => Some(InputUnit::Pixel),
|
||||
"query" => Some(InputUnit::Query { over_results: None }),
|
||||
"request" => Some(InputUnit::Request),
|
||||
"second" => Some(InputUnit::Second),
|
||||
"text_message" => Some(InputUnit::TextMessage),
|
||||
"audio_per_second" => Some(InputUnit::AudioSecond),
|
||||
"video_per_second" => Some(InputUnit::VideoSecond { over_seconds: None }),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn output_unit(unit: &str) -> Option<OutputUnit> {
|
||||
if let Some(kind) = token_kind(unit) {
|
||||
return Some(OutputUnit::Token(kind));
|
||||
}
|
||||
match unit.split_once('_') {
|
||||
Some(("image", label)) => {
|
||||
return ImageSize::new(label).map(|size| OutputUnit::Image(Some(size)));
|
||||
}
|
||||
Some(("second", label)) => {
|
||||
return Resolution::new(label).map(|resolution| OutputUnit::Second(Some(resolution)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
match unit {
|
||||
"reasoning_token" => Some(OutputUnit::ReasoningToken),
|
||||
"character" => Some(OutputUnit::Character),
|
||||
"image" => Some(OutputUnit::Image(None)),
|
||||
"pixel" => Some(OutputUnit::Pixel),
|
||||
"second" => Some(OutputUnit::Second(None)),
|
||||
"video_per_second" => Some(OutputUnit::VideoSecond),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
const TEXT_INPUT: Meter = Meter::Input(InputUnit::Token(TokenKind::Text));
|
||||
const TEXT_OUTPUT: Meter = Meter::Output(OutputUnit::Token(TokenKind::Text));
|
||||
|
||||
fn key(meter: Meter, band: Band, tier: Option<TierSuffix>) -> FlatKey {
|
||||
FlatKey { meter, band, tier }
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::input_token("input_cost_per_token", key(TEXT_INPUT, Band::Base, None))]
|
||||
#[case::band_and_tier(
|
||||
"output_cost_per_token_above_272k_tokens_priority",
|
||||
key(
|
||||
TEXT_OUTPUT,
|
||||
Band::over_prompt_tokens(272_000),
|
||||
Some(TierSuffix::Service(ServiceTier::Priority))
|
||||
)
|
||||
)]
|
||||
#[case::band_without_k(
|
||||
"input_cost_per_token_above_128_tokens",
|
||||
key(TEXT_INPUT, Band::over_prompt_tokens(128), None)
|
||||
)]
|
||||
#[case::zero_band_is_base(
|
||||
"input_cost_per_token_above_0k_tokens",
|
||||
key(TEXT_INPUT, Band::Base, None)
|
||||
)]
|
||||
#[case::batch(
|
||||
"input_cost_per_token_batches",
|
||||
key(TEXT_INPUT, Band::Base, Some(TierSuffix::Batches))
|
||||
)]
|
||||
#[case::reasoning(
|
||||
"output_cost_per_reasoning_token",
|
||||
key(Meter::Output(OutputUnit::ReasoningToken), Band::Base, None)
|
||||
)]
|
||||
#[case::cache_read_audio(
|
||||
"cache_read_input_audio_token_cost",
|
||||
key(Meter::CacheRead { kind: TokenKind::Audio, mode: CacheReadMode::Implicit }, Band::Base, None)
|
||||
)]
|
||||
#[case::explicit_cache_read(
|
||||
"explicit_cache_read_input_token_cost",
|
||||
key(Meter::CacheRead { kind: TokenKind::Text, mode: CacheReadMode::Explicit }, Band::Base, None)
|
||||
)]
|
||||
#[case::cache_write_one_hour_band(
|
||||
"cache_creation_input_token_cost_above_1hr_above_200k_tokens",
|
||||
key(
|
||||
Meter::CacheWrite { kind: TokenKind::Text, ttl: CacheTtl::OneHour },
|
||||
Band::over_prompt_tokens(200_000),
|
||||
None
|
||||
)
|
||||
)]
|
||||
#[case::image_label(
|
||||
"output_cost_per_image_0.5K",
|
||||
key(
|
||||
Meter::Output(OutputUnit::Image(ImageSize::new("0.5K"))),
|
||||
Band::Base,
|
||||
None
|
||||
)
|
||||
)]
|
||||
#[case::image_token_is_a_token(
|
||||
"output_cost_per_image_token",
|
||||
key(Meter::Output(OutputUnit::Token(TokenKind::Image)), Band::Base, None)
|
||||
)]
|
||||
#[case::resolution(
|
||||
"output_cost_per_second_720p",
|
||||
key(
|
||||
Meter::Output(OutputUnit::Second(Resolution::new("720p"))),
|
||||
Band::Base,
|
||||
None
|
||||
)
|
||||
)]
|
||||
#[case::video_interval(
|
||||
"input_cost_per_video_per_second_above_8s_interval",
|
||||
key(Meter::Input(InputUnit::VideoSecond { over_seconds: NonZeroU64::new(8) }), Band::Base, None)
|
||||
)]
|
||||
#[case::feature_batch(
|
||||
"ocr_cost_per_page_batches",
|
||||
key(
|
||||
Meter::Feature(Feature::OcrPage),
|
||||
Band::Base,
|
||||
Some(TierSuffix::Batches)
|
||||
)
|
||||
)]
|
||||
#[case::wall_clock("cost_per_second", key(Meter::WallClockSecond, Band::Base, None))]
|
||||
fn parses_every_key_shape(#[case] text: &str, #[case] expected: FlatKey) {
|
||||
assert_eq!(FlatKey::parse(text), Some(expected));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::typo("input_cost_per_tokne")]
|
||||
#[case::unused_meter("cost_per_token")]
|
||||
#[case::other_currency("input_dbu_cost_per_token")]
|
||||
#[case::cache_hit_alias("input_cost_per_token_cache_hit")]
|
||||
#[case::non_numeric_band("input_cost_per_token_above_xk_tokens")]
|
||||
#[case::interval_on_a_token_meter("input_cost_per_token_above_8s_interval")]
|
||||
#[case::read_has_no_ttl("cache_read_input_token_cost_above_1hr")]
|
||||
#[case::cache_unit_must_be_token("cache_read_input_character_cost")]
|
||||
#[case::bad_image_size("output_cost_per_image_big")]
|
||||
#[case::bad_resolution("output_cost_per_second_hd")]
|
||||
#[case::unknown_feature("telepathy_cost_per_page")]
|
||||
#[case::empty("")]
|
||||
fn rejects_keys_outside_the_grammar(#[case] text: &str) {
|
||||
assert_eq!(FlatKey::parse(text), None);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::plain("input_cost_per_token", (Channel::Online, When::default()))]
|
||||
#[case::service_tier(
|
||||
"input_cost_per_token_flex",
|
||||
(Channel::Online, When { tier: ServiceTier::Flex, band: Band::Base })
|
||||
)]
|
||||
#[case::batch(
|
||||
"input_cost_per_token_above_200k_tokens_batches",
|
||||
(Channel::Batch, When { tier: ServiceTier::Standard, band: Band::over_prompt_tokens(200_000) })
|
||||
)]
|
||||
#[case::decisions("input_cost_per_token_decisions", (Channel::Decisions, When::default()))]
|
||||
fn batch_and_decisions_pick_a_channel_not_a_tier(
|
||||
#[case] text: &str,
|
||||
#[case] expected: (Channel, When),
|
||||
) {
|
||||
assert_eq!(FlatKey::parse(text).unwrap().channel_and_when(), expected);
|
||||
}
|
||||
}
|
||||
128
litellm-rust/crates/model-catalog/src/wire/lax.rs
Normal file
128
litellm-rust/crates/model-catalog/src/wire/lax.rs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
use std::fmt;
|
||||
|
||||
use serde::Deserializer;
|
||||
use serde::de::{self, Visitor};
|
||||
use serde_with::DeserializeAs;
|
||||
|
||||
pub(crate) struct LaxI64;
|
||||
|
||||
pub(crate) struct Count;
|
||||
|
||||
impl<'de> DeserializeAs<'de, i64> for LaxI64 {
|
||||
fn deserialize_as<D: Deserializer<'de>>(deserializer: D) -> Result<i64, D::Error> {
|
||||
deserializer.deserialize_any(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Visitor<'_> for LaxI64 {
|
||||
type Value = i64;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("an integer in the i64 range")
|
||||
}
|
||||
|
||||
fn visit_i64<E: de::Error>(self, value: i64) -> Result<i64, E> {
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn visit_u64<E: de::Error>(self, value: u64) -> Result<i64, E> {
|
||||
i64::try_from(value).map_err(E::custom)
|
||||
}
|
||||
|
||||
fn visit_f64<E: de::Error>(self, value: f64) -> Result<i64, E> {
|
||||
integral_float(value).ok_or_else(|| E::custom("expected an integer in the i64 range"))
|
||||
}
|
||||
|
||||
fn visit_str<E: de::Error>(self, value: &str) -> Result<i64, E> {
|
||||
integer_string(value.trim())
|
||||
.ok_or_else(|| E::custom("expected an integer in the i64 range"))
|
||||
}
|
||||
|
||||
fn visit_bool<E: de::Error>(self, value: bool) -> Result<i64, E> {
|
||||
Ok(i64::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> DeserializeAs<'de, u64> for Count {
|
||||
fn deserialize_as<D: Deserializer<'de>>(deserializer: D) -> Result<u64, D::Error> {
|
||||
let count = LaxI64::deserialize_as(deserializer)?;
|
||||
u64::try_from(count).map_err(|_| {
|
||||
de::Error::invalid_value(de::Unexpected::Signed(count), &"a count of zero or more")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn integer_string(value: &str) -> Option<i64> {
|
||||
let integer = match value.split_once('.') {
|
||||
Some((integer, fraction)) => {
|
||||
if fraction.is_empty() || !fraction.bytes().all(|byte| byte == b'0') {
|
||||
return None;
|
||||
}
|
||||
integer
|
||||
}
|
||||
None => value,
|
||||
};
|
||||
if integer.starts_with('_') || integer.ends_with('_') || integer.contains("__") {
|
||||
return None;
|
||||
}
|
||||
let digits = integer.strip_prefix(['+', '-']).unwrap_or(integer);
|
||||
if digits.is_empty()
|
||||
|| digits.starts_with('_')
|
||||
|| !digits
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_digit() || byte == b'_')
|
||||
{
|
||||
return None;
|
||||
}
|
||||
integer.replace('_', "").parse().ok()
|
||||
}
|
||||
|
||||
fn integral_float(value: f64) -> Option<i64> {
|
||||
(value.is_finite()
|
||||
&& value.fract() == 0.0
|
||||
&& value >= i64::MIN as f64
|
||||
&& value < -(i64::MIN as f64))
|
||||
.then_some(value as i64)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::rstest;
|
||||
use serde_json::{Value, json};
|
||||
use serde_with::de::DeserializeAsWrap;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn count(value: Value) -> Option<u64> {
|
||||
DeserializeAsWrap::<u64, Count>::deserialize(value)
|
||||
.ok()
|
||||
.map(DeserializeAsWrap::into_inner)
|
||||
}
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[rstest]
|
||||
#[case::integer(json!(128000), Some(128_000))]
|
||||
#[case::whole_float(json!(128000.0), Some(128_000))]
|
||||
#[case::string(json!(" 8192 "), Some(8192))]
|
||||
#[case::separated(json!("1_000"), Some(1000))]
|
||||
#[case::trailing_zeros(json!("2.000"), Some(2))]
|
||||
#[case::boolean(json!(true), Some(1))]
|
||||
#[case::maximum(json!(i64::MAX.to_string()), Some(i64::MAX as u64))]
|
||||
#[case::negative(json!(-5), None)]
|
||||
#[case::fraction(json!(2.5), None)]
|
||||
#[case::precise_fraction(json!("1.0000000000000001"), None)]
|
||||
#[case::exponent(json!("1e3"), None)]
|
||||
#[case::missing_fraction(json!("2."), None)]
|
||||
#[case::repeated_separator(json!("2__0"), None)]
|
||||
#[case::above_i64(json!(u64::MAX), None)]
|
||||
#[case::float_above_i64(json!(9_223_372_036_854_775_808.0), None)]
|
||||
#[case::word(json!("8k"), None)]
|
||||
#[case::object(json!({}), None)]
|
||||
fn counts_accept_whole_numbers_however_they_are_written(
|
||||
#[case] written: Value,
|
||||
#[case] expected: Option<u64>,
|
||||
) {
|
||||
assert_eq!(count(written), expected);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,303 +1,240 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::Path;
|
||||
|
||||
use litellm_model_catalog::{AliasIssue, Catalog, Error, IntegrityLimits, Mode, Provenance};
|
||||
use litellm_model_catalog::{
|
||||
Catalog, IntegrityLimits, IntegrityViolation, InvalidDocument, Mode, Problem,
|
||||
};
|
||||
use rstest::{fixture, rstest};
|
||||
use serde_json::json;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
const ALPHA_FIXTURE: &[u8] = br#"{
|
||||
"sample_spec":{"explanation":"example"},
|
||||
"sample_spec":{"explanation":"example","input_cost_per_token":"not checked"},
|
||||
"fallback_generalizations":{"rules":[{"name":"family","pattern":"^new-","model_info":{"mode":"chat"}}]},
|
||||
"Alpha":{"litellm_provider":"test","aliases":["short"],"price":0,"enabled":false,
|
||||
"optional":null,"unknown":{"nested":[1,{"x":true}]}}
|
||||
}"#;
|
||||
|
||||
#[fixture]
|
||||
fn repo_root() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../..")
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn current_catalog(repo_root: PathBuf) -> Catalog {
|
||||
let body = std::fs::read(repo_root.join("model_prices_and_context_window.json")).unwrap();
|
||||
Catalog::parse(&body, Provenance::default()).unwrap()
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn backup_catalog(repo_root: PathBuf) -> Catalog {
|
||||
let body = std::fs::read(repo_root.join("litellm/model_prices_and_context_window_backup.json"))
|
||||
.unwrap();
|
||||
Catalog::parse(&body, Provenance::default()).unwrap()
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn fixture_catalog() -> Catalog {
|
||||
Catalog::parse(
|
||||
ALPHA_FIXTURE,
|
||||
Provenance {
|
||||
source: Some("fixture".into()),
|
||||
revision: Some("rev".into()),
|
||||
etag: None,
|
||||
},
|
||||
)
|
||||
.unwrap()
|
||||
Catalog::parse(ALPHA_FIXTURE).unwrap()
|
||||
}
|
||||
|
||||
fn catalog(document: Value) -> Catalog {
|
||||
Catalog::parse(document.to_string().as_bytes()).unwrap()
|
||||
}
|
||||
|
||||
fn problems(catalog: &Catalog) -> Vec<String> {
|
||||
catalog.problems().iter().map(ToString::to_string).collect()
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::decisions("decisions", Mode::Decisions)]
|
||||
#[case::legacy_evaluation("evaluation", Mode::Evaluation)]
|
||||
fn parses_decision_modes(#[case] mode: &str, #[case] expected: Mode) {
|
||||
let source = json!({"Test":{"litellm_provider":"test","mode":mode}}).to_string();
|
||||
let catalog = Catalog::parse(source.as_bytes(), Provenance::default()).unwrap();
|
||||
let catalog = catalog(json!({"Test": {"litellm_provider": "test", "mode": mode}}));
|
||||
|
||||
assert_eq!(
|
||||
catalog.lookup("Test").unwrap().entry.info().mode,
|
||||
catalog
|
||||
.lookup("Test")
|
||||
.unwrap()
|
||||
.entry
|
||||
.model()
|
||||
.unwrap()
|
||||
.attributes
|
||||
.mode,
|
||||
Some(expected)
|
||||
);
|
||||
assert_eq!(serde_json::to_value(expected).unwrap(), json!(mode));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[ignore]
|
||||
fn preserves_fields_and_metadata(fixture_catalog: Catalog) {
|
||||
let catalog = fixture_catalog;
|
||||
let entry = catalog.lookup("SHORT").unwrap();
|
||||
assert_eq!(entry.canonical_key, "Alpha");
|
||||
assert_eq!(entry.matched_key, "short");
|
||||
assert_eq!(entry.entry.field("price"), Some(&json!(0)));
|
||||
assert_eq!(entry.entry.field("enabled"), Some(&json!(false)));
|
||||
assert_eq!(entry.entry.field("optional"), Some(&json!(null)));
|
||||
assert_eq!(entry.entry.field("missing"), None);
|
||||
fn unknown_keys_are_kept_raw_and_listed_as_unrecognized(fixture_catalog: Catalog) {
|
||||
let entry = fixture_catalog.lookup("Alpha").unwrap().entry;
|
||||
|
||||
assert_eq!(
|
||||
entry.entry.field("unknown"),
|
||||
entry.field("unknown"),
|
||||
Some(&json!({"nested":[1,{"x":true}]}))
|
||||
);
|
||||
assert_eq!(entry.entry.field("aliases"), None);
|
||||
assert_eq!(entry.entry.info().litellm_provider.as_deref(), Some("test"));
|
||||
assert_eq!(
|
||||
catalog.sample_spec(),
|
||||
Some(&json!({"explanation":"example"}))
|
||||
fixture_catalog.unrecognized().collect::<BTreeSet<_>>(),
|
||||
BTreeSet::from([
|
||||
("Alpha", "enabled"),
|
||||
("Alpha", "price"),
|
||||
("Alpha", "unknown")
|
||||
])
|
||||
);
|
||||
assert_eq!(catalog.fallback_rules().unwrap().len(), 1);
|
||||
assert_eq!(catalog.provenance().revision.as_deref(), Some("rev"));
|
||||
assert_eq!(catalog.model_count(), 1);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[ignore]
|
||||
fn snapshot_does_not_borrow_source() {
|
||||
let mut source = ALPHA_FIXTURE.to_vec();
|
||||
let catalog = Catalog::parse(&source, Provenance::default()).unwrap();
|
||||
source.fill(b' ');
|
||||
fn fields_sample_spec_and_fallback_rules_pass_through_as_written(fixture_catalog: Catalog) {
|
||||
let found = fixture_catalog.lookup("SHORT").unwrap();
|
||||
|
||||
let entry = catalog.lookup("short").unwrap();
|
||||
assert_eq!(entry.canonical_key, "Alpha");
|
||||
assert_eq!(entry.entry.field("price"), Some(&json!(0)));
|
||||
assert_eq!(found.canonical_key, "Alpha");
|
||||
assert_eq!(found.matched_key, "short");
|
||||
assert_eq!(found.entry.field("optional"), Some(&json!(null)));
|
||||
assert_eq!(found.entry.field("aliases"), Some(&json!(["short"])));
|
||||
assert_eq!(
|
||||
fixture_catalog.sample_spec(),
|
||||
Some(&json!({"explanation":"example","input_cost_per_token":"not checked"}))
|
||||
);
|
||||
assert_eq!(fixture_catalog.fallback_rules().unwrap()[0].name, "family");
|
||||
assert_eq!(fixture_catalog.model_count(), 1);
|
||||
assert!(fixture_catalog.problems().is_empty());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("Shared", "First")]
|
||||
#[case("Second", "Second")]
|
||||
#[case("shared", "Second")]
|
||||
#[case("FIRST", "First")]
|
||||
#[case("sHaReD", "Second")]
|
||||
#[ignore]
|
||||
fn alias_collisions_and_case_fallback_follow_entry_order(
|
||||
#[case] lookup: &str,
|
||||
#[case] expected: &str,
|
||||
) {
|
||||
let catalog = Catalog::parse(
|
||||
br#"{
|
||||
"First":{"aliases":["Shared","Second","first"],"value":1},
|
||||
"Second":{"aliases":["Shared","sHaReD"],"value":2},
|
||||
"SHARED":{"value":3}
|
||||
}"#,
|
||||
Provenance::default(),
|
||||
)
|
||||
.unwrap();
|
||||
fn a_bad_entry_is_reported_by_model_and_key_and_kept_raw() {
|
||||
let catalog = catalog(json!({
|
||||
"Good": {"litellm_provider": "test"},
|
||||
"Bad": {"litellm_provider": "test", "mode": "chatt", "input_cost_per_token": "free"},
|
||||
"Worse": {"mode": "chat"}
|
||||
}));
|
||||
let problems = problems(&catalog);
|
||||
|
||||
assert_eq!(problems.len(), 3, "{problems:?}");
|
||||
assert!(problems[0].starts_with("Bad.input_cost_per_token: invalid value"));
|
||||
assert!(problems[1].starts_with("Bad.mode: unknown variant `chatt`"));
|
||||
assert_eq!(problems[2], "Worse.litellm_provider is required");
|
||||
assert!(catalog.lookup("Good").unwrap().entry.model().is_some());
|
||||
let bad = catalog.lookup("Bad").unwrap().entry;
|
||||
assert!(bad.model().is_none());
|
||||
assert_eq!(bad.field("mode"), Some(&json!("chatt")));
|
||||
assert_eq!(catalog.model_count(), 3);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::alias_names_a_model(
|
||||
json!({"First": {"litellm_provider": "t", "aliases": ["Second"]}, "Second": {"litellm_provider": "t"}}),
|
||||
r#"First: alias "Second" is already a model name"#
|
||||
)]
|
||||
#[case::alias_claimed_twice(
|
||||
json!({"First": {"litellm_provider": "t", "aliases": ["shared"]}, "Second": {"litellm_provider": "t", "aliases": ["shared"]}}),
|
||||
r#"Second: alias "shared" already belongs to First"#
|
||||
)]
|
||||
#[case::aliases_not_a_list(
|
||||
json!({"a": {"litellm_provider": "t", "aliases": "bad"}}),
|
||||
"a.aliases: invalid type: string \"bad\", expected a sequence"
|
||||
)]
|
||||
#[case::entry_not_object(json!({"a": 1}), "a must be a JSON object")]
|
||||
#[case::unknown_timezone(
|
||||
json!({"a": {"litellm_provider": "t", "off_peak_pricing": {
|
||||
"windows": [{"hours_utc": "00:00-01:00"}],
|
||||
"weekday_timezone": "Not/AZone",
|
||||
"input_cost_per_token": 1e-6
|
||||
}}}),
|
||||
r#"a.off_peak_pricing.weekday_timezone: "Not/AZone" is not an IANA time zone"#
|
||||
)]
|
||||
fn document_level_problems_are_reported(#[case] document: Value, #[case] expected: &str) {
|
||||
assert_eq!(problems(&catalog(document)), [expected]);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn the_first_owner_keeps_a_contested_alias() {
|
||||
let catalog = catalog(json!({
|
||||
"First": {"litellm_provider": "t", "aliases": ["shared"]},
|
||||
"Second": {"litellm_provider": "t", "aliases": ["shared"]}
|
||||
}));
|
||||
assert_eq!(catalog.lookup("shared").unwrap().canonical_key, "First");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn a_known_timezone_is_not_a_problem_and_keeps_the_model() {
|
||||
let catalog = catalog(json!({"a": {"litellm_provider": "t", "off_peak_pricing": {
|
||||
"windows": [{"hours_utc": "00:00-01:00", "weekdays": [6, 7]}],
|
||||
"weekday_timezone": " Asia/Shanghai ",
|
||||
"input_cost_per_token": 1e-6
|
||||
}}}));
|
||||
assert!(catalog.problems().is_empty());
|
||||
assert!(catalog.lookup("a").unwrap().entry.model().is_some());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("foo", "fOO")]
|
||||
#[case("FOO", "fOO")]
|
||||
#[case("Foo", "Foo")]
|
||||
fn case_insensitive_lookup_prefers_the_later_entry(#[case] lookup: &str, #[case] expected: &str) {
|
||||
let catalog =
|
||||
catalog(json!({"Foo": {"litellm_provider": "t"}, "fOO": {"litellm_provider": "t"}}));
|
||||
assert_eq!(catalog.lookup(lookup).unwrap().canonical_key, expected);
|
||||
assert_eq!(catalog.alias_count(), 3);
|
||||
assert!(
|
||||
catalog
|
||||
.alias_issues()
|
||||
.contains(&AliasIssue::CanonicalCollision {
|
||||
model: "First".into(),
|
||||
alias: "Second".into(),
|
||||
})
|
||||
);
|
||||
assert!(
|
||||
catalog
|
||||
.alias_issues()
|
||||
.contains(&AliasIssue::AliasCollision {
|
||||
model: "Second".into(),
|
||||
alias: "Shared".into(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn json_entry_order_controls_alias_ownership_and_case_fallback() {
|
||||
let forward = Catalog::parse(
|
||||
br#"{
|
||||
"Alpha":{"aliases":["shared"]},
|
||||
"Beta":{"aliases":["shared"]},
|
||||
"Foo":{},
|
||||
"fOO":{}
|
||||
}"#,
|
||||
Provenance::default(),
|
||||
)
|
||||
.unwrap();
|
||||
let reversed = Catalog::parse(
|
||||
br#"{
|
||||
"fOO":{},
|
||||
"Foo":{},
|
||||
"Beta":{"aliases":["shared"]},
|
||||
"Alpha":{"aliases":["shared"]}
|
||||
}"#,
|
||||
Provenance::default(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(forward.lookup("shared").unwrap().canonical_key, "Alpha");
|
||||
assert_eq!(reversed.lookup("shared").unwrap().canonical_key, "Beta");
|
||||
assert_eq!(forward.lookup("foo").unwrap().canonical_key, "fOO");
|
||||
assert_eq!(reversed.lookup("foo").unwrap().canonical_key, "Foo");
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ValidationOutcome {
|
||||
Ok,
|
||||
Shrunk,
|
||||
BelowMinimum,
|
||||
InvalidRatio,
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(
|
||||
IntegrityLimits {
|
||||
reference_model_count: 2,
|
||||
min_model_count: 1,
|
||||
min_reference_ratio: 0.5,
|
||||
},
|
||||
ValidationOutcome::Ok
|
||||
#[case::half_of_two(2, 1, 0.5, Ok(()))]
|
||||
#[case::under_half_of_three(
|
||||
3,
|
||||
1,
|
||||
0.5,
|
||||
Err(IntegrityViolation::Shrunk { actual: 1, reference: 3, ratio: 0.5 })
|
||||
)]
|
||||
#[case(
|
||||
IntegrityLimits {
|
||||
reference_model_count: 3,
|
||||
min_model_count: 1,
|
||||
min_reference_ratio: 0.5,
|
||||
},
|
||||
ValidationOutcome::Shrunk
|
||||
#[case::below_minimum(
|
||||
1,
|
||||
2,
|
||||
0.5,
|
||||
Err(IntegrityViolation::BelowMinimum { actual: 1, minimum: 2 })
|
||||
)]
|
||||
#[case(
|
||||
IntegrityLimits {
|
||||
reference_model_count: 0,
|
||||
min_model_count: 2,
|
||||
min_reference_ratio: 0.5,
|
||||
},
|
||||
ValidationOutcome::BelowMinimum
|
||||
)]
|
||||
#[case(
|
||||
IntegrityLimits {
|
||||
reference_model_count: 0,
|
||||
min_model_count: 0,
|
||||
min_reference_ratio: f64::NAN,
|
||||
},
|
||||
ValidationOutcome::InvalidRatio
|
||||
)]
|
||||
#[ignore]
|
||||
fn integrity_uses_canonical_count_and_strict_shrink_boundary(
|
||||
#[case] limits: IntegrityLimits,
|
||||
#[case] expected: ValidationOutcome,
|
||||
fn integrity_limits_count_canonical_models_not_aliases(
|
||||
#[case] reference: usize,
|
||||
#[case] min_model_count: usize,
|
||||
#[case] min_reference_ratio: f64,
|
||||
#[case] expected: Result<(), IntegrityViolation>,
|
||||
) {
|
||||
let catalog = Catalog::parse(
|
||||
br#"{"sample_spec":{},"fallback_generalizations":{"rules":[]},"a":{"aliases":["b","c"]}}"#,
|
||||
Provenance::default(),
|
||||
)
|
||||
.unwrap();
|
||||
let actual = catalog.validate(limits);
|
||||
match expected {
|
||||
ValidationOutcome::Ok => assert!(actual.is_ok()),
|
||||
ValidationOutcome::Shrunk => {
|
||||
assert!(matches!(actual, Err(Error::Shrunk { actual: 1, .. })))
|
||||
}
|
||||
ValidationOutcome::BelowMinimum => {
|
||||
assert!(matches!(actual, Err(Error::BelowMinimum { actual: 1, .. })))
|
||||
}
|
||||
ValidationOutcome::InvalidRatio => assert!(matches!(actual, Err(Error::InvalidRatio))),
|
||||
}
|
||||
let fetched = catalog(json!({"a": {"litellm_provider": "t", "aliases": ["b", "c"]}}));
|
||||
let limits = IntegrityLimits::new(min_model_count, min_reference_ratio).unwrap();
|
||||
|
||||
assert_eq!(fetched.check_size(reference, limits), expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::nan(f64::NAN)]
|
||||
#[case::above_one(1.5)]
|
||||
#[case::negative(-0.1)]
|
||||
fn a_reference_ratio_outside_zero_to_one_is_not_a_limit(#[case] ratio: f64) {
|
||||
assert_eq!(IntegrityLimits::new(0, ratio), None);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum MalformedOutcome {
|
||||
enum Malformed {
|
||||
Empty,
|
||||
Json,
|
||||
EntryNotObject,
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::empty(b"{}", MalformedOutcome::Empty)]
|
||||
#[case::invalid_json(b"{", MalformedOutcome::Json)]
|
||||
#[case::entry_not_object(br#"{"a":1}"#, MalformedOutcome::EntryNotObject)]
|
||||
#[case::empty(b"{}", Malformed::Empty)]
|
||||
#[case::only_sections(br#"{"sample_spec":{}}"#, Malformed::Empty)]
|
||||
#[case::invalid_json(b"{", Malformed::Json)]
|
||||
#[case::fallback_rules_missing(
|
||||
br#"{"fallback_generalizations":{},"a":{}}"#,
|
||||
MalformedOutcome::Json
|
||||
br#"{"fallback_generalizations":{},"a":{"litellm_provider":"t"}}"#,
|
||||
Malformed::Json
|
||||
)]
|
||||
#[ignore]
|
||||
fn malformed_input_and_aliases_have_typed_outcomes(
|
||||
#[case] body: &[u8],
|
||||
#[case] expected: MalformedOutcome,
|
||||
) {
|
||||
let actual = Catalog::parse(body, Provenance::default());
|
||||
fn malformed_documents_are_rejected_whole(#[case] body: &[u8], #[case] expected: Malformed) {
|
||||
let actual = Catalog::parse(body);
|
||||
match expected {
|
||||
MalformedOutcome::Empty => assert!(matches!(actual, Err(Error::Empty))),
|
||||
MalformedOutcome::Json => assert!(matches!(actual, Err(Error::Json(_)))),
|
||||
MalformedOutcome::EntryNotObject => {
|
||||
assert!(matches!(actual, Err(Error::EntryNotObject { .. })))
|
||||
}
|
||||
Malformed::Empty => assert!(matches!(actual, Err(InvalidDocument::Empty))),
|
||||
Malformed::Json => assert!(matches!(actual, Err(InvalidDocument::Json(_)))),
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[ignore]
|
||||
fn invalid_aliases_are_reported_not_fatal() {
|
||||
let catalog = Catalog::parse(
|
||||
br#"{"a":{"aliases":"bad"},"b":{"aliases":[9,"ok"]}}"#,
|
||||
Provenance::default(),
|
||||
fn the_checked_in_registry_parses_with_no_problems_and_every_key_understood() {
|
||||
let body = std::fs::read(
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../model_prices_and_context_window.json"),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
catalog.alias_issues(),
|
||||
&[
|
||||
AliasIssue::InvalidList { model: "a".into() },
|
||||
AliasIssue::InvalidName { model: "b".into() },
|
||||
]
|
||||
);
|
||||
assert_eq!(catalog.lookup("ok").unwrap().canonical_key, "b");
|
||||
assert!(catalog.lookup("missing").is_none());
|
||||
}
|
||||
let catalog = Catalog::parse(&body).unwrap();
|
||||
let problems: Vec<&Problem> = catalog.problems().iter().collect();
|
||||
let unrecognized: Vec<_> = catalog.unrecognized().collect();
|
||||
|
||||
#[rstest]
|
||||
#[ignore]
|
||||
fn parses_current_and_packaged_catalogs_against_independent_baseline(
|
||||
current_catalog: Catalog,
|
||||
backup_catalog: Catalog,
|
||||
) {
|
||||
assert!(current_catalog.model_count() > 0);
|
||||
assert!(backup_catalog.model_count() > 0);
|
||||
assert!(current_catalog.sample_spec().is_some());
|
||||
assert!(backup_catalog.sample_spec().is_some());
|
||||
// Snapshot from 2026-09-23; the backup file mirrors the current file and cannot detect shrinkage.
|
||||
assert!(problems.is_empty(), "{problems:#?}");
|
||||
assert!(
|
||||
unrecognized.is_empty(),
|
||||
"keys no type reads: {unrecognized:#?}"
|
||||
);
|
||||
assert!(
|
||||
catalog
|
||||
.fallback_rules()
|
||||
.is_some_and(|rules| !rules.is_empty())
|
||||
);
|
||||
// Snapshot from 2026-09-23, an independent baseline that catches catalog shrinkage.
|
||||
const REFERENCE_MODEL_COUNT: usize = 4303;
|
||||
current_catalog
|
||||
.validate(IntegrityLimits {
|
||||
reference_model_count: REFERENCE_MODEL_COUNT,
|
||||
min_model_count: 50,
|
||||
min_reference_ratio: 0.9,
|
||||
})
|
||||
.unwrap();
|
||||
assert!(current_catalog.model_names().all(|name| {
|
||||
let entry = current_catalog.lookup(name).unwrap().entry;
|
||||
entry.info().litellm_provider.is_some() == entry.field("litellm_provider").is_some()
|
||||
}));
|
||||
assert!(catalog.model_count() as f64 >= REFERENCE_MODEL_COUNT as f64 * 0.9);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
import itertools
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
from litellm.litellm_core_utils.llm_cost_calc.utils import _get_token_base_cost
|
||||
from litellm.types.utils import Usage
|
||||
|
||||
REPO_ROOT: Final = Path(__file__).resolve().parents[4]
|
||||
TOKEN_RATE_KEY: Final = re.compile(
|
||||
r"^(input_cost_per_token|output_cost_per_token|cache_read_input_token_cost"
|
||||
r"|cache_creation_input_token_cost(_above_1hr)?)"
|
||||
r"(_above_(\d+)(k?)_tokens)?(_(priority|flex|ultrafast|balanced|batches|decisions))?$"
|
||||
)
|
||||
SERVICE_TIERS: Final = ("priority", "flex", "ultrafast", "balanced")
|
||||
LARGEST_PROMPT: Final = 10_000_000
|
||||
INCLUSIVE_PROVIDERS: Final = frozenset({"xai"})
|
||||
|
||||
|
||||
def token_rates(entry: dict[str, object]) -> dict[str, object]:
|
||||
return {
|
||||
key: value
|
||||
for key, value in entry.items()
|
||||
if value is not None and (key == "tiered_pricing" or TOKEN_RATE_KEY.match(key))
|
||||
}
|
||||
|
||||
|
||||
def band_floors(info: dict[str, object]) -> frozenset[int]:
|
||||
matches: Final = (TOKEN_RATE_KEY.match(key) for key in info)
|
||||
flat: Final = frozenset(
|
||||
int(match.group(4)) * (1000 if match.group(5) else 1)
|
||||
for match in matches
|
||||
if match is not None and match.group(4) is not None
|
||||
)
|
||||
rows: Final = info.get("tiered_pricing")
|
||||
tiers: Final = (
|
||||
frozenset(int(row["range"][0]) for row in rows if isinstance(row, dict) and "range" in row)
|
||||
if isinstance(rows, list)
|
||||
else frozenset()
|
||||
)
|
||||
return flat | tiers
|
||||
|
||||
|
||||
def tiers_in(info: dict[str, object]) -> tuple[str | None, ...]:
|
||||
return (None, *(tier for tier in SERVICE_TIERS if any(key.endswith(f"_{tier}") for key in info)))
|
||||
|
||||
|
||||
def prompts(info: dict[str, object]) -> tuple[int, ...]:
|
||||
floors: Final = band_floors(info)
|
||||
return tuple(sorted(({1, LARGEST_PROMPT} | floors | {floor + 1 for floor in floors}) - {0}))
|
||||
|
||||
|
||||
def check(info: dict[str, object], tier: str | None, prompt: int, inclusive: bool) -> list[object]:
|
||||
usage: Final = Usage(prompt_tokens=prompt, completion_tokens=1, total_tokens=prompt + 1)
|
||||
costs: Final = _get_token_base_cost(info, usage, service_tier=tier, threshold_is_inclusive=inclusive)
|
||||
return [tier, prompt, list(costs)]
|
||||
|
||||
|
||||
def shape(info: dict[str, object], inclusive: bool) -> str:
|
||||
rows: Final = info.get("tiered_pricing")
|
||||
row_keys: Final = [sorted(row) for row in rows if isinstance(row, dict)] if isinstance(rows, list) else []
|
||||
return json.dumps([sorted(info), row_keys, inclusive])
|
||||
|
||||
|
||||
def checks(info: dict[str, object], inclusive: bool) -> list[list[object]]:
|
||||
return [check(info, tier, prompt, inclusive) for tier, prompt in itertools.product(tiers_in(info), prompts(info))]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
registry: Final = json.loads((REPO_ROOT / "model_prices_and_context_window.json").read_text())
|
||||
entries: Final = tuple(
|
||||
(token_rates(entry), entry.get("litellm_provider") in INCLUSIVE_PROVIDERS)
|
||||
for name, entry in registry.items()
|
||||
if name not in ("sample_spec", "fallback_generalizations") and isinstance(entry, dict)
|
||||
)
|
||||
by_shape: Final = {shape(info, inclusive): (info, inclusive) for info, inclusive in reversed(entries) if info}
|
||||
models: Final = tuple(
|
||||
json.dumps({"info": info, "inclusive": inclusive, "checks": checks(info, inclusive)})
|
||||
for info, inclusive in by_shape.values()
|
||||
)
|
||||
revision: Final = subprocess.check_output(("git", "rev-parse", "HEAD"), text=True, cwd=REPO_ROOT).strip()
|
||||
Path(__file__).with_name("price_lookup_reference.json").write_text(
|
||||
f'{{"python_reference_commit": "{revision}", "models": [\n' + ",\n".join(models) + "\n]}\n"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
83
litellm-rust/crates/model-catalog/tests/price_lookup.rs
Normal file
83
litellm-rust/crates/model-catalog/tests/price_lookup.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
use litellm_cost::money::UnitPrice;
|
||||
use litellm_cost::price::{
|
||||
CacheReadMode, CacheTtl, InputUnit, Meter, OutputUnit, Prices, ServiceTier, TokenKind,
|
||||
};
|
||||
use litellm_model_catalog::wire::{EntryWire, Fields};
|
||||
use rstest::rstest;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Reference {
|
||||
models: Vec<ReferenceModel>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ReferenceModel {
|
||||
info: Fields,
|
||||
inclusive: bool,
|
||||
checks: Vec<(Option<String>, u64, [f64; 5])>,
|
||||
}
|
||||
|
||||
const METERS: [Meter; 5] = [
|
||||
Meter::Input(InputUnit::Token(TokenKind::Text)),
|
||||
Meter::Output(OutputUnit::Token(TokenKind::Text)),
|
||||
Meter::CacheWrite {
|
||||
kind: TokenKind::Text,
|
||||
ttl: CacheTtl::Default,
|
||||
},
|
||||
Meter::CacheWrite {
|
||||
kind: TokenKind::Text,
|
||||
ttl: CacheTtl::OneHour,
|
||||
},
|
||||
Meter::CacheRead {
|
||||
kind: TokenKind::Text,
|
||||
mode: CacheReadMode::Implicit,
|
||||
},
|
||||
];
|
||||
|
||||
fn service_tier(name: Option<&str>) -> ServiceTier {
|
||||
match name {
|
||||
None => ServiceTier::Standard,
|
||||
Some("priority") => ServiceTier::Priority,
|
||||
Some("flex") => ServiceTier::Flex,
|
||||
Some("ultrafast") => ServiceTier::Ultrafast,
|
||||
Some("balanced") => ServiceTier::Balanced,
|
||||
Some(other) => panic!("the reference uses an unknown service tier {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn mismatch(
|
||||
prices: &Prices,
|
||||
inclusive: bool,
|
||||
(tier_name, prompt, python): &(Option<String>, u64, [f64; 5]),
|
||||
) -> Option<String> {
|
||||
let tier = service_tier(tier_name.as_deref());
|
||||
let band = prices.band(tier, *prompt, inclusive);
|
||||
let rust = METERS
|
||||
.each_ref()
|
||||
.map(|meter| prices.rate(meter, tier, band).map_or(0.0, UnitPrice::get));
|
||||
(rust != *python)
|
||||
.then(|| format!("tier={tier_name:?} prompt={prompt}: rust {rust:?}, python {python:?}"))
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn band_and_rate_match_python_token_base_cost() {
|
||||
let reference: Reference =
|
||||
serde_json::from_str(include_str!("price_lookup_reference.json")).unwrap();
|
||||
|
||||
let mismatches: Vec<String> = reference
|
||||
.models
|
||||
.iter()
|
||||
.flat_map(|model| {
|
||||
let prices = Prices::try_from(&EntryWire::read(&model.info)).unwrap();
|
||||
model
|
||||
.checks
|
||||
.iter()
|
||||
.filter_map(move |check| mismatch(&prices, model.inclusive, check))
|
||||
.map(move |found| format!("{:?} {found}", model.info))
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert!(reference.models.len() > 50);
|
||||
assert!(mismatches.is_empty(), "{mismatches:#?}");
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
{"python_reference_commit": "17db64ef78aa2a3d8517422b4a2a1636c0cdd409", "models": [
|
||||
{"info": {"cache_read_input_token_cost": 7.5e-08, "cache_read_input_token_cost_priority": 1.5e-07, "input_cost_per_token": 7.5e-07, "input_cost_per_token_priority": 1.5e-06, "output_cost_per_token": 4.5e-06, "output_cost_per_token_priority": 9e-06}, "inclusive": false, "checks": [[null, 1, [7.5e-07, 4.5e-06, 7.5e-07, 7.5e-07, 7.5e-08]], [null, 10000000, [7.5e-07, 4.5e-06, 7.5e-07, 7.5e-07, 7.5e-08]], ["priority", 1, [1.5e-06, 9e-06, 1.5e-06, 1.5e-06, 1.5e-07]], ["priority", 10000000, [1.5e-06, 9e-06, 1.5e-06, 1.5e-06, 1.5e-07]]]},
|
||||
{"info": {"cache_read_input_token_cost": 9.9e-08, "input_cost_per_token": 3.96e-07, "output_cost_per_token": 3.311e-06}, "inclusive": false, "checks": [[null, 1, [3.96e-07, 3.311e-06, 3.96e-07, 3.96e-07, 9.9e-08]], [null, 10000000, [3.96e-07, 3.311e-06, 3.96e-07, 3.96e-07, 9.9e-08]]]},
|
||||
{"info": {"cache_read_input_token_cost": 2e-07, "cache_read_input_token_cost_above_200k_tokens": 4e-07, "input_cost_per_token": 1.25e-06, "input_cost_per_token_above_200k_tokens": 2.5e-06, "output_cost_per_token": 2.5e-06, "output_cost_per_token_above_200k_tokens": 5e-06}, "inclusive": false, "checks": [[null, 1, [1.25e-06, 2.5e-06, 1.25e-06, 1.25e-06, 2e-07]], [null, 200000, [1.25e-06, 2.5e-06, 1.25e-06, 1.25e-06, 2e-07]], [null, 200001, [2.5e-06, 5e-06, 2.5e-06, 2.5e-06, 4e-07]], [null, 10000000, [2.5e-06, 5e-06, 2.5e-06, 2.5e-06, 4e-07]]]},
|
||||
{"info": {"cache_creation_input_token_cost": 1e-06, "cache_read_input_token_cost": 8e-08, "input_cost_per_token": 8e-07, "output_cost_per_token": 4e-06}, "inclusive": false, "checks": [[null, 1, [8e-07, 4e-06, 1e-06, 1e-06, 8e-08]], [null, 10000000, [8e-07, 4e-06, 1e-06, 1e-06, 8e-08]]]},
|
||||
{"info": {"input_cost_per_token": 1.25e-05, "output_cost_per_token": 1.25e-05}, "inclusive": false, "checks": [[null, 1, [1.25e-05, 1.25e-05, 1.25e-05, 1.25e-05, 1.25e-05]], [null, 10000000, [1.25e-05, 1.25e-05, 1.25e-05, 1.25e-05, 1.25e-05]]]},
|
||||
{"info": {"cache_creation_input_token_cost": 1.25e-06, "cache_creation_input_token_cost_above_1hr": 2e-06, "cache_read_input_token_cost": 1e-07, "input_cost_per_token": 1e-06, "output_cost_per_token": 5e-06}, "inclusive": false, "checks": [[null, 1, [1e-06, 5e-06, 1.25e-06, 2e-06, 1e-07]], [null, 10000000, [1e-06, 5e-06, 1.25e-06, 2e-06, 1e-07]]]},
|
||||
{"info": {"cache_read_input_token_cost": 8.25e-08, "cache_read_input_token_cost_flex": 4.125e-08, "cache_read_input_token_cost_priority": 1.44375e-07, "input_cost_per_token": 3.3e-07, "input_cost_per_token_batches": 1.65e-07, "input_cost_per_token_flex": 1.65e-07, "input_cost_per_token_priority": 5.775e-07, "output_cost_per_token": 2.75e-06, "output_cost_per_token_batches": 1.375e-06, "output_cost_per_token_flex": 1.375e-06, "output_cost_per_token_priority": 4.8125e-06}, "inclusive": false, "checks": [[null, 1, [3.3e-07, 2.75e-06, 3.3e-07, 3.3e-07, 8.25e-08]], [null, 10000000, [3.3e-07, 2.75e-06, 3.3e-07, 3.3e-07, 8.25e-08]], ["priority", 1, [5.775e-07, 4.8125e-06, 5.775e-07, 5.775e-07, 1.44375e-07]], ["priority", 10000000, [5.775e-07, 4.8125e-06, 5.775e-07, 5.775e-07, 1.44375e-07]], ["flex", 1, [1.65e-07, 1.375e-06, 1.65e-07, 1.65e-07, 4.125e-08]], ["flex", 10000000, [1.65e-07, 1.375e-06, 1.65e-07, 1.65e-07, 4.125e-08]]]},
|
||||
{"info": {"cache_read_input_token_cost": 7.5e-08, "cache_read_input_token_cost_batches": 3.75e-08, "cache_read_input_token_cost_flex": 3.75e-08, "cache_read_input_token_cost_priority": 1.5e-07, "input_cost_per_token": 7.5e-07, "output_cost_per_token": 4.5e-06, "input_cost_per_token_batches": 3.75e-07, "input_cost_per_token_flex": 3.75e-07, "input_cost_per_token_priority": 1.5e-06, "output_cost_per_token_batches": 2.25e-06, "output_cost_per_token_flex": 2.25e-06, "output_cost_per_token_priority": 9e-06}, "inclusive": false, "checks": [[null, 1, [7.5e-07, 4.5e-06, 7.5e-07, 7.5e-07, 7.5e-08]], [null, 10000000, [7.5e-07, 4.5e-06, 7.5e-07, 7.5e-07, 7.5e-08]], ["priority", 1, [1.5e-06, 9e-06, 1.5e-06, 1.5e-06, 1.5e-07]], ["priority", 10000000, [1.5e-06, 9e-06, 1.5e-06, 1.5e-06, 1.5e-07]], ["flex", 1, [3.75e-07, 2.25e-06, 3.75e-07, 3.75e-07, 3.75e-08]], ["flex", 10000000, [3.75e-07, 2.25e-06, 3.75e-07, 3.75e-07, 3.75e-08]]]},
|
||||
{"info": {"cache_read_input_token_cost": 5.5e-09, "input_cost_per_token": 5.5e-08, "input_cost_per_token_batches": 2.75e-08, "output_cost_per_token": 4.4e-07, "output_cost_per_token_batches": 2.2e-07, "cache_read_input_token_cost_batches": 2.75e-09}, "inclusive": false, "checks": [[null, 1, [5.5e-08, 4.4e-07, 5.5e-08, 5.5e-08, 5.5e-09]], [null, 10000000, [5.5e-08, 4.4e-07, 5.5e-08, 5.5e-08, 5.5e-09]]]},
|
||||
{"info": {"output_cost_per_token": 0.0}, "inclusive": false, "checks": [[null, 1, [0.0, 0.0, 0.0, 0.0, 0.0]], [null, 10000000, [0.0, 0.0, 0.0, 0.0, 0.0]]]},
|
||||
{"info": {"input_cost_per_token_above_100k_tokens": 5e-07, "output_cost_per_token_above_100k_tokens": 2.5e-06, "cache_creation_input_token_cost_above_100k_tokens": 6.25e-07, "cache_creation_input_token_cost_above_1hr_above_100k_tokens": 1e-06, "cache_read_input_token_cost_above_100k_tokens": 5e-08, "cache_creation_input_token_cost": 1.25e-07, "cache_creation_input_token_cost_above_1hr": 2e-07, "cache_creation_input_token_cost_batches": 6.25e-08, "cache_read_input_token_cost": 1e-08, "cache_read_input_token_cost_batches": 5e-09, "input_cost_per_token": 1e-07, "input_cost_per_token_batches": 5e-08, "output_cost_per_token": 5e-07, "output_cost_per_token_batches": 2.5e-07}, "inclusive": false, "checks": [[null, 1, [1e-07, 5e-07, 1.25e-07, 2e-07, 1e-08]], [null, 100000, [1e-07, 5e-07, 1.25e-07, 2e-07, 1e-08]], [null, 100001, [5e-07, 2.5e-06, 6.25e-07, 1e-06, 5e-08]], [null, 10000000, [5e-07, 2.5e-06, 6.25e-07, 1e-06, 5e-08]]]},
|
||||
{"info": {"input_cost_per_token_above_100k_tokens": 5.5e-07, "output_cost_per_token_above_100k_tokens": 2.75e-06, "cache_creation_input_token_cost_above_100k_tokens": 6.875e-07, "cache_creation_input_token_cost_above_1hr_above_100k_tokens": 1.1e-06, "cache_read_input_token_cost_above_100k_tokens": 5.5e-08, "cache_creation_input_token_cost": 1.375e-07, "cache_creation_input_token_cost_above_1hr": 2.2e-07, "cache_read_input_token_cost": 1.1e-08, "input_cost_per_token": 1.1e-07, "output_cost_per_token": 5.5e-07}, "inclusive": false, "checks": [[null, 1, [1.1e-07, 5.5e-07, 1.375e-07, 2.2e-07, 1.1e-08]], [null, 100000, [1.1e-07, 5.5e-07, 1.375e-07, 2.2e-07, 1.1e-08]], [null, 100001, [5.5e-07, 2.75e-06, 6.875e-07, 1.1e-06, 5.5e-08]], [null, 10000000, [5.5e-07, 2.75e-06, 6.875e-07, 1.1e-06, 5.5e-08]]]},
|
||||
{"info": {"input_cost_per_token_above_100k_tokens_batches": 2.5e-07, "output_cost_per_token_above_100k_tokens_batches": 1.25e-06, "cache_creation_input_token_cost_above_100k_tokens_batches": 3.125e-07, "cache_read_input_token_cost_above_100k_tokens_batches": 2.5e-08, "cache_creation_input_token_cost": 1.25e-07, "cache_creation_input_token_cost_above_1hr": 2e-07, "cache_creation_input_token_cost_batches": 6.25e-08, "cache_read_input_token_cost": 1e-08, "cache_read_input_token_cost_batches": 5e-09, "input_cost_per_token": 1e-07, "input_cost_per_token_batches": 5e-08, "output_cost_per_token": 5e-07, "output_cost_per_token_batches": 2.5e-07, "input_cost_per_token_above_100k_tokens": 5e-07, "output_cost_per_token_above_100k_tokens": 2.5e-06, "cache_creation_input_token_cost_above_100k_tokens": 6.25e-07, "cache_creation_input_token_cost_above_1hr_above_100k_tokens": 1e-06, "cache_read_input_token_cost_above_100k_tokens": 5e-08}, "inclusive": false, "checks": [[null, 1, [1e-07, 5e-07, 1.25e-07, 2e-07, 1e-08]], [null, 100000, [1e-07, 5e-07, 1.25e-07, 2e-07, 1e-08]], [null, 100001, [5e-07, 2.5e-06, 6.25e-07, 1e-06, 5e-08]], [null, 10000000, [5e-07, 2.5e-06, 6.25e-07, 1e-06, 5e-08]]]},
|
||||
{"info": {"cache_creation_input_token_cost": 4.125e-06, "cache_creation_input_token_cost_flex": 2.0625e-06, "cache_creation_input_token_cost_priority": 7.21875e-06, "cache_read_input_token_cost": 3.3e-07, "cache_read_input_token_cost_flex": 1.65e-07, "cache_read_input_token_cost_priority": 5.775e-07, "input_cost_per_token": 3.3e-06, "input_cost_per_token_flex": 1.65e-06, "input_cost_per_token_priority": 5.775e-06, "output_cost_per_token": 1.65e-05, "output_cost_per_token_flex": 8.25e-06, "output_cost_per_token_priority": 2.8875e-05}, "inclusive": false, "checks": [[null, 1, [3.3e-06, 1.65e-05, 4.125e-06, 4.125e-06, 3.3e-07]], [null, 10000000, [3.3e-06, 1.65e-05, 4.125e-06, 4.125e-06, 3.3e-07]], ["priority", 1, [5.775e-06, 2.8875e-05, 7.21875e-06, 7.21875e-06, 5.775e-07]], ["priority", 10000000, [5.775e-06, 2.8875e-05, 7.21875e-06, 7.21875e-06, 5.775e-07]], ["flex", 1, [1.65e-06, 8.25e-06, 2.0625e-06, 2.0625e-06, 1.65e-07]], ["flex", 10000000, [1.65e-06, 8.25e-06, 2.0625e-06, 2.0625e-06, 1.65e-07]]]},
|
||||
{"info": {"cache_read_input_token_cost": 1.375e-07, "cache_read_input_token_cost_priority": 2.75e-07, "input_cost_per_token": 1.375e-06, "input_cost_per_token_batches": 6.875e-07, "input_cost_per_token_priority": 2.75e-06, "output_cost_per_token": 1.1e-05, "output_cost_per_token_batches": 5.5e-06, "output_cost_per_token_priority": 2.2e-05, "cache_read_input_token_cost_batches": 6.875e-08}, "inclusive": false, "checks": [[null, 1, [1.375e-06, 1.1e-05, 1.375e-06, 1.375e-06, 1.375e-07]], [null, 10000000, [1.375e-06, 1.1e-05, 1.375e-06, 1.375e-06, 1.375e-07]], ["priority", 1, [2.75e-06, 2.2e-05, 2.75e-06, 2.75e-06, 2.75e-07]], ["priority", 10000000, [2.75e-06, 2.2e-05, 2.75e-06, 2.75e-06, 2.75e-07]]]},
|
||||
{"info": {"input_cost_per_token": 5e-06, "input_cost_per_token_batches": 2.5e-06, "output_cost_per_token": 1.5e-05, "output_cost_per_token_batches": 7.5e-06}, "inclusive": false, "checks": [[null, 1, [5e-06, 1.5e-05, 5e-06, 5e-06, 5e-06]], [null, 10000000, [5e-06, 1.5e-05, 5e-06, 5e-06, 5e-06]]]},
|
||||
{"info": {"cache_creation_input_token_cost": 2.1e-06, "cache_creation_input_token_cost_batches": 1.05e-06, "cache_creation_input_token_cost_flex": 1.05e-06, "cache_creation_input_token_cost_priority": 3.675e-06, "cache_read_input_token_cost": 3.12e-07, "cache_read_input_token_cost_batches": 1.56e-07, "cache_read_input_token_cost_flex": 1.56e-07, "cache_read_input_token_cost_priority": 5.46e-07, "input_cost_per_token": 1.68e-06, "input_cost_per_token_batches": 8.4e-07, "input_cost_per_token_flex": 8.4e-07, "input_cost_per_token_priority": 2.94e-06, "output_cost_per_token": 5.28e-06, "output_cost_per_token_batches": 2.64e-06, "output_cost_per_token_flex": 2.64e-06, "output_cost_per_token_priority": 9.24e-06}, "inclusive": false, "checks": [[null, 1, [1.68e-06, 5.28e-06, 2.1e-06, 2.1e-06, 3.12e-07]], [null, 10000000, [1.68e-06, 5.28e-06, 2.1e-06, 2.1e-06, 3.12e-07]], ["priority", 1, [2.94e-06, 9.24e-06, 3.675e-06, 3.675e-06, 5.46e-07]], ["priority", 10000000, [2.94e-06, 9.24e-06, 3.675e-06, 3.675e-06, 5.46e-07]], ["flex", 1, [8.4e-07, 2.64e-06, 1.05e-06, 1.05e-06, 1.56e-07]], ["flex", 10000000, [8.4e-07, 2.64e-06, 1.05e-06, 1.05e-06, 1.56e-07]]]},
|
||||
{"info": {"input_cost_per_token": 1.1e-05, "input_cost_per_token_above_272k_tokens": 2.2e-05, "cache_creation_input_token_cost": 1.375e-05, "cache_creation_input_token_cost_above_272k_tokens": 2.75e-05, "cache_creation_input_token_cost_above_272k_tokens_ultrafast": 0.000165, "cache_creation_input_token_cost_ultrafast": 8.25e-05, "cache_read_input_token_cost": 1.1e-06, "cache_read_input_token_cost_above_272k_tokens": 2.2e-06, "cache_read_input_token_cost_above_272k_tokens_ultrafast": 1.32e-05, "cache_read_input_token_cost_ultrafast": 6.6e-06, "input_cost_per_token_above_272k_tokens_ultrafast": 0.000132, "input_cost_per_token_ultrafast": 6.6e-05, "output_cost_per_token": 5.5e-05, "output_cost_per_token_above_272k_tokens": 8.25e-05, "output_cost_per_token_above_272k_tokens_ultrafast": 0.000495, "output_cost_per_token_ultrafast": 0.00033}, "inclusive": false, "checks": [[null, 1, [1.1e-05, 5.5e-05, 1.375e-05, 1.375e-05, 1.1e-06]], [null, 272000, [1.1e-05, 5.5e-05, 1.375e-05, 1.375e-05, 1.1e-06]], [null, 272001, [2.2e-05, 8.25e-05, 2.75e-05, 2.75e-05, 2.2e-06]], [null, 10000000, [2.2e-05, 8.25e-05, 2.75e-05, 2.75e-05, 2.2e-06]], ["ultrafast", 1, [6.6e-05, 0.00033, 8.25e-05, 8.25e-05, 6.6e-06]], ["ultrafast", 272000, [6.6e-05, 0.00033, 8.25e-05, 8.25e-05, 6.6e-06]], ["ultrafast", 272001, [0.000132, 0.000495, 0.000165, 0.000165, 1.32e-05]], ["ultrafast", 10000000, [0.000132, 0.000495, 0.000165, 0.000165, 1.32e-05]]]},
|
||||
{"info": {"cache_creation_input_token_cost": 1.25e-05, "cache_creation_input_token_cost_above_272k_tokens": 2.5e-05, "cache_creation_input_token_cost_above_272k_tokens_flex": 1.25e-05, "cache_creation_input_token_cost_above_272k_tokens_priority": 5e-05, "cache_creation_input_token_cost_flex": 6.25e-06, "cache_creation_input_token_cost_priority": 2.5e-05, "cache_read_input_token_cost": 1e-06, "cache_read_input_token_cost_above_272k_tokens": 2e-06, "cache_read_input_token_cost_above_272k_tokens_flex": 1e-06, "cache_read_input_token_cost_above_272k_tokens_priority": 4e-06, "cache_read_input_token_cost_batches": 5e-07, "cache_read_input_token_cost_above_272k_tokens_batches": 1e-06, "cache_creation_input_token_cost_batches": 6.25e-06, "cache_creation_input_token_cost_above_272k_tokens_batches": 1.25e-05, "cache_creation_input_token_cost_above_272k_tokens_ultrafast": 0.00015, "cache_creation_input_token_cost_ultrafast": 7.5e-05, "cache_read_input_token_cost_above_272k_tokens_ultrafast": 1.2e-05, "cache_read_input_token_cost_flex": 5e-07, "cache_read_input_token_cost_priority": 2e-06, "cache_read_input_token_cost_ultrafast": 6e-06, "input_cost_per_token": 1e-05, "input_cost_per_token_above_272k_tokens": 2e-05, "input_cost_per_token_above_272k_tokens_flex": 1e-05, "input_cost_per_token_above_272k_tokens_priority": 4e-05, "input_cost_per_token_batches": 5e-06, "input_cost_per_token_above_272k_tokens_batches": 1e-05, "input_cost_per_token_above_272k_tokens_ultrafast": 0.00012, "input_cost_per_token_flex": 5e-06, "input_cost_per_token_priority": 2e-05, "input_cost_per_token_ultrafast": 6e-05, "output_cost_per_token": 5e-05, "output_cost_per_token_above_272k_tokens": 7.5e-05, "output_cost_per_token_above_272k_tokens_flex": 3.75e-05, "output_cost_per_token_above_272k_tokens_priority": 0.00015, "output_cost_per_token_batches": 2.5e-05, "output_cost_per_token_above_272k_tokens_batches": 3.75e-05, "output_cost_per_token_above_272k_tokens_ultrafast": 0.00045, "output_cost_per_token_flex": 2.5e-05, "output_cost_per_token_priority": 0.0001, "output_cost_per_token_ultrafast": 0.0003}, "inclusive": false, "checks": [[null, 1, [1e-05, 5e-05, 1.25e-05, 1.25e-05, 1e-06]], [null, 272000, [1e-05, 5e-05, 1.25e-05, 1.25e-05, 1e-06]], [null, 272001, [2e-05, 7.5e-05, 2.5e-05, 2.5e-05, 2e-06]], [null, 10000000, [2e-05, 7.5e-05, 2.5e-05, 2.5e-05, 2e-06]], ["priority", 1, [2e-05, 0.0001, 2.5e-05, 2.5e-05, 2e-06]], ["priority", 272000, [2e-05, 0.0001, 2.5e-05, 2.5e-05, 2e-06]], ["priority", 272001, [4e-05, 0.00015, 5e-05, 5e-05, 4e-06]], ["priority", 10000000, [4e-05, 0.00015, 5e-05, 5e-05, 4e-06]], ["flex", 1, [5e-06, 2.5e-05, 6.25e-06, 6.25e-06, 5e-07]], ["flex", 272000, [5e-06, 2.5e-05, 6.25e-06, 6.25e-06, 5e-07]], ["flex", 272001, [1e-05, 3.75e-05, 1.25e-05, 1.25e-05, 1e-06]], ["flex", 10000000, [1e-05, 3.75e-05, 1.25e-05, 1.25e-05, 1e-06]], ["ultrafast", 1, [6e-05, 0.0003, 7.5e-05, 7.5e-05, 6e-06]], ["ultrafast", 272000, [6e-05, 0.0003, 7.5e-05, 7.5e-05, 6e-06]], ["ultrafast", 272001, [0.00012, 0.00045, 0.00015, 0.00015, 1.2e-05]], ["ultrafast", 10000000, [0.00012, 0.00045, 0.00015, 0.00015, 1.2e-05]]]},
|
||||
{"info": {"input_cost_per_token": 2.2e-06, "output_cost_per_token": 6.6e-06, "cache_read_input_token_cost": 5.5e-07, "cache_read_input_token_cost_flex": 2.75e-07, "cache_read_input_token_cost_priority": 9.625e-07, "input_cost_per_token_flex": 1.1e-06, "input_cost_per_token_priority": 3.85e-06, "output_cost_per_token_flex": 3.3e-06, "output_cost_per_token_priority": 1.155e-05}, "inclusive": false, "checks": [[null, 1, [2.2e-06, 6.6e-06, 2.2e-06, 2.2e-06, 5.5e-07]], [null, 10000000, [2.2e-06, 6.6e-06, 2.2e-06, 2.2e-06, 5.5e-07]], ["priority", 1, [3.85e-06, 1.155e-05, 3.85e-06, 3.85e-06, 9.625e-07]], ["priority", 10000000, [3.85e-06, 1.155e-05, 3.85e-06, 3.85e-06, 9.625e-07]], ["flex", 1, [1.1e-06, 3.3e-06, 1.1e-06, 1.1e-06, 2.75e-07]], ["flex", 10000000, [1.1e-06, 3.3e-06, 1.1e-06, 1.1e-06, 2.75e-07]]]},
|
||||
{"info": {"cache_creation_input_token_cost": 1.25e-06, "cache_creation_input_token_cost_above_1hr": 2e-06, "cache_creation_input_token_cost_batches": 6.25e-07, "cache_read_input_token_cost": 1e-07, "cache_read_input_token_cost_batches": 5e-08, "input_cost_per_token": 1e-06, "input_cost_per_token_batches": 5e-07, "output_cost_per_token": 5e-06, "output_cost_per_token_batches": 2.5e-06}, "inclusive": false, "checks": [[null, 1, [1e-06, 5e-06, 1.25e-06, 2e-06, 1e-07]], [null, 10000000, [1e-06, 5e-06, 1.25e-06, 2e-06, 1e-07]]]},
|
||||
{"info": {"cache_read_input_token_cost": 3e-06, "cache_read_input_token_cost_above_272k_tokens": 6e-06, "input_cost_per_token": 3e-05, "input_cost_per_token_above_272k_tokens": 6e-05, "output_cost_per_token": 0.00018, "output_cost_per_token_above_272k_tokens": 0.00027}, "inclusive": false, "checks": [[null, 1, [3e-05, 0.00018, 3e-05, 3e-05, 3e-06]], [null, 272000, [3e-05, 0.00018, 3e-05, 3e-05, 3e-06]], [null, 272001, [6e-05, 0.00027, 6e-05, 6e-05, 6e-06]], [null, 10000000, [6e-05, 0.00027, 6e-05, 6e-05, 6e-06]]]},
|
||||
{"info": {"input_cost_per_token": 1.5e-07, "input_cost_per_token_batches": 1.2e-07, "output_cost_per_token": 0}, "inclusive": false, "checks": [[null, 1, [1.5e-07, 0.0, 1.5e-07, 1.5e-07, 1.5e-07]], [null, 10000000, [1.5e-07, 0.0, 1.5e-07, 1.5e-07, 1.5e-07]]]},
|
||||
{"info": {"cache_read_input_token_cost_priority": 6e-07, "input_cost_per_token_priority": 1.2e-06, "output_cost_per_token_priority": 1.2e-06}, "inclusive": false, "checks": [[null, 1, [0.0, 0.0, 0.0, 0.0, 0.0]], [null, 10000000, [0.0, 0.0, 0.0, 0.0, 0.0]], ["priority", 1, [1.2e-06, 1.2e-06, 1.2e-06, 1.2e-06, 6e-07]], ["priority", 10000000, [1.2e-06, 1.2e-06, 1.2e-06, 1.2e-06, 6e-07]]]},
|
||||
{"info": {"cache_read_input_token_cost": 2e-07, "input_cost_per_token": 1.25e-06, "output_cost_per_token": 2.5e-06, "input_cost_per_token_above_200k_tokens": 2.5e-06, "output_cost_per_token_above_200k_tokens": 5e-06, "cache_read_input_token_cost_above_200k_tokens": 4e-07}, "inclusive": true, "checks": [[null, 1, [1.25e-06, 2.5e-06, 1.25e-06, 1.25e-06, 2e-07]], [null, 200000, [2.5e-06, 5e-06, 2.5e-06, 2.5e-06, 4e-07]], [null, 200001, [2.5e-06, 5e-06, 2.5e-06, 2.5e-06, 4e-07]], [null, 10000000, [2.5e-06, 5e-06, 2.5e-06, 2.5e-06, 4e-07]]]},
|
||||
{"info": {"cache_read_input_token_cost": 2e-07, "cache_read_input_token_cost_batches": 1.6e-07, "input_cost_per_token": 1.25e-06, "input_cost_per_token_batches": 1e-06, "output_cost_per_token": 2.5e-06, "output_cost_per_token_batches": 2e-06, "input_cost_per_token_above_200k_tokens": 2.5e-06, "input_cost_per_token_above_200k_tokens_batches": 2e-06, "output_cost_per_token_above_200k_tokens": 5e-06, "output_cost_per_token_above_200k_tokens_batches": 4e-06, "cache_read_input_token_cost_above_200k_tokens": 4e-07, "cache_read_input_token_cost_above_200k_tokens_batches": 3.2e-07}, "inclusive": true, "checks": [[null, 1, [1.25e-06, 2.5e-06, 1.25e-06, 1.25e-06, 2e-07]], [null, 200000, [2.5e-06, 5e-06, 2.5e-06, 2.5e-06, 4e-07]], [null, 200001, [2.5e-06, 5e-06, 2.5e-06, 2.5e-06, 4e-07]], [null, 10000000, [2.5e-06, 5e-06, 2.5e-06, 2.5e-06, 4e-07]]]},
|
||||
{"info": {"input_cost_per_token": 5e-06}, "inclusive": false, "checks": [[null, 1, [5e-06, 0.0, 5e-06, 5e-06, 5e-06]], [null, 10000000, [5e-06, 0.0, 5e-06, 5e-06, 5e-06]]]},
|
||||
{"info": {"cache_read_input_token_cost": 1.5e-08, "input_cost_per_token": 6e-08, "input_cost_per_token_batches": 3e-08, "output_cost_per_token": 2.4e-07, "output_cost_per_token_batches": 1.2e-07}, "inclusive": false, "checks": [[null, 1, [6e-08, 2.4e-07, 6e-08, 6e-08, 1.5e-08]], [null, 10000000, [6e-08, 2.4e-07, 6e-08, 6e-08, 1.5e-08]]]},
|
||||
{"info": {"input_cost_per_token": 3.3e-05, "input_cost_per_token_above_272k_tokens": 6.6e-05, "input_cost_per_token_above_272k_tokens_batches": 3.3e-05, "input_cost_per_token_batches": 1.65e-05, "output_cost_per_token": 0.000198, "output_cost_per_token_above_272k_tokens": 0.000297, "output_cost_per_token_above_272k_tokens_batches": 0.0001485, "output_cost_per_token_batches": 9.9e-05}, "inclusive": false, "checks": [[null, 1, [3.3e-05, 0.000198, 3.3e-05, 3.3e-05, 3.3e-05]], [null, 272000, [3.3e-05, 0.000198, 3.3e-05, 3.3e-05, 3.3e-05]], [null, 272001, [6.6e-05, 0.000297, 6.6e-05, 6.6e-05, 6.6e-05]], [null, 10000000, [6.6e-05, 0.000297, 6.6e-05, 6.6e-05, 6.6e-05]]]},
|
||||
{"info": {"cache_read_input_token_cost": 7.5e-08, "cache_read_input_token_cost_priority": 1.3125e-07, "input_cost_per_token": 3e-07, "input_cost_per_token_batches": 1.5e-07, "input_cost_per_token_priority": 5.25e-07, "output_cost_per_token": 2.5e-06, "output_cost_per_token_batches": 1.25e-06, "output_cost_per_token_priority": 4.375e-06}, "inclusive": false, "checks": [[null, 1, [3e-07, 2.5e-06, 3e-07, 3e-07, 7.5e-08]], [null, 10000000, [3e-07, 2.5e-06, 3e-07, 3e-07, 7.5e-08]], ["priority", 1, [5.25e-07, 4.375e-06, 5.25e-07, 5.25e-07, 1.3125e-07]], ["priority", 10000000, [5.25e-07, 4.375e-06, 5.25e-07, 5.25e-07, 1.3125e-07]]]},
|
||||
{"info": {"cache_creation_input_token_cost": 1.25e-05, "cache_creation_input_token_cost_above_272k_tokens": 2.5e-05, "cache_read_input_token_cost": 1e-06, "cache_read_input_token_cost_above_272k_tokens": 2e-06, "input_cost_per_token": 1e-05, "input_cost_per_token_above_272k_tokens": 2e-05, "output_cost_per_token": 5e-05, "output_cost_per_token_above_272k_tokens": 7.5e-05}, "inclusive": false, "checks": [[null, 1, [1e-05, 5e-05, 1.25e-05, 1.25e-05, 1e-06]], [null, 272000, [1e-05, 5e-05, 1.25e-05, 1.25e-05, 1e-06]], [null, 272001, [2e-05, 7.5e-05, 2.5e-05, 2.5e-05, 2e-06]], [null, 10000000, [2e-05, 7.5e-05, 2.5e-05, 2.5e-05, 2e-06]]]},
|
||||
{"info": {"input_cost_per_token": 1.25e-06, "output_cost_per_token": 2.5e-06, "cache_read_input_token_cost": 2e-07, "input_cost_per_token_above_200k_tokens": 2.5e-06, "output_cost_per_token_above_200k_tokens": 5e-06}, "inclusive": false, "checks": [[null, 1, [1.25e-06, 2.5e-06, 1.25e-06, 1.25e-06, 2e-07]], [null, 200000, [1.25e-06, 2.5e-06, 1.25e-06, 1.25e-06, 2e-07]], [null, 200001, [2.5e-06, 5e-06, 2.5e-06, 2.5e-06, 2e-07]], [null, 10000000, [2.5e-06, 5e-06, 2.5e-06, 2.5e-06, 2e-07]]]},
|
||||
{"info": {"input_cost_per_token": 5e-06, "output_cost_per_token": 3e-05, "cache_read_input_token_cost": 5e-07, "input_cost_per_token_above_272k_tokens": 1e-05, "output_cost_per_token_above_272k_tokens": 4.5e-05}, "inclusive": false, "checks": [[null, 1, [5e-06, 3e-05, 5e-06, 5e-06, 5e-07]], [null, 272000, [5e-06, 3e-05, 5e-06, 5e-06, 5e-07]], [null, 272001, [1e-05, 4.5e-05, 1e-05, 1e-05, 5e-07]], [null, 10000000, [1e-05, 4.5e-05, 1e-05, 1e-05, 5e-07]]]},
|
||||
{"info": {"input_cost_per_token": 1.5e-07, "input_cost_per_token_batches": 7.5e-08, "input_cost_per_token_flex": 7.5e-08, "input_cost_per_token_priority": 2.625e-07, "output_cost_per_token": 6e-07, "output_cost_per_token_batches": 3e-07, "output_cost_per_token_flex": 3e-07, "output_cost_per_token_priority": 1.05e-06}, "inclusive": false, "checks": [[null, 1, [1.5e-07, 6e-07, 1.5e-07, 1.5e-07, 1.5e-07]], [null, 10000000, [1.5e-07, 6e-07, 1.5e-07, 1.5e-07, 1.5e-07]], ["priority", 1, [2.625e-07, 1.05e-06, 2.625e-07, 2.625e-07, 2.625e-07]], ["priority", 10000000, [2.625e-07, 1.05e-06, 2.625e-07, 2.625e-07, 2.625e-07]], ["flex", 1, [7.5e-08, 3e-07, 7.5e-08, 7.5e-08, 7.5e-08]], ["flex", 10000000, [7.5e-08, 3e-07, 7.5e-08, 7.5e-08, 7.5e-08]]]},
|
||||
{"info": {"tiered_pricing": [{"input_cost_per_token": 1.2e-06, "output_cost_per_token": 6e-06, "range": [0, 32000.0]}, {"input_cost_per_token": 2.4e-06, "output_cost_per_token": 1.2e-05, "range": [32000.0, 128000.0]}, {"input_cost_per_token": 3e-06, "output_cost_per_token": 1.5e-05, "range": [128000.0, 252000.0]}]}, "inclusive": false, "checks": [[null, 1, [1.2e-06, 6e-06, 1.2e-06, 1.2e-06, 1.2e-06]], [null, 32000, [1.2e-06, 6e-06, 1.2e-06, 1.2e-06, 1.2e-06]], [null, 32001, [2.4e-06, 1.2e-05, 2.4e-06, 2.4e-06, 2.4e-06]], [null, 128000, [2.4e-06, 1.2e-05, 2.4e-06, 2.4e-06, 2.4e-06]], [null, 128001, [3e-06, 1.5e-05, 3e-06, 3e-06, 3e-06]], [null, 10000000, [3e-06, 1.5e-05, 3e-06, 3e-06, 3e-06]]]},
|
||||
{"info": {"cache_creation_input_token_cost": 1.25e-06, "cache_creation_input_token_cost_above_1hr": 2e-06, "cache_read_input_token_cost": 1e-07, "input_cost_per_token": 1e-06, "output_cost_per_token": 5e-06, "input_cost_per_token_batches": 5e-07, "output_cost_per_token_batches": 2.5e-06}, "inclusive": false, "checks": [[null, 1, [1e-06, 5e-06, 1.25e-06, 2e-06, 1e-07]], [null, 10000000, [1e-06, 5e-06, 1.25e-06, 2e-06, 1e-07]]]},
|
||||
{"info": {"cache_read_input_token_cost": 2e-07, "cache_read_input_token_cost_above_200k_tokens": 4e-07, "input_cost_per_token": 2e-06, "input_cost_per_token_above_200k_tokens": 4e-06, "output_cost_per_token": 1.2e-05, "output_cost_per_token_above_200k_tokens": 1.8e-05, "cache_read_input_token_cost_above_200k_tokens_priority": 7.2e-07, "cache_read_input_token_cost_priority": 3.6e-07, "input_cost_per_token_above_200k_tokens_priority": 7.2e-06, "input_cost_per_token_batches": 1e-06, "input_cost_per_token_priority": 3.6e-06, "output_cost_per_token_above_200k_tokens_priority": 3.24e-05, "output_cost_per_token_batches": 6e-06, "output_cost_per_token_priority": 2.16e-05, "cache_read_input_token_cost_flex": 2e-07, "input_cost_per_token_flex": 1e-06, "output_cost_per_token_flex": 6e-06}, "inclusive": false, "checks": [[null, 1, [2e-06, 1.2e-05, 2e-06, 2e-06, 2e-07]], [null, 200000, [2e-06, 1.2e-05, 2e-06, 2e-06, 2e-07]], [null, 200001, [4e-06, 1.8e-05, 4e-06, 4e-06, 4e-07]], [null, 10000000, [4e-06, 1.8e-05, 4e-06, 4e-06, 4e-07]], ["priority", 1, [3.6e-06, 2.16e-05, 3.6e-06, 3.6e-06, 3.6e-07]], ["priority", 200000, [3.6e-06, 2.16e-05, 3.6e-06, 3.6e-06, 3.6e-07]], ["priority", 200001, [7.2e-06, 3.24e-05, 7.2e-06, 7.2e-06, 7.2e-07]], ["priority", 10000000, [7.2e-06, 3.24e-05, 7.2e-06, 7.2e-06, 7.2e-07]], ["flex", 1, [1e-06, 6e-06, 1e-06, 1e-06, 2e-07]], ["flex", 200000, [1e-06, 6e-06, 1e-06, 1e-06, 2e-07]], ["flex", 200001, [4e-06, 1.8e-05, 4e-06, 4e-06, 4e-07]], ["flex", 10000000, [4e-06, 1.8e-05, 4e-06, 4e-06, 4e-07]]]},
|
||||
{"info": {"input_cost_per_token": 5e-06, "input_cost_per_token_batches": 2.5e-06, "input_cost_per_token_priority": 8.75e-06, "output_cost_per_token": 1.5e-05, "output_cost_per_token_batches": 7.5e-06, "output_cost_per_token_priority": 2.625e-05}, "inclusive": false, "checks": [[null, 1, [5e-06, 1.5e-05, 5e-06, 5e-06, 5e-06]], [null, 10000000, [5e-06, 1.5e-05, 5e-06, 5e-06, 5e-06]], ["priority", 1, [8.75e-06, 2.625e-05, 8.75e-06, 8.75e-06, 8.75e-06]], ["priority", 10000000, [8.75e-06, 2.625e-05, 8.75e-06, 8.75e-06, 8.75e-06]]]},
|
||||
{"info": {"cache_read_input_token_cost": 2e-07, "cache_read_input_token_cost_priority": 3.6e-07, "cache_read_input_token_cost_above_200k_tokens": 4e-07, "cache_read_input_token_cost_above_200k_tokens_priority": 7.2e-07, "cache_read_input_token_cost_batches": 1e-07, "input_cost_per_token": 2e-06, "input_cost_per_token_priority": 3.6e-06, "input_cost_per_token_above_200k_tokens": 4e-06, "input_cost_per_token_above_200k_tokens_priority": 7.2e-06, "input_cost_per_token_batches": 1e-06, "output_cost_per_token": 1.2e-05, "output_cost_per_token_priority": 2.16e-05, "output_cost_per_token_above_200k_tokens": 1.8e-05, "output_cost_per_token_above_200k_tokens_priority": 3.24e-05, "output_cost_per_token_batches": 6e-06}, "inclusive": false, "checks": [[null, 1, [2e-06, 1.2e-05, 2e-06, 2e-06, 2e-07]], [null, 200000, [2e-06, 1.2e-05, 2e-06, 2e-06, 2e-07]], [null, 200001, [4e-06, 1.8e-05, 4e-06, 4e-06, 4e-07]], [null, 10000000, [4e-06, 1.8e-05, 4e-06, 4e-06, 4e-07]], ["priority", 1, [3.6e-06, 2.16e-05, 3.6e-06, 3.6e-06, 3.6e-07]], ["priority", 200000, [3.6e-06, 2.16e-05, 3.6e-06, 3.6e-06, 3.6e-07]], ["priority", 200001, [7.2e-06, 3.24e-05, 7.2e-06, 7.2e-06, 7.2e-07]], ["priority", 10000000, [7.2e-06, 3.24e-05, 7.2e-06, 7.2e-06, 7.2e-07]]]},
|
||||
{"info": {"cache_read_input_token_cost": 2e-07, "cache_read_input_token_cost_above_200k_tokens": 4e-07, "cache_read_input_token_cost_above_200k_tokens_priority": 7.2e-07, "cache_read_input_token_cost_batches": 1e-07, "cache_read_input_token_cost_flex": 1e-07, "cache_read_input_token_cost_priority": 3.6e-07, "input_cost_per_token": 2e-06, "input_cost_per_token_above_200k_tokens": 4e-06, "input_cost_per_token_above_200k_tokens_priority": 7.2e-06, "input_cost_per_token_batches": 1e-06, "input_cost_per_token_flex": 1e-06, "input_cost_per_token_priority": 3.6e-06, "output_cost_per_token": 1.2e-05, "output_cost_per_token_above_200k_tokens": 1.8e-05, "output_cost_per_token_above_200k_tokens_priority": 3.24e-05, "output_cost_per_token_batches": 6e-06, "output_cost_per_token_flex": 6e-06, "output_cost_per_token_priority": 2.16e-05}, "inclusive": false, "checks": [[null, 1, [2e-06, 1.2e-05, 2e-06, 2e-06, 2e-07]], [null, 200000, [2e-06, 1.2e-05, 2e-06, 2e-06, 2e-07]], [null, 200001, [4e-06, 1.8e-05, 4e-06, 4e-06, 4e-07]], [null, 10000000, [4e-06, 1.8e-05, 4e-06, 4e-06, 4e-07]], ["priority", 1, [3.6e-06, 2.16e-05, 3.6e-06, 3.6e-06, 3.6e-07]], ["priority", 200000, [3.6e-06, 2.16e-05, 3.6e-06, 3.6e-06, 3.6e-07]], ["priority", 200001, [7.2e-06, 3.24e-05, 7.2e-06, 7.2e-06, 7.2e-07]], ["priority", 10000000, [7.2e-06, 3.24e-05, 7.2e-06, 7.2e-06, 7.2e-07]], ["flex", 1, [1e-06, 6e-06, 1e-06, 1e-06, 1e-07]], ["flex", 200000, [1e-06, 6e-06, 1e-06, 1e-06, 1e-07]], ["flex", 200001, [4e-06, 1.8e-05, 4e-06, 4e-06, 4e-07]], ["flex", 10000000, [4e-06, 1.8e-05, 4e-06, 4e-06, 4e-07]]]},
|
||||
{"info": {"input_cost_per_token": 3e-07, "input_cost_per_token_batches": 1.5e-07, "input_cost_per_token_flex": 1.5e-07, "output_cost_per_token": 2.5e-06, "output_cost_per_token_batches": 1.25e-06, "output_cost_per_token_flex": 1.25e-06}, "inclusive": false, "checks": [[null, 1, [3e-07, 2.5e-06, 3e-07, 3e-07, 3e-07]], [null, 10000000, [3e-07, 2.5e-06, 3e-07, 3e-07, 3e-07]], ["flex", 1, [1.5e-07, 1.25e-06, 1.5e-07, 1.5e-07, 1.5e-07]], ["flex", 10000000, [1.5e-07, 1.25e-06, 1.5e-07, 1.5e-07, 1.5e-07]]]},
|
||||
{"info": {"cache_creation_input_token_cost": 3.75e-06, "cache_creation_input_token_cost_above_1hr": 6e-06, "cache_creation_input_token_cost_above_1hr_above_200k_tokens": 1.2e-05, "cache_read_input_token_cost": 3e-07, "input_cost_per_token": 3e-06, "input_cost_per_token_above_200k_tokens": 6e-06, "output_cost_per_token_above_200k_tokens": 2.25e-05, "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, "cache_read_input_token_cost_above_200k_tokens": 6e-07, "cache_read_input_token_cost_batches": 1.5e-07, "input_cost_per_token_batches": 1.5e-06, "output_cost_per_token": 1.5e-05, "output_cost_per_token_batches": 7.5e-06, "cache_creation_input_token_cost_batches": 1.88e-06}, "inclusive": false, "checks": [[null, 1, [3e-06, 1.5e-05, 3.75e-06, 6e-06, 3e-07]], [null, 200000, [3e-06, 1.5e-05, 3.75e-06, 6e-06, 3e-07]], [null, 200001, [6e-06, 2.25e-05, 7.5e-06, 1.2e-05, 6e-07]], [null, 10000000, [6e-06, 2.25e-05, 7.5e-06, 1.2e-05, 6e-07]]]},
|
||||
{"info": {"input_cost_per_token": 7.4e-07, "input_cost_per_token_flex": 3.7e-07, "output_cost_per_token": 2.22e-06, "output_cost_per_token_flex": 1.11e-06}, "inclusive": false, "checks": [[null, 1, [7.4e-07, 2.22e-06, 7.4e-07, 7.4e-07, 7.4e-07]], [null, 10000000, [7.4e-07, 2.22e-06, 7.4e-07, 7.4e-07, 7.4e-07]], ["flex", 1, [3.7e-07, 1.11e-06, 3.7e-07, 3.7e-07, 3.7e-07]], ["flex", 10000000, [3.7e-07, 1.11e-06, 3.7e-07, 3.7e-07, 3.7e-07]]]},
|
||||
{"info": {"cache_creation_input_token_cost": 3.75e-06, "cache_read_input_token_cost": 3e-07, "input_cost_per_token": 3e-06, "input_cost_per_token_above_200k_tokens": 6e-06, "output_cost_per_token_above_200k_tokens": 2.25e-05, "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, "cache_read_input_token_cost_above_200k_tokens": 6e-07, "output_cost_per_token": 1.5e-05}, "inclusive": false, "checks": [[null, 1, [3e-06, 1.5e-05, 3.75e-06, 3.75e-06, 3e-07]], [null, 200000, [3e-06, 1.5e-05, 3.75e-06, 3.75e-06, 3e-07]], [null, 200001, [6e-06, 2.25e-05, 7.5e-06, 7.5e-06, 6e-07]], [null, 10000000, [6e-06, 2.25e-05, 7.5e-06, 7.5e-06, 6e-07]]]},
|
||||
{"info": {"cache_creation_input_token_cost": 4.5e-06, "cache_creation_input_token_cost_above_1hr": 7.2e-06, "cache_read_input_token_cost": 3.6e-07, "input_cost_per_token": 3.6e-06, "input_cost_per_token_above_200k_tokens": 7.2e-06, "output_cost_per_token_above_200k_tokens": 2.7e-05, "cache_creation_input_token_cost_above_200k_tokens": 9e-06, "cache_creation_input_token_cost_above_1hr_above_200k_tokens": 1.44e-05, "cache_read_input_token_cost_above_200k_tokens": 7.2e-07, "output_cost_per_token": 1.8e-05}, "inclusive": false, "checks": [[null, 1, [3.6e-06, 1.8e-05, 4.5e-06, 7.2e-06, 3.6e-07]], [null, 200000, [3.6e-06, 1.8e-05, 4.5e-06, 7.2e-06, 3.6e-07]], [null, 200001, [7.2e-06, 2.7e-05, 9e-06, 1.44e-05, 7.2e-07]], [null, 10000000, [7.2e-06, 2.7e-05, 9e-06, 1.44e-05, 7.2e-07]]]},
|
||||
{"info": {"cache_creation_input_token_cost": 3.75e-06, "cache_creation_input_token_cost_above_1hr": 6e-06, "cache_creation_input_token_cost_above_1hr_above_200k_tokens": 1.2e-05, "cache_read_input_token_cost": 3e-07, "input_cost_per_token": 3e-06, "input_cost_per_token_above_200k_tokens": 6e-06, "output_cost_per_token_above_200k_tokens": 2.25e-05, "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, "cache_read_input_token_cost_above_200k_tokens": 6e-07, "output_cost_per_token": 1.5e-05, "input_cost_per_token_batches": 1.5e-06, "output_cost_per_token_batches": 7.5e-06}, "inclusive": false, "checks": [[null, 1, [3e-06, 1.5e-05, 3.75e-06, 6e-06, 3e-07]], [null, 200000, [3e-06, 1.5e-05, 3.75e-06, 6e-06, 3e-07]], [null, 200001, [6e-06, 2.25e-05, 7.5e-06, 1.2e-05, 6e-07]], [null, 10000000, [6e-06, 2.25e-05, 7.5e-06, 1.2e-05, 6e-07]]]},
|
||||
{"info": {"input_cost_per_token": 1.25e-06, "input_cost_per_token_above_200k_tokens": 2.5e-06, "output_cost_per_token": 1e-05, "output_cost_per_token_above_200k_tokens": 1.5e-05}, "inclusive": false, "checks": [[null, 1, [1.25e-06, 1e-05, 1.25e-06, 1.25e-06, 1.25e-06]], [null, 200000, [1.25e-06, 1e-05, 1.25e-06, 1.25e-06, 1.25e-06]], [null, 200001, [2.5e-06, 1.5e-05, 2.5e-06, 2.5e-06, 2.5e-06]], [null, 10000000, [2.5e-06, 1.5e-05, 2.5e-06, 2.5e-06, 2.5e-06]]]},
|
||||
{"info": {"cache_read_input_token_cost": 1.25e-06, "input_cost_per_token": 5e-06}, "inclusive": false, "checks": [[null, 1, [5e-06, 0.0, 5e-06, 5e-06, 1.25e-06]], [null, 10000000, [5e-06, 0.0, 5e-06, 5e-06, 1.25e-06]]]},
|
||||
{"info": {"input_cost_per_token": 3e-07, "input_cost_per_token_above_512k_tokens": 6e-07, "output_cost_per_token": 1.2e-06, "output_cost_per_token_above_512k_tokens": 2.4e-06, "cache_read_input_token_cost": 6e-08, "cache_read_input_token_cost_above_512k_tokens": 1.2e-07}, "inclusive": false, "checks": [[null, 1, [3e-07, 1.2e-06, 3e-07, 3e-07, 6e-08]], [null, 512000, [3e-07, 1.2e-06, 3e-07, 3e-07, 6e-08]], [null, 512001, [6e-07, 2.4e-06, 6e-07, 6e-07, 1.2e-07]], [null, 10000000, [6e-07, 2.4e-06, 6e-07, 6e-07, 1.2e-07]]]},
|
||||
{"info": {"cache_read_input_token_cost": 1.25e-06, "cache_read_input_token_cost_batches": 6.25e-07, "input_cost_per_token": 5e-06, "input_cost_per_token_batches": 2.5e-06}, "inclusive": false, "checks": [[null, 1, [5e-06, 0.0, 5e-06, 5e-06, 1.25e-06]], [null, 10000000, [5e-06, 0.0, 5e-06, 5e-06, 1.25e-06]]]},
|
||||
{"info": {"cache_read_input_token_cost": 5e-09, "cache_read_input_token_cost_batches": 2.5e-09, "cache_read_input_token_cost_flex": 2.5e-09, "input_cost_per_token": 5e-08, "input_cost_per_token_batches": 2.5e-08, "input_cost_per_token_flex": 2.5e-08, "input_cost_per_token_priority": 2.5e-06, "output_cost_per_token": 4e-07, "output_cost_per_token_batches": 2e-07, "output_cost_per_token_flex": 2e-07}, "inclusive": false, "checks": [[null, 1, [5e-08, 4e-07, 5e-08, 5e-08, 5e-09]], [null, 10000000, [5e-08, 4e-07, 5e-08, 5e-08, 5e-09]], ["priority", 1, [2.5e-06, 4e-07, 2.5e-06, 2.5e-06, 5e-09]], ["priority", 10000000, [2.5e-06, 4e-07, 2.5e-06, 2.5e-06, 5e-09]], ["flex", 1, [2.5e-08, 2e-07, 2.5e-08, 2.5e-08, 2.5e-09]], ["flex", 10000000, [2.5e-08, 2e-07, 2.5e-08, 2.5e-08, 2.5e-09]]]},
|
||||
{"info": {"cache_read_input_token_cost": 2e-08, "cache_read_input_token_cost_batches": 1e-08, "cache_read_input_token_cost_flex": 1e-08, "input_cost_per_token": 2e-07, "output_cost_per_token": 1.25e-06, "input_cost_per_token_batches": 1e-07, "input_cost_per_token_flex": 1e-07, "output_cost_per_token_batches": 6.25e-07, "output_cost_per_token_flex": 6.25e-07}, "inclusive": false, "checks": [[null, 1, [2e-07, 1.25e-06, 2e-07, 2e-07, 2e-08]], [null, 10000000, [2e-07, 1.25e-06, 2e-07, 2e-07, 2e-08]], ["flex", 1, [1e-07, 6.25e-07, 1e-07, 1e-07, 1e-08]], ["flex", 10000000, [1e-07, 6.25e-07, 1e-07, 1e-07, 1e-08]]]},
|
||||
{"info": {"input_cost_per_token": 3e-05, "input_cost_per_token_above_272k_tokens": 6e-05, "input_cost_per_token_flex": 1.5e-05, "input_cost_per_token_batches": 1.5e-05, "input_cost_per_token_above_272k_tokens_batches": 3e-05, "output_cost_per_token": 0.00018, "output_cost_per_token_above_272k_tokens": 0.00027, "output_cost_per_token_flex": 9e-05, "output_cost_per_token_batches": 9e-05, "output_cost_per_token_above_272k_tokens_batches": 0.000135, "input_cost_per_token_above_272k_tokens_flex": 3e-05, "output_cost_per_token_above_272k_tokens_flex": 0.000135}, "inclusive": false, "checks": [[null, 1, [3e-05, 0.00018, 3e-05, 3e-05, 3e-05]], [null, 272000, [3e-05, 0.00018, 3e-05, 3e-05, 3e-05]], [null, 272001, [6e-05, 0.00027, 6e-05, 6e-05, 6e-05]], [null, 10000000, [6e-05, 0.00027, 6e-05, 6e-05, 6e-05]], ["flex", 1, [1.5e-05, 9e-05, 1.5e-05, 1.5e-05, 1.5e-05]], ["flex", 272000, [1.5e-05, 9e-05, 1.5e-05, 1.5e-05, 1.5e-05]], ["flex", 272001, [3e-05, 0.000135, 3e-05, 3e-05, 3e-05]], ["flex", 10000000, [3e-05, 0.000135, 3e-05, 3e-05, 3e-05]]]},
|
||||
{"info": {"cache_read_input_token_cost": 5e-07, "cache_read_input_token_cost_above_272k_tokens": 1e-06, "cache_read_input_token_cost_batches": 2.5e-07, "cache_read_input_token_cost_above_272k_tokens_batches": 5e-07, "cache_read_input_token_cost_flex": 2.5e-07, "cache_read_input_token_cost_priority": 1.25e-06, "input_cost_per_token": 5e-06, "input_cost_per_token_above_272k_tokens": 1e-05, "input_cost_per_token_flex": 2.5e-06, "input_cost_per_token_batches": 2.5e-06, "input_cost_per_token_above_272k_tokens_batches": 5e-06, "input_cost_per_token_priority": 1.25e-05, "output_cost_per_token": 3e-05, "output_cost_per_token_above_272k_tokens": 4.5e-05, "output_cost_per_token_flex": 1.5e-05, "output_cost_per_token_batches": 1.5e-05, "output_cost_per_token_above_272k_tokens_batches": 2.25e-05, "output_cost_per_token_priority": 7.5e-05, "input_cost_per_token_above_272k_tokens_flex": 5e-06, "output_cost_per_token_above_272k_tokens_flex": 2.25e-05, "cache_read_input_token_cost_above_272k_tokens_flex": 5e-07}, "inclusive": false, "checks": [[null, 1, [5e-06, 3e-05, 5e-06, 5e-06, 5e-07]], [null, 272000, [5e-06, 3e-05, 5e-06, 5e-06, 5e-07]], [null, 272001, [1e-05, 4.5e-05, 1e-05, 1e-05, 1e-06]], [null, 10000000, [1e-05, 4.5e-05, 1e-05, 1e-05, 1e-06]], ["priority", 1, [1.25e-05, 7.5e-05, 1.25e-05, 1.25e-05, 1.25e-06]], ["priority", 272000, [1.25e-05, 7.5e-05, 1.25e-05, 1.25e-05, 1.25e-06]], ["priority", 272001, [1e-05, 4.5e-05, 1e-05, 1e-05, 1e-06]], ["priority", 10000000, [1e-05, 4.5e-05, 1e-05, 1e-05, 1e-06]], ["flex", 1, [2.5e-06, 1.5e-05, 2.5e-06, 2.5e-06, 2.5e-07]], ["flex", 272000, [2.5e-06, 1.5e-05, 2.5e-06, 2.5e-06, 2.5e-07]], ["flex", 272001, [5e-06, 2.25e-05, 5e-06, 5e-06, 5e-07]], ["flex", 10000000, [5e-06, 2.25e-05, 5e-06, 5e-06, 5e-07]]]},
|
||||
{"info": {"input_cost_per_token": 3e-05, "input_cost_per_token_above_272k_tokens": 6e-05, "input_cost_per_token_flex": 1.5e-05, "input_cost_per_token_batches": 1.5e-05, "input_cost_per_token_above_272k_tokens_batches": 3e-05, "output_cost_per_token": 0.00018, "output_cost_per_token_above_272k_tokens": 0.00027, "output_cost_per_token_flex": 9e-05, "output_cost_per_token_batches": 9e-05, "output_cost_per_token_above_272k_tokens_batches": 0.000135}, "inclusive": false, "checks": [[null, 1, [3e-05, 0.00018, 3e-05, 3e-05, 3e-05]], [null, 272000, [3e-05, 0.00018, 3e-05, 3e-05, 3e-05]], [null, 272001, [6e-05, 0.00027, 6e-05, 6e-05, 6e-05]], [null, 10000000, [6e-05, 0.00027, 6e-05, 6e-05, 6e-05]], ["flex", 1, [1.5e-05, 9e-05, 1.5e-05, 1.5e-05, 1.5e-05]], ["flex", 272000, [1.5e-05, 9e-05, 1.5e-05, 1.5e-05, 1.5e-05]], ["flex", 272001, [6e-05, 0.00027, 6e-05, 6e-05, 6e-05]], ["flex", 10000000, [6e-05, 0.00027, 6e-05, 6e-05, 6e-05]]]},
|
||||
{"info": {"cache_creation_input_token_cost": 2.5e-06, "cache_creation_input_token_cost_above_272k_tokens": 5e-06, "cache_creation_input_token_cost_above_272k_tokens_flex": 2.5e-06, "cache_creation_input_token_cost_above_272k_tokens_priority": 1e-05, "cache_creation_input_token_cost_flex": 1.25e-06, "cache_creation_input_token_cost_priority": 5e-06, "cache_read_input_token_cost": 2e-07, "cache_read_input_token_cost_above_272k_tokens": 4e-07, "cache_read_input_token_cost_above_272k_tokens_flex": 2e-07, "cache_read_input_token_cost_above_272k_tokens_priority": 8e-07, "cache_read_input_token_cost_batches": 1e-07, "cache_read_input_token_cost_above_272k_tokens_batches": 2e-07, "cache_creation_input_token_cost_batches": 1.25e-06, "cache_creation_input_token_cost_above_272k_tokens_batches": 2.5e-06, "cache_read_input_token_cost_flex": 1e-07, "cache_read_input_token_cost_priority": 4e-07, "input_cost_per_token": 2e-06, "input_cost_per_token_above_272k_tokens": 4e-06, "input_cost_per_token_above_272k_tokens_flex": 2e-06, "input_cost_per_token_above_272k_tokens_priority": 8e-06, "input_cost_per_token_batches": 1e-06, "input_cost_per_token_above_272k_tokens_batches": 2e-06, "input_cost_per_token_flex": 1e-06, "input_cost_per_token_priority": 4e-06, "output_cost_per_token": 1e-05, "output_cost_per_token_above_272k_tokens": 1.5e-05, "output_cost_per_token_above_272k_tokens_flex": 7.5e-06, "output_cost_per_token_above_272k_tokens_priority": 3e-05, "output_cost_per_token_batches": 5e-06, "output_cost_per_token_above_272k_tokens_batches": 7.5e-06, "output_cost_per_token_flex": 5e-06, "output_cost_per_token_priority": 2e-05}, "inclusive": false, "checks": [[null, 1, [2e-06, 1e-05, 2.5e-06, 2.5e-06, 2e-07]], [null, 272000, [2e-06, 1e-05, 2.5e-06, 2.5e-06, 2e-07]], [null, 272001, [4e-06, 1.5e-05, 5e-06, 5e-06, 4e-07]], [null, 10000000, [4e-06, 1.5e-05, 5e-06, 5e-06, 4e-07]], ["priority", 1, [4e-06, 2e-05, 5e-06, 5e-06, 4e-07]], ["priority", 272000, [4e-06, 2e-05, 5e-06, 5e-06, 4e-07]], ["priority", 272001, [8e-06, 3e-05, 1e-05, 1e-05, 8e-07]], ["priority", 10000000, [8e-06, 3e-05, 1e-05, 1e-05, 8e-07]], ["flex", 1, [1e-06, 5e-06, 1.25e-06, 1.25e-06, 1e-07]], ["flex", 272000, [1e-06, 5e-06, 1.25e-06, 1.25e-06, 1e-07]], ["flex", 272001, [2e-06, 7.5e-06, 2.5e-06, 2.5e-06, 2e-07]], ["flex", 10000000, [2e-06, 7.5e-06, 2.5e-06, 2.5e-06, 2e-07]]]},
|
||||
{"info": {"cache_creation_input_token_cost": 1.25e-07, "cache_creation_input_token_cost_decisions": 1e-07, "cache_creation_input_token_cost_above_272k_tokens": 2.5e-07, "cache_creation_input_token_cost_above_272k_tokens_decisions": 2e-07, "cache_creation_input_token_cost_above_272k_tokens_flex": 1.25e-07, "cache_creation_input_token_cost_above_272k_tokens_priority": 5e-07, "cache_creation_input_token_cost_flex": 6.25e-08, "cache_creation_input_token_cost_priority": 2.5e-07, "cache_read_input_token_cost": 1e-08, "cache_read_input_token_cost_decisions": 1e-07, "cache_read_input_token_cost_above_272k_tokens": 2e-08, "cache_read_input_token_cost_above_272k_tokens_decisions": 2e-07, "cache_read_input_token_cost_above_272k_tokens_flex": 1e-08, "cache_read_input_token_cost_above_272k_tokens_priority": 4e-08, "cache_read_input_token_cost_batches": 5e-09, "cache_read_input_token_cost_above_272k_tokens_batches": 1e-08, "cache_creation_input_token_cost_batches": 6.25e-08, "cache_creation_input_token_cost_above_272k_tokens_batches": 1.25e-07, "cache_read_input_token_cost_flex": 5e-09, "cache_read_input_token_cost_priority": 2e-08, "input_cost_per_token": 1e-07, "input_cost_per_token_decisions": 1e-07, "input_cost_per_token_above_272k_tokens": 2e-07, "input_cost_per_token_above_272k_tokens_decisions": 2e-07, "input_cost_per_token_above_272k_tokens_flex": 1e-07, "input_cost_per_token_above_272k_tokens_priority": 4e-07, "input_cost_per_token_batches": 5e-08, "input_cost_per_token_above_272k_tokens_batches": 1e-07, "input_cost_per_token_flex": 5e-08, "input_cost_per_token_priority": 2e-07, "output_cost_per_token": 5e-07, "output_cost_per_token_decisions": 0.0, "output_cost_per_token_above_272k_tokens": 7.5e-07, "output_cost_per_token_above_272k_tokens_decisions": 0.0, "output_cost_per_token_above_272k_tokens_flex": 3.75e-07, "output_cost_per_token_above_272k_tokens_priority": 1.5e-06, "output_cost_per_token_batches": 2.5e-07, "output_cost_per_token_above_272k_tokens_batches": 3.75e-07, "output_cost_per_token_flex": 2.5e-07, "output_cost_per_token_priority": 1e-06}, "inclusive": false, "checks": [[null, 1, [1e-07, 5e-07, 1.25e-07, 1.25e-07, 1e-08]], [null, 272000, [1e-07, 5e-07, 1.25e-07, 1.25e-07, 1e-08]], [null, 272001, [2e-07, 7.5e-07, 2.5e-07, 2.5e-07, 2e-08]], [null, 10000000, [2e-07, 7.5e-07, 2.5e-07, 2.5e-07, 2e-08]], ["priority", 1, [2e-07, 1e-06, 2.5e-07, 2.5e-07, 2e-08]], ["priority", 272000, [2e-07, 1e-06, 2.5e-07, 2.5e-07, 2e-08]], ["priority", 272001, [4e-07, 1.5e-06, 5e-07, 5e-07, 4e-08]], ["priority", 10000000, [4e-07, 1.5e-06, 5e-07, 5e-07, 4e-08]], ["flex", 1, [5e-08, 2.5e-07, 6.25e-08, 6.25e-08, 5e-09]], ["flex", 272000, [5e-08, 2.5e-07, 6.25e-08, 6.25e-08, 5e-09]], ["flex", 272001, [1e-07, 3.75e-07, 1.25e-07, 1.25e-07, 1e-08]], ["flex", 10000000, [1e-07, 3.75e-07, 1.25e-07, 1.25e-07, 1e-08]]]},
|
||||
{"info": {"cache_creation_input_token_cost": 1.25e-07, "cache_creation_input_token_cost_above_100k_tokens": 6.25e-07, "cache_read_input_token_cost": 1e-08, "cache_read_input_token_cost_above_100k_tokens": 5e-08, "input_cost_per_token": 1e-07, "input_cost_per_token_above_100k_tokens": 5e-07, "output_cost_per_token": 5e-07, "output_cost_per_token_above_100k_tokens": 2.5e-06}, "inclusive": false, "checks": [[null, 1, [1e-07, 5e-07, 1.25e-07, 1.25e-07, 1e-08]], [null, 100000, [1e-07, 5e-07, 1.25e-07, 1.25e-07, 1e-08]], [null, 100001, [5e-07, 2.5e-06, 6.25e-07, 6.25e-07, 5e-08]], [null, 10000000, [5e-07, 2.5e-06, 6.25e-07, 6.25e-07, 5e-08]]]},
|
||||
{"info": {"input_cost_per_token": 0, "input_cost_per_token_above_128k_tokens": 0, "output_cost_per_token": 0, "output_cost_per_token_above_128k_tokens": 0}, "inclusive": false, "checks": [[null, 1, [0.0, 0.0, 0.0, 0.0, 0.0]], [null, 128000, [0.0, 0.0, 0.0, 0.0, 0.0]], [null, 128001, [0.0, 0.0, 0.0, 0.0, 0.0]], [null, 10000000, [0.0, 0.0, 0.0, 0.0, 0.0]]]},
|
||||
{"info": {"cache_read_input_token_cost": 5e-08, "cache_read_input_token_cost_priority": 9e-08, "input_cost_per_token": 5e-07, "input_cost_per_token_batches": 2.5e-07, "input_cost_per_token_flex": 2.5e-07, "input_cost_per_token_priority": 9e-07, "output_cost_per_token": 3e-06, "output_cost_per_token_batches": 1.5e-06, "output_cost_per_token_flex": 1.5e-06, "output_cost_per_token_priority": 5.4e-06}, "inclusive": false, "checks": [[null, 1, [5e-07, 3e-06, 5e-07, 5e-07, 5e-08]], [null, 10000000, [5e-07, 3e-06, 5e-07, 5e-07, 5e-08]], ["priority", 1, [9e-07, 5.4e-06, 9e-07, 9e-07, 9e-08]], ["priority", 10000000, [9e-07, 5.4e-06, 9e-07, 9e-07, 9e-08]], ["flex", 1, [2.5e-07, 1.5e-06, 2.5e-07, 2.5e-07, 5e-08]], ["flex", 10000000, [2.5e-07, 1.5e-06, 2.5e-07, 2.5e-07, 5e-08]]]},
|
||||
{"info": {"cache_read_input_token_cost": 3e-08, "input_cost_per_token": 3e-07, "input_cost_per_token_batches": 1.5e-07, "input_cost_per_token_flex": 1.5e-07, "input_cost_per_token_priority": 5.4e-07, "output_cost_per_token": 2.5e-06}, "inclusive": false, "checks": [[null, 1, [3e-07, 2.5e-06, 3e-07, 3e-07, 3e-08]], [null, 10000000, [3e-07, 2.5e-06, 3e-07, 3e-07, 3e-08]], ["priority", 1, [5.4e-07, 2.5e-06, 5.4e-07, 5.4e-07, 3e-08]], ["priority", 10000000, [5.4e-07, 2.5e-06, 5.4e-07, 5.4e-07, 3e-08]], ["flex", 1, [1.5e-07, 2.5e-06, 1.5e-07, 1.5e-07, 3e-08]], ["flex", 10000000, [1.5e-07, 2.5e-06, 1.5e-07, 1.5e-07, 3e-08]]]},
|
||||
{"info": {"cache_read_input_token_cost": 8.3e-08, "input_cost_per_token": 1.65e-07, "input_cost_per_token_batches": 8.3e-08, "output_cost_per_token": 6.6e-07}, "inclusive": false, "checks": [[null, 1, [1.65e-07, 6.6e-07, 1.65e-07, 1.65e-07, 8.3e-08]], [null, 10000000, [1.65e-07, 6.6e-07, 1.65e-07, 1.65e-07, 8.3e-08]]]},
|
||||
{"info": {"cache_read_input_token_cost": 2e-07, "cache_read_input_token_cost_above_200k_tokens": 4e-07, "cache_creation_input_token_cost_above_200k_tokens": 2.5e-07, "input_cost_per_token": 2e-06, "input_cost_per_token_above_200k_tokens": 4e-06, "input_cost_per_token_batches": 1e-06, "output_cost_per_token": 1.2e-05, "output_cost_per_token_above_200k_tokens": 1.8e-05, "output_cost_per_token_batches": 6e-06, "input_cost_per_token_priority": 3.6e-06, "input_cost_per_token_above_200k_tokens_priority": 7.2e-06, "output_cost_per_token_priority": 2.16e-05, "output_cost_per_token_above_200k_tokens_priority": 3.24e-05, "cache_read_input_token_cost_priority": 3.6e-07, "cache_read_input_token_cost_above_200k_tokens_priority": 7.2e-07}, "inclusive": false, "checks": [[null, 1, [2e-06, 1.2e-05, 2e-06, 2e-06, 2e-07]], [null, 200000, [2e-06, 1.2e-05, 2e-06, 2e-06, 2e-07]], [null, 200001, [4e-06, 1.8e-05, 2.5e-07, 2.5e-07, 4e-07]], [null, 10000000, [4e-06, 1.8e-05, 2.5e-07, 2.5e-07, 4e-07]], ["priority", 1, [3.6e-06, 2.16e-05, 3.6e-06, 3.6e-06, 3.6e-07]], ["priority", 200000, [3.6e-06, 2.16e-05, 3.6e-06, 3.6e-06, 3.6e-07]], ["priority", 200001, [7.2e-06, 3.24e-05, 2.5e-07, 2.5e-07, 7.2e-07]], ["priority", 10000000, [7.2e-06, 3.24e-05, 2.5e-07, 2.5e-07, 7.2e-07]]]},
|
||||
{"info": {"cache_creation_input_token_cost_above_200k_tokens": 2.5e-07, "cache_read_input_token_cost": 2e-07, "cache_read_input_token_cost_above_200k_tokens": 4e-07, "cache_read_input_token_cost_above_200k_tokens_priority": 7.2e-07, "cache_read_input_token_cost_priority": 3.6e-07, "input_cost_per_token": 2e-06, "input_cost_per_token_above_200k_tokens": 4e-06, "input_cost_per_token_above_200k_tokens_priority": 7.2e-06, "input_cost_per_token_batches": 1e-06, "input_cost_per_token_flex": 1e-06, "input_cost_per_token_priority": 3.6e-06, "output_cost_per_token": 1.2e-05, "output_cost_per_token_above_200k_tokens": 1.8e-05, "output_cost_per_token_above_200k_tokens_priority": 3.24e-05, "output_cost_per_token_batches": 6e-06, "output_cost_per_token_flex": 6e-06, "output_cost_per_token_priority": 2.16e-05}, "inclusive": false, "checks": [[null, 1, [2e-06, 1.2e-05, 2e-06, 2e-06, 2e-07]], [null, 200000, [2e-06, 1.2e-05, 2e-06, 2e-06, 2e-07]], [null, 200001, [4e-06, 1.8e-05, 2.5e-07, 2.5e-07, 4e-07]], [null, 10000000, [4e-06, 1.8e-05, 2.5e-07, 2.5e-07, 4e-07]], ["priority", 1, [3.6e-06, 2.16e-05, 3.6e-06, 3.6e-06, 3.6e-07]], ["priority", 200000, [3.6e-06, 2.16e-05, 3.6e-06, 3.6e-06, 3.6e-07]], ["priority", 200001, [7.2e-06, 3.24e-05, 2.5e-07, 2.5e-07, 7.2e-07]], ["priority", 10000000, [7.2e-06, 3.24e-05, 2.5e-07, 2.5e-07, 7.2e-07]], ["flex", 1, [1e-06, 6e-06, 1e-06, 1e-06, 2e-07]], ["flex", 200000, [1e-06, 6e-06, 1e-06, 1e-06, 2e-07]], ["flex", 200001, [4e-06, 1.8e-05, 2.5e-07, 2.5e-07, 4e-07]], ["flex", 10000000, [4e-06, 1.8e-05, 2.5e-07, 2.5e-07, 4e-07]]]},
|
||||
{"info": {"cache_read_input_token_cost": 2e-07, "cache_read_input_token_cost_above_200k_tokens": 4e-07, "cache_creation_input_token_cost_above_200k_tokens": 2.5e-07, "input_cost_per_token": 2e-06, "input_cost_per_token_above_200k_tokens": 4e-06, "input_cost_per_token_batches": 1e-06, "output_cost_per_token": 1.2e-05, "output_cost_per_token_above_200k_tokens": 1.8e-05, "output_cost_per_token_batches": 6e-06}, "inclusive": false, "checks": [[null, 1, [2e-06, 1.2e-05, 2e-06, 2e-06, 2e-07]], [null, 200000, [2e-06, 1.2e-05, 2e-06, 2e-06, 2e-07]], [null, 200001, [4e-06, 1.8e-05, 2.5e-07, 2.5e-07, 4e-07]], [null, 10000000, [4e-06, 1.8e-05, 2.5e-07, 2.5e-07, 4e-07]]]},
|
||||
{"info": {"cache_read_input_token_cost": 1.25e-07, "cache_read_input_token_cost_above_200k_tokens": 2.5e-07, "cache_creation_input_token_cost_above_200k_tokens": 2.5e-07, "cache_read_input_token_cost_above_200k_tokens_priority": 4.5e-07, "cache_read_input_token_cost_flex": 1.25e-07, "cache_read_input_token_cost_priority": 2.25e-07, "input_cost_per_token": 1.25e-06, "input_cost_per_token_above_200k_tokens": 2.5e-06, "output_cost_per_token": 1e-05, "output_cost_per_token_above_200k_tokens": 1.5e-05, "input_cost_per_token_above_200k_tokens_priority": 4.5e-06, "input_cost_per_token_batches": 6.25e-07, "input_cost_per_token_flex": 6.25e-07, "input_cost_per_token_priority": 2.25e-06, "output_cost_per_token_above_200k_tokens_priority": 2.7e-05, "output_cost_per_token_batches": 5e-06, "output_cost_per_token_flex": 5e-06, "output_cost_per_token_priority": 1.8e-05}, "inclusive": false, "checks": [[null, 1, [1.25e-06, 1e-05, 1.25e-06, 1.25e-06, 1.25e-07]], [null, 200000, [1.25e-06, 1e-05, 1.25e-06, 1.25e-06, 1.25e-07]], [null, 200001, [2.5e-06, 1.5e-05, 2.5e-07, 2.5e-07, 2.5e-07]], [null, 10000000, [2.5e-06, 1.5e-05, 2.5e-07, 2.5e-07, 2.5e-07]], ["priority", 1, [2.25e-06, 1.8e-05, 2.25e-06, 2.25e-06, 2.25e-07]], ["priority", 200000, [2.25e-06, 1.8e-05, 2.25e-06, 2.25e-06, 2.25e-07]], ["priority", 200001, [4.5e-06, 2.7e-05, 2.5e-07, 2.5e-07, 4.5e-07]], ["priority", 10000000, [4.5e-06, 2.7e-05, 2.5e-07, 2.5e-07, 4.5e-07]], ["flex", 1, [6.25e-07, 5e-06, 6.25e-07, 6.25e-07, 1.25e-07]], ["flex", 200000, [6.25e-07, 5e-06, 6.25e-07, 6.25e-07, 1.25e-07]], ["flex", 200001, [2.5e-06, 1.5e-05, 2.5e-07, 2.5e-07, 2.5e-07]], ["flex", 10000000, [2.5e-06, 1.5e-05, 2.5e-07, 2.5e-07, 2.5e-07]]]},
|
||||
{"info": {"cache_creation_input_token_cost": 1.875e-06, "input_cost_per_token": 3.75e-06, "output_cost_per_token": 1.5e-05}, "inclusive": false, "checks": [[null, 1, [3.75e-06, 1.5e-05, 1.875e-06, 1.875e-06, 3.75e-06]], [null, 10000000, [3.75e-06, 1.5e-05, 1.875e-06, 1.875e-06, 3.75e-06]]]},
|
||||
{"info": {"cache_read_input_token_cost": 2.1e-07, "cache_read_input_token_cost_flex": 1.05e-07, "input_cost_per_token": 8.4e-07, "input_cost_per_token_flex": 4.2e-07, "output_cost_per_token": 3.36e-06, "output_cost_per_token_flex": 1.68e-06}, "inclusive": false, "checks": [[null, 1, [8.4e-07, 3.36e-06, 8.4e-07, 8.4e-07, 2.1e-07]], [null, 10000000, [8.4e-07, 3.36e-06, 8.4e-07, 8.4e-07, 2.1e-07]], ["flex", 1, [4.2e-07, 1.68e-06, 4.2e-07, 4.2e-07, 1.05e-07]], ["flex", 10000000, [4.2e-07, 1.68e-06, 4.2e-07, 4.2e-07, 1.05e-07]]]},
|
||||
{"info": {"input_cost_per_token": 2.5e-06, "output_cost_per_token": 1.25e-05, "cache_read_input_token_cost": 2.5e-07, "input_cost_per_token_balanced": 2e-06, "output_cost_per_token_balanced": 1e-05, "cache_read_input_token_cost_balanced": 2e-07, "input_cost_per_token_flex": 1.25e-06, "output_cost_per_token_flex": 6.25e-06, "cache_read_input_token_cost_flex": 1.5e-07}, "inclusive": false, "checks": [[null, 1, [2.5e-06, 1.25e-05, 2.5e-06, 2.5e-06, 2.5e-07]], [null, 10000000, [2.5e-06, 1.25e-05, 2.5e-06, 2.5e-06, 2.5e-07]], ["flex", 1, [1.25e-06, 6.25e-06, 1.25e-06, 1.25e-06, 1.5e-07]], ["flex", 10000000, [1.25e-06, 6.25e-06, 1.25e-06, 1.25e-06, 1.5e-07]], ["balanced", 1, [2e-06, 1e-05, 2e-06, 2e-06, 2e-07]], ["balanced", 10000000, [2e-06, 1e-05, 2e-06, 2e-06, 2e-07]]]},
|
||||
{"info": {"tiered_pricing": [{"input_cost_per_query": 0.00166, "max_results_range": [1, 10]}, {"input_cost_per_query": 0.00332, "max_results_range": [11, 20]}, {"input_cost_per_query": 0.00498, "max_results_range": [21, 30]}, {"input_cost_per_query": 0.00664, "max_results_range": [31, 40]}, {"input_cost_per_query": 0.0083, "max_results_range": [41, 50]}, {"input_cost_per_query": 0.00996, "max_results_range": [51, 60]}, {"input_cost_per_query": 0.01162, "max_results_range": [61, 70]}, {"input_cost_per_query": 0.01328, "max_results_range": [71, 80]}, {"input_cost_per_query": 0.01494, "max_results_range": [81, 90]}, {"input_cost_per_query": 0.0166, "max_results_range": [91, 100]}]}, "inclusive": false, "checks": [[null, 1, [0.0, 0.0, 0.0, 0.0, 0.0]], [null, 10000000, [0.0, 0.0, 0.0, 0.0, 0.0]]]},
|
||||
{"info": {"tiered_pricing": [{"input_cost_per_query": 0.005, "max_results_range": [0, 25]}, {"input_cost_per_query": 0.025, "max_results_range": [26, 100]}]}, "inclusive": false, "checks": [[null, 1, [0.0, 0.0, 0.0, 0.0, 0.0]], [null, 10000000, [0.0, 0.0, 0.0, 0.0, 0.0]]]},
|
||||
{"info": {"tiered_pricing": [{"cache_read_input_token_cost": 8e-08, "input_cost_per_token": 4e-07, "output_cost_per_token": 1.6e-06, "range": [0, 256000.0]}, {"cache_read_input_token_cost": 2.4e-07, "input_cost_per_token": 1.2e-06, "output_cost_per_token": 4.8e-06, "range": [256000.0, 1000000.0]}]}, "inclusive": false, "checks": [[null, 1, [4e-07, 1.6e-06, 4e-07, 4e-07, 8e-08]], [null, 256000, [4e-07, 1.6e-06, 4e-07, 4e-07, 8e-08]], [null, 256001, [1.2e-06, 4.8e-06, 1.2e-06, 1.2e-06, 2.4e-07]], [null, 10000000, [1.2e-06, 4.8e-06, 1.2e-06, 1.2e-06, 2.4e-07]]]},
|
||||
{"info": {"tiered_pricing": [{"input_cost_per_token": 5e-08, "output_cost_per_token": 4e-07, "range": [0, 256000.0]}, {"input_cost_per_token": 2.5e-07, "output_cost_per_token": 2e-06, "range": [256000.0, 1000000.0]}]}, "inclusive": false, "checks": [[null, 1, [5e-08, 4e-07, 5e-08, 5e-08, 5e-08]], [null, 256000, [5e-08, 4e-07, 5e-08, 5e-08, 5e-08]], [null, 256001, [2.5e-07, 2e-06, 2.5e-07, 2.5e-07, 2.5e-07]], [null, 10000000, [2.5e-07, 2e-06, 2.5e-07, 2.5e-07, 2.5e-07]]]},
|
||||
{"info": {"tiered_pricing": [{"input_cost_per_token": 3e-07, "output_cost_per_token": 1.5e-06, "range": [0, 32000.0]}, {"input_cost_per_token": 5e-07, "output_cost_per_token": 2.5e-06, "range": [32000.0, 128000.0]}, {"input_cost_per_token": 8e-07, "output_cost_per_token": 4e-06, "range": [128000.0, 256000.0]}, {"input_cost_per_token": 1.6e-06, "output_cost_per_token": 9.6e-06, "range": [256000.0, 1000000.0]}]}, "inclusive": false, "checks": [[null, 1, [3e-07, 1.5e-06, 3e-07, 3e-07, 3e-07]], [null, 32000, [3e-07, 1.5e-06, 3e-07, 3e-07, 3e-07]], [null, 32001, [5e-07, 2.5e-06, 5e-07, 5e-07, 5e-07]], [null, 128000, [5e-07, 2.5e-06, 5e-07, 5e-07, 5e-07]], [null, 128001, [8e-07, 4e-06, 8e-07, 8e-07, 8e-07]], [null, 256000, [8e-07, 4e-06, 8e-07, 8e-07, 8e-07]], [null, 256001, [1.6e-06, 9.6e-06, 1.6e-06, 1.6e-06, 1.6e-06]], [null, 10000000, [1.6e-06, 9.6e-06, 1.6e-06, 1.6e-06, 1.6e-06]]]},
|
||||
{"info": {"tiered_pricing": [{"cache_read_input_token_cost": 8e-08, "input_cost_per_token": 3e-07, "output_cost_per_token": 1.5e-06, "range": [0, 32000.0]}, {"cache_read_input_token_cost": 1.2e-07, "input_cost_per_token": 5e-07, "output_cost_per_token": 2.5e-06, "range": [32000.0, 128000.0]}, {"cache_read_input_token_cost": 2e-07, "input_cost_per_token": 8e-07, "output_cost_per_token": 4e-06, "range": [128000.0, 256000.0]}, {"cache_read_input_token_cost": 4e-07, "input_cost_per_token": 1.6e-06, "output_cost_per_token": 9.6e-06, "range": [256000.0, 1000000.0]}]}, "inclusive": false, "checks": [[null, 1, [3e-07, 1.5e-06, 3e-07, 3e-07, 8e-08]], [null, 32000, [3e-07, 1.5e-06, 3e-07, 3e-07, 8e-08]], [null, 32001, [5e-07, 2.5e-06, 5e-07, 5e-07, 1.2e-07]], [null, 128000, [5e-07, 2.5e-06, 5e-07, 5e-07, 1.2e-07]], [null, 128001, [8e-07, 4e-06, 8e-07, 8e-07, 2e-07]], [null, 256000, [8e-07, 4e-06, 8e-07, 8e-07, 2e-07]], [null, 256001, [1.6e-06, 9.6e-06, 1.6e-06, 1.6e-06, 4e-07]], [null, 10000000, [1.6e-06, 9.6e-06, 1.6e-06, 1.6e-06, 4e-07]]]},
|
||||
{"info": {"tiered_pricing": [{"input_cost_per_token": 4e-07, "output_cost_per_reasoning_token": 4e-06, "output_cost_per_token": 1.2e-06, "range": [0, 256000.0]}, {"input_cost_per_token": 1.2e-06, "output_cost_per_reasoning_token": 1.2e-05, "output_cost_per_token": 3.6e-06, "range": [256000.0, 1000000.0]}]}, "inclusive": false, "checks": [[null, 1, [4e-07, 1.2e-06, 4e-07, 4e-07, 4e-07]], [null, 256000, [4e-07, 1.2e-06, 4e-07, 4e-07, 4e-07]], [null, 256001, [1.2e-06, 3.6e-06, 1.2e-06, 1.2e-06, 1.2e-06]], [null, 10000000, [1.2e-06, 3.6e-06, 1.2e-06, 1.2e-06, 1.2e-06]]]},
|
||||
{"info": {"cache_creation_input_token_cost": 1.375e-06, "cache_read_input_token_cost": 1.1e-07, "input_cost_per_token": 1.1e-06, "output_cost_per_token": 5.5e-06, "input_cost_per_token_batches": 5.5e-07, "output_cost_per_token_batches": 2.75e-06}, "inclusive": false, "checks": [[null, 1, [1.1e-06, 5.5e-06, 1.375e-06, 1.375e-06, 1.1e-07]], [null, 10000000, [1.1e-06, 5.5e-06, 1.375e-06, 1.375e-06, 1.1e-07]]]},
|
||||
{"info": {"cache_read_input_token_cost": 2.75e-07, "cache_read_input_token_cost_above_272k_tokens": 5.5e-07, "cache_read_input_token_cost_above_272k_tokens_batches": 2.75e-07, "cache_read_input_token_cost_batches": 1.43e-07, "cache_read_input_token_cost_priority": 5.5e-07, "input_cost_per_token": 2.75e-06, "input_cost_per_token_priority": 5.5e-06, "output_cost_per_token": 1.65e-05, "output_cost_per_token_priority": 3.3e-05, "input_cost_per_token_above_272k_tokens": 5.5e-06, "input_cost_per_token_above_272k_tokens_batches": 2.75e-06, "input_cost_per_token_batches": 1.375e-06, "output_cost_per_token_above_272k_tokens": 2.475e-05, "output_cost_per_token_above_272k_tokens_batches": 1.2375e-05, "output_cost_per_token_batches": 8.25e-06}, "inclusive": false, "checks": [[null, 1, [2.75e-06, 1.65e-05, 2.75e-06, 2.75e-06, 2.75e-07]], [null, 272000, [2.75e-06, 1.65e-05, 2.75e-06, 2.75e-06, 2.75e-07]], [null, 272001, [5.5e-06, 2.475e-05, 5.5e-06, 5.5e-06, 5.5e-07]], [null, 10000000, [5.5e-06, 2.475e-05, 5.5e-06, 5.5e-06, 5.5e-07]], ["priority", 1, [5.5e-06, 3.3e-05, 5.5e-06, 5.5e-06, 5.5e-07]], ["priority", 272000, [5.5e-06, 3.3e-05, 5.5e-06, 5.5e-06, 5.5e-07]], ["priority", 272001, [5.5e-06, 2.475e-05, 5.5e-06, 5.5e-06, 5.5e-07]], ["priority", 10000000, [5.5e-06, 2.475e-05, 5.5e-06, 5.5e-06, 5.5e-07]]]},
|
||||
{"info": {"cache_read_input_token_cost": 5e-07, "cache_read_input_token_cost_above_272k_tokens": 1e-06, "cache_read_input_token_cost_above_272k_tokens_batches": 5e-07, "cache_read_input_token_cost_priority": 1.25e-06, "cache_read_input_token_cost_above_272k_tokens_priority": 2e-06, "cache_read_input_token_cost_batches": 2.5e-07, "cache_read_input_token_cost_flex": 2.5e-07, "input_cost_per_token": 5e-06, "input_cost_per_token_above_272k_tokens": 1e-05, "input_cost_per_token_above_272k_tokens_batches": 5e-06, "input_cost_per_token_above_272k_tokens_flex": 5e-06, "input_cost_per_token_priority": 1.25e-05, "input_cost_per_token_above_272k_tokens_priority": 2e-05, "input_cost_per_token_batches": 2.5e-06, "input_cost_per_token_flex": 2.5e-06, "output_cost_per_token": 3e-05, "output_cost_per_token_above_272k_tokens": 4.5e-05, "output_cost_per_token_above_272k_tokens_batches": 2.25e-05, "output_cost_per_token_priority": 7.5e-05, "output_cost_per_token_above_272k_tokens_priority": 9e-05, "output_cost_per_token_batches": 1.5e-05}, "inclusive": false, "checks": [[null, 1, [5e-06, 3e-05, 5e-06, 5e-06, 5e-07]], [null, 272000, [5e-06, 3e-05, 5e-06, 5e-06, 5e-07]], [null, 272001, [1e-05, 4.5e-05, 1e-05, 1e-05, 1e-06]], [null, 10000000, [1e-05, 4.5e-05, 1e-05, 1e-05, 1e-06]], ["priority", 1, [1.25e-05, 7.5e-05, 1.25e-05, 1.25e-05, 1.25e-06]], ["priority", 272000, [1.25e-05, 7.5e-05, 1.25e-05, 1.25e-05, 1.25e-06]], ["priority", 272001, [2e-05, 9e-05, 2e-05, 2e-05, 2e-06]], ["priority", 10000000, [2e-05, 9e-05, 2e-05, 2e-05, 2e-06]], ["flex", 1, [2.5e-06, 3e-05, 2.5e-06, 2.5e-06, 2.5e-07]], ["flex", 272000, [2.5e-06, 3e-05, 2.5e-06, 2.5e-06, 2.5e-07]], ["flex", 272001, [5e-06, 4.5e-05, 5e-06, 5e-06, 1e-06]], ["flex", 10000000, [5e-06, 4.5e-05, 5e-06, 5e-06, 1e-06]]]},
|
||||
{"info": {"cache_creation_input_token_cost": 5e-06, "cache_creation_input_token_cost_above_272k_tokens": 1e-05, "cache_creation_input_token_cost_priority": 1e-05, "cache_creation_input_token_cost_above_272k_tokens_priority": 2e-05, "cache_read_input_token_cost": 4e-07, "cache_read_input_token_cost_above_272k_tokens": 8e-07, "cache_read_input_token_cost_priority": 8e-07, "cache_read_input_token_cost_above_272k_tokens_priority": 1.6e-06, "input_cost_per_token": 4e-06, "input_cost_per_token_above_272k_tokens": 8e-06, "input_cost_per_token_priority": 8e-06, "input_cost_per_token_above_272k_tokens_priority": 1.6e-05, "output_cost_per_token": 2e-05, "output_cost_per_token_above_272k_tokens": 3e-05, "output_cost_per_token_priority": 4e-05, "output_cost_per_token_above_272k_tokens_priority": 6e-05}, "inclusive": false, "checks": [[null, 1, [4e-06, 2e-05, 5e-06, 5e-06, 4e-07]], [null, 272000, [4e-06, 2e-05, 5e-06, 5e-06, 4e-07]], [null, 272001, [8e-06, 3e-05, 1e-05, 1e-05, 8e-07]], [null, 10000000, [8e-06, 3e-05, 1e-05, 1e-05, 8e-07]], ["priority", 1, [8e-06, 4e-05, 1e-05, 1e-05, 8e-07]], ["priority", 272000, [8e-06, 4e-05, 1e-05, 1e-05, 8e-07]], ["priority", 272001, [1.6e-05, 6e-05, 2e-05, 2e-05, 1.6e-06]], ["priority", 10000000, [1.6e-05, 6e-05, 2e-05, 2e-05, 1.6e-06]]]},
|
||||
{"info": {"cache_creation_input_token_cost": 5e-06, "cache_creation_input_token_cost_above_272k_tokens": 1e-05, "cache_creation_input_token_cost_above_272k_tokens_flex": 5e-06, "cache_creation_input_token_cost_priority": 1e-05, "cache_creation_input_token_cost_above_272k_tokens_priority": 2e-05, "cache_creation_input_token_cost_flex": 2.5e-06, "cache_read_input_token_cost": 4e-07, "cache_read_input_token_cost_above_272k_tokens": 8e-07, "cache_read_input_token_cost_above_272k_tokens_flex": 4e-07, "cache_read_input_token_cost_priority": 8e-07, "cache_read_input_token_cost_above_272k_tokens_priority": 1.6e-06, "cache_read_input_token_cost_flex": 2e-07, "input_cost_per_token": 4e-06, "input_cost_per_token_above_272k_tokens": 8e-06, "input_cost_per_token_above_272k_tokens_flex": 4e-06, "input_cost_per_token_priority": 8e-06, "input_cost_per_token_above_272k_tokens_priority": 1.6e-05, "input_cost_per_token_flex": 2e-06, "output_cost_per_token": 2e-05, "output_cost_per_token_above_272k_tokens": 3e-05, "output_cost_per_token_above_272k_tokens_flex": 1.5e-05, "output_cost_per_token_priority": 4e-05, "output_cost_per_token_above_272k_tokens_priority": 6e-05, "output_cost_per_token_flex": 1e-05}, "inclusive": false, "checks": [[null, 1, [4e-06, 2e-05, 5e-06, 5e-06, 4e-07]], [null, 272000, [4e-06, 2e-05, 5e-06, 5e-06, 4e-07]], [null, 272001, [8e-06, 3e-05, 1e-05, 1e-05, 8e-07]], [null, 10000000, [8e-06, 3e-05, 1e-05, 1e-05, 8e-07]], ["priority", 1, [8e-06, 4e-05, 1e-05, 1e-05, 8e-07]], ["priority", 272000, [8e-06, 4e-05, 1e-05, 1e-05, 8e-07]], ["priority", 272001, [1.6e-05, 6e-05, 2e-05, 2e-05, 1.6e-06]], ["priority", 10000000, [1.6e-05, 6e-05, 2e-05, 2e-05, 1.6e-06]], ["flex", 1, [2e-06, 1e-05, 2.5e-06, 2.5e-06, 2e-07]], ["flex", 272000, [2e-06, 1e-05, 2.5e-06, 2.5e-06, 2e-07]], ["flex", 272001, [4e-06, 1.5e-05, 5e-06, 5e-06, 4e-07]], ["flex", 10000000, [4e-06, 1.5e-05, 5e-06, 5e-06, 4e-07]]]},
|
||||
{"info": {"cache_read_input_token_cost": 3e-06, "cache_read_input_token_cost_above_272k_tokens": 6e-06, "input_cost_per_token": 3e-05, "input_cost_per_token_above_272k_tokens": 6e-05, "input_cost_per_token_above_272k_tokens_batches": 3e-05, "input_cost_per_token_above_272k_tokens_flex": 3e-05, "input_cost_per_token_batches": 1.5e-05, "input_cost_per_token_flex": 1.5e-05, "output_cost_per_token": 0.00018, "output_cost_per_token_above_272k_tokens": 0.00027, "output_cost_per_token_above_272k_tokens_batches": 0.000135, "output_cost_per_token_above_272k_tokens_flex": 0.000135, "output_cost_per_token_batches": 9e-05, "output_cost_per_token_flex": 9e-05}, "inclusive": false, "checks": [[null, 1, [3e-05, 0.00018, 3e-05, 3e-05, 3e-06]], [null, 272000, [3e-05, 0.00018, 3e-05, 3e-05, 3e-06]], [null, 272001, [6e-05, 0.00027, 6e-05, 6e-05, 6e-06]], [null, 10000000, [6e-05, 0.00027, 6e-05, 6e-05, 6e-06]], ["flex", 1, [1.5e-05, 9e-05, 1.5e-05, 1.5e-05, 3e-06]], ["flex", 272000, [1.5e-05, 9e-05, 1.5e-05, 1.5e-05, 3e-06]], ["flex", 272001, [3e-05, 0.000135, 3e-05, 3e-05, 6e-06]], ["flex", 10000000, [3e-05, 0.000135, 3e-05, 3e-05, 6e-06]]]},
|
||||
{"info": {"cache_read_input_token_cost": 2.5e-07, "cache_read_input_token_cost_above_272k_tokens": 5e-07, "cache_read_input_token_cost_above_272k_tokens_batches": 2.5e-07, "cache_read_input_token_cost_above_272k_tokens_flex": 2.5e-07, "cache_read_input_token_cost_priority": 5e-07, "cache_read_input_token_cost_above_272k_tokens_priority": 1e-06, "cache_read_input_token_cost_batches": 1.3e-07, "input_cost_per_token": 2.5e-06, "input_cost_per_token_above_272k_tokens": 5e-06, "input_cost_per_token_priority": 5e-06, "input_cost_per_token_above_272k_tokens_priority": 1e-05, "output_cost_per_token": 1.5e-05, "output_cost_per_token_above_272k_tokens": 2.25e-05, "output_cost_per_token_priority": 3e-05, "output_cost_per_token_above_272k_tokens_priority": 4.5e-05, "input_cost_per_token_above_272k_tokens_batches": 2.5e-06, "input_cost_per_token_above_272k_tokens_flex": 2.5e-06, "input_cost_per_token_batches": 1.25e-06, "input_cost_per_token_flex": 1.25e-06, "output_cost_per_token_above_272k_tokens_batches": 1.125e-05, "output_cost_per_token_above_272k_tokens_flex": 1.125e-05, "output_cost_per_token_batches": 7.5e-06, "output_cost_per_token_flex": 7.5e-06}, "inclusive": false, "checks": [[null, 1, [2.5e-06, 1.5e-05, 2.5e-06, 2.5e-06, 2.5e-07]], [null, 272000, [2.5e-06, 1.5e-05, 2.5e-06, 2.5e-06, 2.5e-07]], [null, 272001, [5e-06, 2.25e-05, 5e-06, 5e-06, 5e-07]], [null, 10000000, [5e-06, 2.25e-05, 5e-06, 5e-06, 5e-07]], ["priority", 1, [5e-06, 3e-05, 5e-06, 5e-06, 5e-07]], ["priority", 272000, [5e-06, 3e-05, 5e-06, 5e-06, 5e-07]], ["priority", 272001, [1e-05, 4.5e-05, 1e-05, 1e-05, 1e-06]], ["priority", 10000000, [1e-05, 4.5e-05, 1e-05, 1e-05, 1e-06]], ["flex", 1, [1.25e-06, 7.5e-06, 1.25e-06, 1.25e-06, 2.5e-07]], ["flex", 272000, [1.25e-06, 7.5e-06, 1.25e-06, 1.25e-06, 2.5e-07]], ["flex", 272001, [2.5e-06, 1.125e-05, 2.5e-06, 2.5e-06, 2.5e-07]], ["flex", 10000000, [2.5e-06, 1.125e-05, 2.5e-06, 2.5e-06, 2.5e-07]]]},
|
||||
{"info": {"cache_read_input_token_cost": 5e-07, "cache_read_input_token_cost_above_272k_tokens": 1e-06, "cache_read_input_token_cost_priority": 1e-06, "cache_read_input_token_cost_above_272k_tokens_priority": 2e-06, "input_cost_per_token": 5e-06, "input_cost_per_token_above_272k_tokens": 1e-05, "input_cost_per_token_priority": 1e-05, "input_cost_per_token_above_272k_tokens_priority": 2e-05, "output_cost_per_token": 3e-05, "output_cost_per_token_above_272k_tokens": 4.5e-05, "output_cost_per_token_priority": 6e-05, "output_cost_per_token_above_272k_tokens_priority": 9e-05}, "inclusive": false, "checks": [[null, 1, [5e-06, 3e-05, 5e-06, 5e-06, 5e-07]], [null, 272000, [5e-06, 3e-05, 5e-06, 5e-06, 5e-07]], [null, 272001, [1e-05, 4.5e-05, 1e-05, 1e-05, 1e-06]], [null, 10000000, [1e-05, 4.5e-05, 1e-05, 1e-05, 1e-06]], ["priority", 1, [1e-05, 6e-05, 1e-05, 1e-05, 1e-06]], ["priority", 272000, [1e-05, 6e-05, 1e-05, 1e-05, 1e-06]], ["priority", 272001, [2e-05, 9e-05, 2e-05, 2e-05, 2e-06]], ["priority", 10000000, [2e-05, 9e-05, 2e-05, 2e-05, 2e-06]]]},
|
||||
{"info": {"cache_read_input_token_cost": 3.125e-07, "input_cost_per_token": 1.25e-06, "input_cost_per_token_batches": 6.25e-07, "input_cost_per_token_priority": 2.1875e-06, "output_cost_per_token": 1e-05, "output_cost_per_token_batches": 5e-06, "output_cost_per_token_priority": 1.75e-05}, "inclusive": false, "checks": [[null, 1, [1.25e-06, 1e-05, 1.25e-06, 1.25e-06, 3.125e-07]], [null, 10000000, [1.25e-06, 1e-05, 1.25e-06, 1.25e-06, 3.125e-07]], ["priority", 1, [2.1875e-06, 1.75e-05, 2.1875e-06, 2.1875e-06, 3.125e-07]], ["priority", 10000000, [2.1875e-06, 1.75e-05, 2.1875e-06, 2.1875e-06, 3.125e-07]]]}
|
||||
]}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use litellm_model_catalog::{
|
||||
Catalog, FallbackGeneralizations, Provenance, validate_model_entry, validate_registry,
|
||||
};
|
||||
use rstest::{fixture, rstest};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
#[fixture]
|
||||
fn repo_root() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../..")
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("model_prices_and_context_window.json")]
|
||||
#[case("litellm/model_prices_and_context_window_backup.json")]
|
||||
#[ignore]
|
||||
fn checked_in_registry_passes_strict_validation(repo_root: PathBuf, #[case] filename: &str) {
|
||||
let body = std::fs::read(repo_root.join(filename)).unwrap();
|
||||
let catalog = Catalog::parse(&body, Provenance::default()).unwrap();
|
||||
validate_registry(&catalog).unwrap();
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[ignore]
|
||||
fn fallback_generalizations_are_typed(repo_root: PathBuf) {
|
||||
let body = std::fs::read(repo_root.join("model_prices_and_context_window.json")).unwrap();
|
||||
let document: Map<String, Value> = serde_json::from_slice(&body).unwrap();
|
||||
let Some(raw_rules) = document.get("fallback_generalizations") else {
|
||||
return;
|
||||
};
|
||||
let _: FallbackGeneralizations = serde_json::from_value(raw_rules.clone()).unwrap();
|
||||
let catalog = Catalog::parse(&body, Provenance::default()).unwrap();
|
||||
assert!(
|
||||
catalog
|
||||
.fallback_rules()
|
||||
.is_some_and(|rules| !rules.is_empty())
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::missing_provider(serde_json::json!({"mode": "chat"}), "litellm_provider")]
|
||||
#[case::unknown_field(serde_json::json!({"litellm_provider": "test", "typo": true}), "unknown field")]
|
||||
#[case::negative_price(serde_json::json!({"litellm_provider": "test", "input_cost_per_token": -1}), "nonnegative")]
|
||||
#[case::negative_nested_price(serde_json::json!({"litellm_provider": "test", "guardrail_cost_per_unit": {"unit": -1}}), "nonnegative")]
|
||||
#[case::invalid_mode(serde_json::json!({"litellm_provider": "test", "mode": "invalid"}), "unknown variant")]
|
||||
#[case::invalid_date(serde_json::json!({"litellm_provider": "test", "deprecation_date": "2026-02-31"}), "deprecation_date")]
|
||||
#[case::invalid_hours(serde_json::json!({"litellm_provider": "test", "off_peak_pricing": {"hours_utc": "25:00-01:00"}}), "hours_utc")]
|
||||
#[case::empty_windows(serde_json::json!({"litellm_provider": "test", "off_peak_pricing": {"windows": []}}), "windows is empty")]
|
||||
#[case::invalid_weekday(serde_json::json!({"litellm_provider": "test", "off_peak_pricing": {"windows": [{"hours_utc": "00:00-01:00", "weekdays": [0]}]}}), "weekdays is invalid")]
|
||||
#[case::invalid_aliases(serde_json::json!({"litellm_provider": "test", "aliases": ["good", 7]}), "aliases must contain strings")]
|
||||
#[case::null_aliases(serde_json::json!({"litellm_provider": "test", "aliases": null}), "aliases must be an array")]
|
||||
#[ignore]
|
||||
fn registry_validation_rejects_malformed_entries(#[case] entry: Value, #[case] expected: &str) {
|
||||
assert!(
|
||||
validate_model_entry("test", &entry)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains(expected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn checked_in_catalog_and_backup_match() {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../..");
|
||||
let current = std::fs::read(root.join("model_prices_and_context_window.json")).unwrap();
|
||||
let backup =
|
||||
std::fs::read(root.join("litellm/model_prices_and_context_window_backup.json")).unwrap();
|
||||
assert_eq!(current, backup);
|
||||
let catalog = Catalog::parse(¤t, Provenance::default()).unwrap();
|
||||
assert!(
|
||||
catalog.alias_issues().is_empty(),
|
||||
"invalid registry aliases"
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::input("input_cost_per_token_above_100k_tokens")]
|
||||
#[case::input_batches("input_cost_per_token_above_100k_tokens_batches")]
|
||||
#[case::output("output_cost_per_token_above_100k_tokens")]
|
||||
#[case::output_batches("output_cost_per_token_above_100k_tokens_batches")]
|
||||
#[case::cache_creation("cache_creation_input_token_cost_above_100k_tokens")]
|
||||
#[case::cache_creation_batches("cache_creation_input_token_cost_above_100k_tokens_batches")]
|
||||
#[case::cache_creation_1hr("cache_creation_input_token_cost_above_1hr_above_100k_tokens")]
|
||||
#[case::cache_read("cache_read_input_token_cost_above_100k_tokens")]
|
||||
#[case::cache_read_batches("cache_read_input_token_cost_above_100k_tokens_batches")]
|
||||
fn registry_validation_keeps_above_100k_tier_rates(#[case] field: &str) {
|
||||
let mut entry = Map::new();
|
||||
entry.insert("litellm_provider".into(), "anthropic".into());
|
||||
entry.insert(field.into(), 5e-7.into());
|
||||
validate_model_entry("test", &Value::Object(entry)).unwrap();
|
||||
}
|
||||
|
|
@ -1,16 +1,21 @@
|
|||
#![cfg(feature = "schema")]
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::Path;
|
||||
|
||||
use litellm_model_catalog::{model_entry_json_schema, registry_json_schema};
|
||||
use rstest::rstest;
|
||||
use serde_json::{Value, json};
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
fn schema() -> Value {
|
||||
serde_json::to_value(model_entry_json_schema()).expect("generated schema serializes")
|
||||
}
|
||||
|
||||
fn checked_in_catalog() -> Value {
|
||||
let path =
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../model_prices_and_context_window.json");
|
||||
serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap()
|
||||
}
|
||||
|
||||
fn registry_validator() -> jsonschema::Validator {
|
||||
let schema = serde_json::to_value(registry_json_schema()).unwrap();
|
||||
jsonschema::options()
|
||||
|
|
@ -19,26 +24,22 @@ fn registry_validator() -> jsonschema::Validator {
|
|||
.expect("generated registry schema is valid")
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("model_prices_and_context_window.json")]
|
||||
#[case("litellm/model_prices_and_context_window_backup.json")]
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn generated_registry_schema_validates_checked_in_catalog(#[case] path: &str) {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../..");
|
||||
let catalog: Value = serde_json::from_slice(&std::fs::read(root.join(path)).unwrap()).unwrap();
|
||||
fn generated_registry_schema_validates_checked_in_catalog() {
|
||||
let catalog = checked_in_catalog();
|
||||
let validator = registry_validator();
|
||||
let errors: Vec<_> = validator
|
||||
.iter_errors(&catalog)
|
||||
.map(|error| error.to_string())
|
||||
.collect();
|
||||
assert!(errors.is_empty(), "{path}: {errors:?}");
|
||||
assert!(errors.is_empty(), "{errors:?}");
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(json!({"example": {"litellm_provider": "test"}}))]
|
||||
#[case(json!({"example": {"litellm_provider": "test", "future_field": true}}))]
|
||||
#[case(json!({"sample_spec": {"litellm_provider": "placeholder"}}))]
|
||||
#[ignore]
|
||||
fn generated_registry_schema_keeps_reader_compatibility(#[case] document: Value) {
|
||||
assert!(registry_validator().is_valid(&document));
|
||||
}
|
||||
|
|
@ -59,47 +60,56 @@ fn generated_registry_schema_keeps_reader_compatibility(#[case] document: Value)
|
|||
#[case::invalid_weekday(json!({"litellm_provider": "test", "off_peak_pricing": {"windows": [{"hours_utc": "00:00-01:00", "weekdays": [0]}]}}))]
|
||||
#[case::invalid_aliases(json!({"litellm_provider": "test", "aliases": "wrong"}))]
|
||||
#[case::non_object_model(json!(4))]
|
||||
#[ignore]
|
||||
fn generated_registry_schema_rejects_invalid_entries(#[case] entry: Value) {
|
||||
assert!(!registry_validator().is_valid(&json!({"example": entry})));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("model_prices_and_context_window.json")]
|
||||
#[case("litellm/model_prices_and_context_window_backup.json")]
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn generated_schema_covers_catalog_fields(#[case] path: &str) {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../..");
|
||||
let catalog: Value = serde_json::from_slice(&std::fs::read(root.join(path)).unwrap()).unwrap();
|
||||
fn generated_schema_covers_catalog_fields() {
|
||||
let catalog = checked_in_catalog();
|
||||
let schema = schema();
|
||||
let properties = schema["properties"]
|
||||
let any_value = |keys: &Value| -> Map<String, Value> {
|
||||
keys.as_object()
|
||||
.expect("schema keys are an object")
|
||||
.keys()
|
||||
.map(|key| (key.clone(), json!({})))
|
||||
.collect()
|
||||
};
|
||||
let keys_only = json!({
|
||||
"type": "object",
|
||||
"properties": any_value(&schema["properties"]),
|
||||
"patternProperties": any_value(&schema["patternProperties"]),
|
||||
"additionalProperties": false,
|
||||
});
|
||||
let registry = serde_json::to_value(registry_json_schema()).unwrap();
|
||||
let sections = registry["properties"]
|
||||
.as_object()
|
||||
.expect("ModelInfo schema has properties");
|
||||
let fields: BTreeSet<&str> = catalog
|
||||
.expect("registry has sections");
|
||||
let fields: Map<String, Value> = catalog
|
||||
.as_object()
|
||||
.expect("catalog is an object")
|
||||
.iter()
|
||||
.filter(|(name, _)| *name != "sample_spec" && *name != "fallback_generalizations")
|
||||
.filter(|(name, _)| !sections.contains_key(*name))
|
||||
.flat_map(|(_, entry)| entry.as_object().expect("model entry is an object").keys())
|
||||
.map(String::as_str)
|
||||
.filter(|name| *name != "aliases")
|
||||
.map(|name| (name.clone(), Value::Null))
|
||||
.collect();
|
||||
let missing: Vec<_> = fields
|
||||
.into_iter()
|
||||
.filter(|name| !properties.contains_key(*name))
|
||||
let missing: Vec<_> = jsonschema::validator_for(&keys_only)
|
||||
.unwrap()
|
||||
.iter_errors(&Value::Object(fields))
|
||||
.map(|error| error.to_string())
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
missing.is_empty(),
|
||||
"{path}: fields missing from schema: {missing:?}"
|
||||
"fields missing from schema: {missing:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("Mode", "chat")]
|
||||
#[case("ReasoningEffort", "high")]
|
||||
#[case("EffortLevel", "high")]
|
||||
#[case("InputModality", "image")]
|
||||
#[ignore]
|
||||
fn generated_schema_includes_enum_values(#[case] definition: &str, #[case] value: &str) {
|
||||
let schema = schema();
|
||||
let variants = schema["$defs"][definition]["enum"]
|
||||
|
|
@ -110,7 +120,6 @@ fn generated_schema_includes_enum_values(#[case] definition: &str, #[case] value
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn generated_schema_includes_nested_pricing_types() {
|
||||
let schema = schema();
|
||||
let definitions = schema["$defs"].as_object().expect("schema has definitions");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue