Skip to content
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
243 changes: 207 additions & 36 deletions src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,25 @@ pub struct TieredCaching {
pub bracket_pricing: bool,
}

/// Cache tier for models with separate, tiered write and read pricing.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CachingTierWithWrites {
/// Maximum tokens for this caching tier (None means unlimited).
pub max_tokens: Option<u64>,
/// Cache write cost per 1M tokens.
pub cache_write_per_1m: f64,
/// Cache read cost per 1M tokens.
pub cache_read_per_1m: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TieredCachingWithWrites {
/// Cache tiers ordered from lowest threshold to highest.
pub tiers: Vec<CachingTierWithWrites>,
/// If true, bill the entire token count at the single matching tier's rate.
pub bracket_pricing: bool,
}

/// Different cache pricing structures.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CachingSupport {
Expand All @@ -73,6 +92,8 @@ pub enum CachingSupport {
},
/// Tiered cached input pricing.
Tiered(TieredCaching),
/// Tiered cache pricing with separate write and read rates.
TieredWithWrites(TieredCachingWithWrites),
}

/// Provider service tier used for pricing.
Expand Down Expand Up @@ -192,6 +213,10 @@ impl Registry {
CachingSupport::Tiered(tiered) => {
Self::validate_tier_bounds(&tiered.tiers, |tier| tier.max_tokens)
}
CachingSupport::TieredWithWrites(tiered) => {
tiered.bracket_pricing
&& Self::validate_tier_bounds(&tiered.tiers, |tier| tier.max_tokens)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
_ => true,
};

Expand Down Expand Up @@ -349,6 +374,56 @@ fn populate_defaults(
};
}

macro_rules! add_tiered_service_tier_pricing_with_cache_writes {
(
$name:expr,
$service_tier:expr,
$short_input:expr,
$short_cache_write:expr,
$short_cache_read:expr,
$short_output:expr,
$long_input:expr,
$long_cache_write:expr,
$long_cache_read:expr,
$long_output:expr
) => {
add_service_tier_pricing!(
$name,
$service_tier,
PricingStructure::Tiered(TieredPricing {
tiers: vec![
PricingTier {
max_tokens: Some(272_000),
input_per_1m: $short_input,
output_per_1m: $short_output,
},
PricingTier {
max_tokens: None,
input_per_1m: $long_input,
output_per_1m: $long_output,
},
],
bracket_pricing: true,
}),
CachingSupport::TieredWithWrites(TieredCachingWithWrites {
tiers: vec![
CachingTierWithWrites {
max_tokens: Some(272_000),
cache_write_per_1m: $short_cache_write,
cache_read_per_1m: $short_cache_read,
},
CachingTierWithWrites {
max_tokens: None,
cache_write_per_1m: $long_cache_write,
cache_read_per_1m: $long_cache_read,
},
],
bracket_pricing: true,
})
);
};
}

macro_rules! add_tiered_service_tier_pricing {
(
$name:expr,
Expand Down Expand Up @@ -843,14 +918,36 @@ fn populate_defaults(

add_model!(
"gpt-5.6-sol",
PricingStructure::Flat {
input_per_1m: 5.0,
output_per_1m: 30.0
},
CachingSupport::OpenAIWithWrites {
cache_write_per_1m: 6.25,
cache_read_per_1m: 0.50
},
PricingStructure::Tiered(TieredPricing {
tiers: vec![
PricingTier {
max_tokens: Some(272_000),
input_per_1m: 5.0,
output_per_1m: 30.0
},
PricingTier {
max_tokens: None,
input_per_1m: 10.0,
output_per_1m: 45.0
},
],
bracket_pricing: true,
}),
CachingSupport::TieredWithWrites(TieredCachingWithWrites {
tiers: vec![
CachingTierWithWrites {
max_tokens: Some(272_000),
cache_write_per_1m: 6.25,
cache_read_per_1m: 0.50
},
CachingTierWithWrites {
max_tokens: None,
cache_write_per_1m: 12.50,
cache_read_per_1m: 1.0
},
],
bracket_pricing: true,
}),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
false
);
add_model!(
Expand Down Expand Up @@ -937,13 +1034,17 @@ fn populate_defaults(
false
);

add_flat_service_tier_pricing_with_cache_writes!(
add_tiered_service_tier_pricing_with_cache_writes!(
"gpt-5.6-sol",
ServiceTier::Priority,
10.0,
12.50,
1.0,
60.0
60.0,
20.0,
25.0,
2.0,
90.0
);
add_flat_service_tier_pricing_with_cache_writes!(
"gpt-5.6-terra",
Expand All @@ -966,13 +1067,17 @@ fn populate_defaults(
add_flat_service_tier_pricing!("gpt-5.4-mini", ServiceTier::Priority, 1.50, 0.15, 9.0);

for service_tier in [ServiceTier::Flex, ServiceTier::Batch] {
add_flat_service_tier_pricing_with_cache_writes!(
add_tiered_service_tier_pricing_with_cache_writes!(
"gpt-5.6-sol",
service_tier,
2.50,
3.125,
0.25,
15.0
15.0,
5.0,
6.25,
0.50,
22.50
);
add_flat_service_tier_pricing_with_cache_writes!(
"gpt-5.6-terra",
Expand Down Expand Up @@ -2412,6 +2517,12 @@ fn cache_cost_for_caching(
// cache creation tokens are intentionally not charged here.
calculate_tiered_cache_cost(cache_read_tokens, &tiered.tiers, tiered.bracket_pricing)
}
CachingSupport::TieredWithWrites(tiered) => calculate_tiered_cache_cost_with_writes(
cache_creation_tokens,
cache_read_tokens,
&tiered.tiers,
tiered.bracket_pricing,
),
}
}

Expand Down Expand Up @@ -2523,9 +2634,17 @@ pub fn calculate_total_cost_for_service_tier_at(
Some(model_info) => {
let (pricing, caching) =
pricing_for_service_tier(&model_info, service_tier, effective_at);
input_cost_for_pricing(pricing, input_tokens)
+ output_cost_for_pricing(pricing, output_tokens)
+ cache_cost_for_caching(caching, cache_creation_tokens, cache_read_tokens)
let context_tokens =
input_tokens.saturating_add(cache_creation_tokens.max(cache_read_tokens));
calculate_context_cost(
pricing,
caching,
input_tokens,
output_tokens,
cache_creation_tokens,
cache_read_tokens,
context_tokens,
)
}
None => {
warn_once(format!(
Expand Down Expand Up @@ -2647,6 +2766,23 @@ fn calculate_tiered_cache_cost(tokens: u64, tiers: &[CachingTier], bracket_prici
total_cost
}

fn calculate_tiered_cache_cost_with_writes(
cache_creation_tokens: u64,
cache_read_tokens: u64,
tiers: &[CachingTierWithWrites],
bracket_pricing: bool,
) -> f64 {
debug_assert!(bracket_pricing);
let context_tokens = cache_creation_tokens.max(cache_read_tokens);

find_tier(context_tokens, tiers, |tier| tier.max_tokens)
.map(|tier| {
(cache_creation_tokens as f64 / 1_000_000.0) * tier.cache_write_per_1m
+ (cache_read_tokens as f64 / 1_000_000.0) * tier.cache_read_per_1m
})
.unwrap_or(0.0)
}

fn find_tier<T, F>(tokens: u64, tiers: &[T], max_tokens: F) -> Option<&T>
where
F: Fn(&T) -> Option<u64>,
Expand Down Expand Up @@ -2697,6 +2833,14 @@ fn calculate_context_cost(
.map(|tier| (cache_read_tokens as f64 / 1_000_000.0) * tier.cached_input_per_1m)
.unwrap_or(0.0)
}
CachingSupport::TieredWithWrites(tiered) => {
find_tier(context_tokens, &tiered.tiers, |tier| tier.max_tokens)
.map(|tier| {
(cache_creation_tokens as f64 / 1_000_000.0) * tier.cache_write_per_1m
+ (cache_read_tokens as f64 / 1_000_000.0) * tier.cache_read_per_1m
})
.unwrap_or(0.0)
}
_ => cache_cost_for_caching(caching, cache_creation_tokens, cache_read_tokens),
};

Expand Down Expand Up @@ -3090,6 +3234,39 @@ mod tests {
approx_eq(cache_cost, 1.25);
}

#[test]
fn gpt_5_6_sol_uses_long_context_pricing_for_full_request() {
let cost = calculate_total_cost_for_service_tier_at(
"gpt-5.6-sol",
ServiceTier::Standard,
100_000,
10_000,
0,
200_000,
None,
);

// The 300K prompt crosses the 272K boundary even though uncached input,
// cached input, and output are each below it individually.
approx_eq(cost, 1.65);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

#[test]
fn gpt_5_6_sol_context_boundary_selects_one_rate_for_every_token_category() {
for (input, expected) in [(172_000, 1.21), (172_001, 2.270_01)] {
let cost = calculate_total_cost_for_service_tier_at(
"gpt-5.6-sol",
ServiceTier::Standard,
input,
10_000,
0,
100_000,
None,
);
approx_eq(cost, expected);
}
}

#[test]
fn gpt_5_6_pricing_is_available() {
let sol_info = get_model_info("gpt-5.6-sol").expect("model should exist");
Expand All @@ -3099,13 +3276,10 @@ mod tests {
assert!(!terra_info.is_estimated);
assert!(!luna_info.is_estimated);

approx_eq(calculate_input_cost("gpt-5.6-sol", 1_000_000), 5.0);
approx_eq(calculate_output_cost("gpt-5.6-sol", 1_000_000), 30.0);
approx_eq(calculate_cache_cost("gpt-5.6-sol", 0, 1_000_000), 0.50);
approx_eq(
calculate_cache_cost("gpt-5.6-sol", 1_000_000, 1_000_000),
6.75,
);
approx_eq(calculate_input_cost("gpt-5.6-sol", 200_000), 1.0);
approx_eq(calculate_output_cost("gpt-5.6-sol", 200_000), 6.0);
approx_eq(calculate_cache_cost("gpt-5.6-sol", 0, 200_000), 0.10);
approx_eq(calculate_cache_cost("gpt-5.6-sol", 100_000, 100_000), 0.675);

approx_eq(calculate_input_cost("gpt-5.6-terra", 1_000_000), 2.0);
approx_eq(calculate_output_cost("gpt-5.6-terra", 1_000_000), 12.0);
Expand Down Expand Up @@ -3213,23 +3387,20 @@ mod tests {
let model_info = get_model_info("gpt-5.6-sol-ultra").expect("alias should resolve");
assert!(!model_info.is_estimated);

approx_eq(calculate_input_cost("gpt-5.6", 1_000_000), 5.0);
approx_eq(calculate_output_cost("gpt-5.6", 1_000_000), 30.0);
approx_eq(
calculate_cache_cost("gpt-5.6-sol-ultra", 0, 1_000_000),
0.50,
);
approx_eq(calculate_input_cost("gpt-5.6", 1_000_000), 10.0);
approx_eq(calculate_output_cost("gpt-5.6", 1_000_000), 45.0);
approx_eq(calculate_cache_cost("gpt-5.6-sol-ultra", 0, 1_000_000), 1.0);
}

#[test]
fn gpt_priority_pricing_is_available_for_supported_models() {
approx_eq(
calculate_input_cost_for_service_tier("gpt-5.6-sol", ServiceTier::Priority, 1_000_000),
10.0,
20.0,
);
approx_eq(
calculate_output_cost_for_service_tier("gpt-5.6-sol", ServiceTier::Priority, 1_000_000),
60.0,
90.0,
);
approx_eq(
calculate_cache_cost_for_service_tier(
Expand All @@ -3238,7 +3409,7 @@ mod tests {
0,
1_000_000,
),
1.0,
2.0,
);
approx_eq(
calculate_cache_cost_for_service_tier(
Expand All @@ -3247,7 +3418,7 @@ mod tests {
1_000_000,
1_000_000,
),
13.50,
27.0,
);

approx_eq(
Expand Down Expand Up @@ -3370,15 +3541,15 @@ mod tests {
for service_tier in [ServiceTier::Flex, ServiceTier::Batch] {
approx_eq(
calculate_input_cost_for_service_tier("gpt-5.6-sol", service_tier, 1_000_000),
2.50,
5.0,
);
approx_eq(
calculate_output_cost_for_service_tier("gpt-5.6-sol", service_tier, 1_000_000),
15.0,
22.50,
);
approx_eq(
calculate_cache_cost_for_service_tier("gpt-5.6-sol", service_tier, 0, 1_000_000),
0.25,
0.50,
);
approx_eq(
calculate_cache_cost_for_service_tier(
Expand All @@ -3387,7 +3558,7 @@ mod tests {
1_000_000,
1_000_000,
),
3.375,
6.75,
);

approx_eq(
Expand Down
Loading