diff --git a/crates/statsai-pricing/src/catalog.rs b/crates/statsai-pricing/src/catalog.rs index a6afa96..c8a101a 100644 --- a/crates/statsai-pricing/src/catalog.rs +++ b/crates/statsai-pricing/src/catalog.rs @@ -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" => { @@ -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()); @@ -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)), diff --git a/crates/statsai-pricing/src/cost.rs b/crates/statsai-pricing/src/cost.rs index 00f780c..61ef5a3 100644 --- a/crates/statsai-pricing/src/cost.rs +++ b/crates/statsai-pricing/src/cost.rs @@ -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, diff --git a/crates/statsai-pricing/src/lib.rs b/crates/statsai-pricing/src/lib.rs index 48c2d86..7c57406 100644 --- a/crates/statsai-pricing/src/lib.rs +++ b/crates/statsai-pricing/src/lib.rs @@ -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"; diff --git a/crates/statsai-pricing/src/normalize.rs b/crates/statsai-pricing/src/normalize.rs index 916556d..1c5f349 100644 --- a/crates/statsai-pricing/src/normalize.rs +++ b/crates/statsai-pricing/src/normalize.rs @@ -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", @@ -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"); } @@ -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"); } @@ -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(), @@ -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(), diff --git a/crates/statsai-pricing/src/tests/cost.rs b/crates/statsai-pricing/src/tests/cost.rs index 0673b8e..c783fb4 100644 --- a/crates/statsai-pricing/src/tests/cost.rs +++ b/crates/statsai-pricing/src/tests/cost.rs @@ -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::DateTime::parse_from_rfc3339(value) .expect("valid timestamp") diff --git a/crates/statsai-pricing/src/tests/normalize.rs b/crates/statsai-pricing/src/tests/normalize.rs index ba51fc1..65ab410 100644 --- a/crates/statsai-pricing/src/tests/normalize.rs +++ b/crates/statsai-pricing/src/tests/normalize.rs @@ -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");