Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions crates/statsai-pricing/src/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,14 @@ pub(crate) fn pricing_for_effective_speed(
}

let fast = match model_name {
"claude-opus-5-5" => pricing_with_cache_creation(8.0, 10.0, 0.4, 40.0),
"claude-opus-5" | "claude-opus-4-8" => pricing_with_cache_creation(10.0, 12.5, 1.0, 50.0),
"gpt-6-sol" | "gpt-6-luna" => pricing_with_cache_creation(
standard.input_per_million * 2.0,
standard.cache_creation_per_million * 2.0,
standard.cached_input_per_million * 2.0,
standard.output_per_million * 2.0,
),
// Historical fast-mode rates. Effective `usage.speed` is authoritative:
// unsupported requests either failed or reported `standard` after fallback.
"claude-opus-4-6" | "claude-opus-4-7" => {
Expand Down Expand Up @@ -140,6 +147,7 @@ pub(crate) fn pricing_for_model_on(
"claude-fable-5" | "claude-mythos-5" => {
Some(pricing_with_cache_creation(10.0, 12.5, 1.0, 50.0))
}
"claude-opus-5-5" => Some(pricing_with_cache_creation(4.0, 5.0, 0.2, 20.0)),
"claude-opus-5" => Some(pricing_with_cache_creation(5.0, 6.25, 0.5, 25.0)),
"claude-sonnet-5" => {
let date = (usage_date.year(), usage_date.month(), usage_date.day());
Expand All @@ -163,6 +171,8 @@ pub(crate) fn pricing_for_model_on(
// default. A larger prompt reprices the whole request at 20/25/2/75,
// applied by `pricing_multipliers` where a record is one known request.
"gpt-6-astra" => Some(pricing_with_cache_creation(10.0, 12.5, 1.0, 50.0)),
"gpt-6-sol" => Some(pricing_with_cache_creation(2.0, 2.5, 0.2, 10.0)),
"gpt-6-luna" => Some(pricing_with_cache_creation(0.1, 0.125, 0.01, 0.5)),
// GPT-5.6 uses a 1.25x cache-write multiplier and a 90% cache-read discount.
"gpt-5.6-sol" => Some(gpt_5_6_sol_pricing(usage_date)),
"gpt-5.6-terra" => Some(gpt_5_6_terra_pricing(usage_date)),
Expand Down
8 changes: 7 additions & 1 deletion crates/statsai-pricing/src/cost.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,13 @@ fn pricing_multipliers(

let is_openai_long_context_model = matches!(
model_name,
"gpt-5.4" | "gpt-5.6-sol" | "gpt-5.6-terra" | "gpt-5.6-luna" | "gpt-6-astra"
"gpt-5.4"
| "gpt-5.6-sol"
| "gpt-5.6-terra"
| "gpt-5.6-luna"
| "gpt-6-astra"
| "gpt-6-sol"
| "gpt-6-luna"
);
let is_xai_long_context_model = matches!(
model_name,
Expand Down
2 changes: 1 addition & 1 deletion crates/statsai-pricing/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
/// Increment this constant on every semantic pricing-rule change so persisted
/// stores can reprice automatically. The descriptive catalog string is not an
/// ordering key.
pub const PRICING_RULESET_VERSION: u64 = 3;
pub const PRICING_RULESET_VERSION: u64 = 4;

/// Descriptive identifier for the compiled price list. Not an ordering key.
pub const PRICING_CATALOG_VERSION: &str = "official:2026-09-22";
Expand Down
15 changes: 15 additions & 0 deletions crates/statsai-pricing/src/normalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ fn normalize_reversed_claude_model_name(lower: &str) -> Option<&'static str> {
let (version, family) = rest.split_once('-')?;
let family = family.split('-').next()?;
Some(match (family, version) {
("opus", "5.5") => "claude-opus-5-5",
("opus", "5") => "claude-opus-5",
("opus", "4.8") => "claude-opus-4-8",
("opus", "4.7") => "claude-opus-4-7",
Expand Down Expand Up @@ -39,6 +40,9 @@ fn normalize_proxy_wrapped_model_name(lower: &str) -> Option<&'static str> {
if lower.contains("claude-mythos-5") {
return Some("claude-mythos-5");
}
if lower.contains("claude-opus-5-5") || lower.contains("claude-opus-5.5") {
return Some("claude-opus-5-5");
}
if lower.contains("claude-opus-5") {
return Some("claude-opus-5");
}
Expand Down Expand Up @@ -87,6 +91,12 @@ fn normalize_proxy_wrapped_model_name(lower: &str) -> Option<&'static str> {
if lower.contains("gpt-6-astra") {
return Some("gpt-6-astra");
}
if lower.contains("gpt-6-sol") {
return Some("gpt-6-sol");
}
if lower.contains("gpt-6-luna") {
return Some("gpt-6-luna");
}
if lower.contains("gpt-5.6-sol") {
return Some("gpt-5.6-sol");
}
Expand Down Expand Up @@ -225,6 +235,9 @@ pub fn normalize_model_name(name: &str) -> String {
"claude-fable-5-1" | "claude-fable-5.1" => "claude-fable-5-1".to_string(),
"claude-fable-5" => "claude-fable-5".to_string(),
"claude-mythos-5" => "claude-mythos-5".to_string(),
"claude-opus-5-5" | "claude-opus-5-5-thinking" | "claude-opus-5.5" => {
"claude-opus-5-5".to_string()
}
"claude-opus-5" | "claude-opus-5-thinking" => "claude-opus-5".to_string(),
"claude-sonnet-5" | "claude-sonnet-5-thinking" => "claude-sonnet-5".to_string(),
"claude-3-5-sonnet-20241022" | "claude-sonnet-3-5" => "claude-sonnet-3-5".to_string(),
Expand Down Expand Up @@ -261,6 +274,8 @@ pub fn normalize_model_name(name: &str) -> String {
"gpt-5.4" => "gpt-5.4".to_string(),
"gpt-5.4-mini" => "gpt-5.4-mini".to_string(),
"gpt-6-astra" | "gpt-6-astra-codex" => "gpt-6-astra".to_string(),
"gpt-6-sol" => "gpt-6-sol".to_string(),
"gpt-6-luna" => "gpt-6-luna".to_string(),
"gpt-5.6-sol" => "gpt-5.6-sol".to_string(),
"gpt-5.6-terra" => "gpt-5.6-terra".to_string(),
"gpt-5.6-luna" => "gpt-5.6-luna".to_string(),
Expand Down
84 changes: 84 additions & 0 deletions crates/statsai-pricing/src/tests/cost.rs
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,90 @@ fn test_model(name: &str) -> statsai_core::ModelInfo {
}
}

#[test]
fn opus_5_5_prices_both_cache_write_lifetimes_without_long_context_surcharge() {
let model = test_model("claude-opus-5.5");
let usage = UsageCounts {
input_tokens: Some(300_000),
cache_creation_tokens: Some(200_000),
cache_creation_5m_tokens: Some(100_000),
cache_creation_1h_tokens: Some(100_000),
cache_read_tokens: Some(400_000),
output_tokens: Some(10_000),
requests: Some(1),
..UsageCounts::default()
};
let at = parse_utc("2026-09-22T12:00:00Z");
let standard = estimate_cost_at("claude_code", Some(&model), &usage, &at);
let fast = estimate_cost_at(
"claude_code",
Some(&statsai_core::ModelInfo {
speed: Some("fast".to_string()),
..model
}),
&usage,
&at,
);

assert_eq!(standard.estimated_api_equivalent_micro_usd, Some(2_780_000));
assert_eq!(fast.estimated_api_equivalent_micro_usd, Some(5_560_000));
assert_eq!(
fast.pricing_source.as_deref(),
Some("claude_code_api_pricing:claude-opus-5-5:fast")
);
}

#[test]
fn gpt_6_sol_and_luna_reprice_entire_request_above_272k() {
let at = parse_utc("2026-09-22T12:00:00Z");
for (name, short_cost, long_cost, fast_long_cost) in [
("gpt-6-sol", 564_400, 1_079_200, 2_158_400),
("gpt-6-luna", 28_220, 53_960, 107_920),
] {
let model = test_model(name);
let short_usage = UsageCounts {
input_tokens: Some(100_000),
cache_creation_tokens: Some(100_000),
cache_read_tokens: Some(72_000),
output_tokens: Some(10_000),
requests: Some(1),
..UsageCounts::default()
};
let long_usage = UsageCounts {
cache_read_tokens: Some(73_000),
..short_usage.clone()
};
let standard_short = estimate_cost_at("codex", Some(&model), &short_usage, &at);
let standard_long = estimate_cost_at("codex", Some(&model), &long_usage, &at);
let fast_long = estimate_cost_at(
"codex",
Some(&statsai_core::ModelInfo {
speed: Some("fast".to_string()),
..model
}),
&long_usage,
&at,
);

assert_eq!(
standard_short.estimated_api_equivalent_micro_usd,
Some(short_cost)
);
assert_eq!(
standard_long.estimated_api_equivalent_micro_usd,
Some(long_cost)
);
assert_eq!(
fast_long.estimated_api_equivalent_micro_usd,
Some(fast_long_cost)
);
assert_eq!(
fast_long.pricing_source.as_deref(),
Some(format!("codex_api_pricing:{name}:fast").as_str())
);
}
}

fn parse_utc(value: &str) -> chrono::DateTime<chrono::Utc> {
chrono::DateTime::parse_from_rfc3339(value)
.expect("valid timestamp")
Expand Down
16 changes: 16 additions & 0 deletions crates/statsai-pricing/src/tests/normalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,22 @@ fn normalizes_gpt_6_astra() {
);
}

#[test]
fn normalizes_new_opus_and_gpt_6_models_without_collapsing_opus_5_5() {
assert_eq!(normalize_model_name("claude-opus-5-5"), "claude-opus-5-5");
assert_eq!(normalize_model_name("claude-opus-5.5"), "claude-opus-5-5");
assert_eq!(normalize_model_name("claude-5.5-opus"), "claude-opus-5-5");
assert_eq!(
normalize_model_name("openrouter/claude-opus-5-5-thinking"),
"claude-opus-5-5"
);
assert_eq!(normalize_model_name("openai/gpt-6-sol"), "gpt-6-sol");
assert_eq!(
normalize_model_name("relay/openai-gpt-6-luna"),
"gpt-6-luna"
);
}

#[test]
fn normalizes_codex_aliases() {
assert_eq!(normalize_model_name("gpt-5.1-codex"), "gpt-5-codex");
Expand Down
Loading