diff --git a/src/models.rs b/src/models.rs index 7bd37dd..7e27267 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1815,6 +1815,7 @@ fn populate_defaults( ); // Google Models + // Source: https://ai.google.dev/gemini-api/docs/pricing add_model!( "gemini-3-flash-preview", PricingStructure::Flat { @@ -1903,11 +1904,11 @@ fn populate_defaults( tiers: vec![ CachingTier { max_tokens: Some(200_000), - cached_input_per_1m: 0.31 + cached_input_per_1m: 0.125 }, CachingTier { max_tokens: None, - cached_input_per_1m: 0.625 + cached_input_per_1m: 0.25 }, ], bracket_pricing: false, @@ -1923,7 +1924,7 @@ fn populate_defaults( CachingSupport::Tiered(TieredCaching { tiers: vec![CachingTier { max_tokens: None, - cached_input_per_1m: 0.075 + cached_input_per_1m: 0.03 }], bracket_pricing: false, }), @@ -1938,7 +1939,7 @@ fn populate_defaults( CachingSupport::Tiered(TieredCaching { tiers: vec![CachingTier { max_tokens: None, - cached_input_per_1m: 0.025 + cached_input_per_1m: 0.01 }], bracket_pricing: false, }), @@ -2128,6 +2129,134 @@ fn populate_defaults( // xAI Models // Source: https://docs.x.ai/developers/pricing + add_model!( + "grok-4.3", + PricingStructure::Tiered(TieredPricing { + tiers: vec![ + PricingTier { + max_tokens: Some(200_000), + input_per_1m: 1.25, + output_per_1m: 2.50, + }, + PricingTier { + max_tokens: None, + input_per_1m: 2.50, + output_per_1m: 5.00, + }, + ], + bracket_pricing: true, + }), + CachingSupport::Tiered(TieredCaching { + tiers: vec![ + CachingTier { + max_tokens: Some(200_000), + cached_input_per_1m: 0.20, + }, + CachingTier { + max_tokens: None, + cached_input_per_1m: 0.40, + }, + ], + bracket_pricing: true, + }), + false + ); + add_model!( + "grok-4.20-0309-reasoning", + PricingStructure::Tiered(TieredPricing { + tiers: vec![ + PricingTier { + max_tokens: Some(200_000), + input_per_1m: 1.25, + output_per_1m: 2.50, + }, + PricingTier { + max_tokens: None, + input_per_1m: 2.50, + output_per_1m: 5.00, + }, + ], + bracket_pricing: true, + }), + CachingSupport::Tiered(TieredCaching { + tiers: vec![ + CachingTier { + max_tokens: Some(200_000), + cached_input_per_1m: 0.20, + }, + CachingTier { + max_tokens: None, + cached_input_per_1m: 0.40, + }, + ], + bracket_pricing: true, + }), + false + ); + add_model!( + "grok-4.20-0309-non-reasoning", + PricingStructure::Tiered(TieredPricing { + tiers: vec![ + PricingTier { + max_tokens: Some(200_000), + input_per_1m: 1.25, + output_per_1m: 2.50, + }, + PricingTier { + max_tokens: None, + input_per_1m: 2.50, + output_per_1m: 5.00, + }, + ], + bracket_pricing: true, + }), + CachingSupport::Tiered(TieredCaching { + tiers: vec![ + CachingTier { + max_tokens: Some(200_000), + cached_input_per_1m: 0.20, + }, + CachingTier { + max_tokens: None, + cached_input_per_1m: 0.40, + }, + ], + bracket_pricing: true, + }), + false + ); + add_model!( + "grok-4.20-multi-agent-0309", + PricingStructure::Tiered(TieredPricing { + tiers: vec![ + PricingTier { + max_tokens: Some(200_000), + input_per_1m: 1.25, + output_per_1m: 2.50, + }, + PricingTier { + max_tokens: None, + input_per_1m: 2.50, + output_per_1m: 5.00, + }, + ], + bracket_pricing: true, + }), + CachingSupport::Tiered(TieredCaching { + tiers: vec![ + CachingTier { + max_tokens: Some(200_000), + cached_input_per_1m: 0.20, + }, + CachingTier { + max_tokens: None, + cached_input_per_1m: 0.40, + }, + ], + bracket_pricing: true, + }), + false + ); add_model!( "grok-4.5", PricingStructure::Tiered(TieredPricing { @@ -2160,6 +2289,38 @@ fn populate_defaults( }), false ); + add_model!( + "grok-4.6", + PricingStructure::Tiered(TieredPricing { + tiers: vec![ + PricingTier { + max_tokens: Some(200_000), + input_per_1m: 2.00, + output_per_1m: 6.00, + }, + PricingTier { + max_tokens: None, + input_per_1m: 4.00, + output_per_1m: 12.00, + }, + ], + bracket_pricing: true, + }), + CachingSupport::Tiered(TieredCaching { + tiers: vec![ + CachingTier { + max_tokens: Some(200_000), + cached_input_per_1m: 0.50, + }, + CachingTier { + max_tokens: None, + cached_input_per_1m: 1.00, + }, + ], + bracket_pricing: true, + }), + false + ); add_model!( "grok-build-0.1", PricingStructure::Tiered(TieredPricing { @@ -2227,29 +2388,33 @@ fn populate_defaults( ); // DeepSeek Models - // Source: https://api-docs.deepseek.com/quick_start/pricing/ (official pricing page; - // use the standard rates rather than the temporary promotional discount: - // cache hit $0.0145/M, cache miss $1.74/M, output $3.48/M) + // Source: https://api-docs.deepseek.com/quick_start/pricing/ + // The page publishes peak and off-peak rates, where off-peak is half of + // peak. Splitrail prices usage without a time-of-day dimension, so the + // peak (standard) rates are used. add_model!( "deepseek-v4-pro", PricingStructure::Flat { - input_per_1m: 1.74, - output_per_1m: 3.48 + input_per_1m: 1.32, + output_per_1m: 3.96 }, CachingSupport::OpenAI { - cached_input_per_1m: 0.0145 + cached_input_per_1m: 0.044 }, false ); - // Source: https://api-docs.deepseek.com/quick_start/pricing/ + // DeepSeek now asks callers to use `deepseek-flash`; the legacy + // `deepseek-v4-flash` name is still accepted and billed at the same rate. + // From September 14, 2026 requests to `deepseek-v4-pro` are routed to + // DeepSeek-V4.1-Flash and billed at this price. add_model!( "deepseek-v4-flash", PricingStructure::Flat { - input_per_1m: 0.14, - output_per_1m: 0.28 + input_per_1m: 0.30, + output_per_1m: 1.20 }, CachingSupport::OpenAI { - cached_input_per_1m: 0.0028 + cached_input_per_1m: 0.006 }, false ); @@ -2338,14 +2503,17 @@ fn populate_defaults( ); // MiniMax Models - // Source: https://platform.minimax.io/docs/api-reference/anthropic-api-compatible-cache + // Source: https://platform.minimax.io/docs/guides/pricing-paygo add_model!( "minimax-m2.1", PricingStructure::Flat { input_per_1m: 0.30, output_per_1m: 1.20 }, - CachingSupport::None, + CachingSupport::Anthropic { + cache_write_per_1m: 0.375, + cache_read_per_1m: 0.03 + }, false ); add_model!( @@ -2364,21 +2532,46 @@ fn populate_defaults( "minimax-m2.5", PricingStructure::Flat { input_per_1m: 0.30, - output_per_1m: 1.10 + output_per_1m: 1.20 + }, + CachingSupport::Anthropic { + cache_write_per_1m: 0.375, + cache_read_per_1m: 0.03 }, - CachingSupport::None, false ); - // Source: https://platform.minimax.io/docs/api-reference/anthropic-api-compatible-cache + // MiniMax-M3 is bracketed on input size: at or below 512k tokens the + // permanent 50% discount applies, above it the undiscounted rates apply. add_model!( "minimax-m3", - PricingStructure::Flat { - input_per_1m: 0.60, - output_per_1m: 2.40 - }, - CachingSupport::OpenAI { - cached_input_per_1m: 0.12 - }, + PricingStructure::Tiered(TieredPricing { + tiers: vec![ + PricingTier { + max_tokens: Some(512_000), + input_per_1m: 0.30, + output_per_1m: 1.20, + }, + PricingTier { + max_tokens: None, + input_per_1m: 0.60, + output_per_1m: 2.40, + }, + ], + bracket_pricing: true, + }), + CachingSupport::Tiered(TieredCaching { + tiers: vec![ + CachingTier { + max_tokens: Some(512_000), + cached_input_per_1m: 0.06, + }, + CachingTier { + max_tokens: None, + cached_input_per_1m: 0.12, + }, + ], + bracket_pricing: true, + }), false ); @@ -2772,6 +2965,9 @@ fn populate_defaults( add_alias!("minimax-m2.7", "minimax-m2.7"); add_alias!("minimax-m3", "minimax-m3"); + // DeepSeek aliases + add_alias!("deepseek-flash", "deepseek-v4-flash"); + // Moonshot / ByteDance / Qwen / Xiaomi / Meituan aliases add_alias!("doubao-seed-code", "doubao-seed-2.0-code"); add_alias!("kimi-k2.6", "kimi-k2.6"); @@ -3840,6 +4036,35 @@ mod tests { approx_eq(cost, 0.375); } + #[test] + fn gemini_2_5_cache_reads_match_published_rates() { + // ai.google.dev lists context caching at 10% of input: $0.125/$0.25 for + // Pro, $0.03 for Flash, and $0.01 for Flash-Lite. + approx_eq(calculate_cache_cost("gemini-2.5-pro", 0, 100_000), 0.0125); + // Pro applies the tiers progressively rather than bracketing the whole + // request: 200k at $0.125 plus 50k at $0.25. + approx_eq(calculate_cache_cost("gemini-2.5-pro", 0, 250_000), 0.0375); + approx_eq(calculate_cache_cost("gemini-2.5-flash", 0, 1_000_000), 0.03); + approx_eq( + calculate_cache_cost("gemini-2.5-flash-lite", 0, 1_000_000), + 0.01, + ); + } + + #[test] + fn minimax_m2_5_pricing_matches_published_rates() { + let model_info = get_model_info("minimax-m2.5").expect("model should exist"); + assert!(!model_info.is_estimated); + + approx_eq(calculate_input_cost("minimax-m2.5", 1_000_000), 0.30); + approx_eq(calculate_output_cost("minimax-m2.5", 1_000_000), 1.20); + // Pay-as-you-go lists cache reads at $0.03 and cache writes at $0.375 + // for the legacy M2 era, matching the M2.7 entry. + approx_eq(calculate_cache_cost("minimax-m2.5", 0, 1_000_000), 0.03); + approx_eq(calculate_cache_cost("minimax-m2.5", 1_000_000, 0), 0.375); + approx_eq(calculate_cache_cost("minimax-m2.1", 0, 1_000_000), 0.03); + } + #[test] fn gpt_6_astra_aliases_map_to_official_standard_pricing() { assert!(get_model_info("gpt-6-astra-2026-09-03").is_none()); @@ -4517,13 +4742,54 @@ mod tests { .expect("Grok 4.5 should exist") .is_estimated ); + assert!( + !get_model_info("grok-4.6") + .expect("Grok 4.6 should exist") + .is_estimated + ); + for model in [ + "grok-4.3", + "grok-4.20-0309-reasoning", + "grok-4.20-0309-non-reasoning", + "grok-4.20-multi-agent-0309", + ] { + assert!( + !get_model_info(model) + .unwrap_or_else(|| panic!("{model} should exist")) + .is_estimated + ); + } + approx_eq( + calculate_total_cost_for_context_at( + "grok-4.3", 1_000_000, 1_000_000, 0, 1_000_000, 199_999, None, + ), + 3.95, + ); + approx_eq( + calculate_total_cost_for_context_at( + "grok-4.3", 1_000_000, 1_000_000, 0, 1_000_000, 200_001, None, + ), + 7.9, + ); approx_eq( calculate_total_cost_for_context_at( "grok-4.5", 1_000_000, 1_000_000, 0, 1_000_000, 199_999, None, ), 8.3, ); + approx_eq( + calculate_total_cost_for_context_at( + "grok-4.6", 1_000_000, 1_000_000, 0, 1_000_000, 199_999, None, + ), + 8.5, + ); + approx_eq( + calculate_total_cost_for_context_at( + "grok-4.6", 1_000_000, 1_000_000, 0, 1_000_000, 200_001, None, + ), + 17.0, + ); approx_eq( calculate_total_cost_for_context_at( "grok-code-fast-1", @@ -4544,6 +4810,14 @@ mod tests { "grok-4.5", 1_000_000, 1_000_000, 0, 1_000_000, 2_000_000, None, ), ); + approx_eq( + calculate_total_cost_for_context_at( + "grok-4.6", 1_000_000, 1_000_000, 999_999, 1_000_000, 2_000_000, None, + ), + calculate_total_cost_for_context_at( + "grok-4.6", 1_000_000, 1_000_000, 0, 1_000_000, 2_000_000, None, + ), + ); } #[test] @@ -4609,9 +4883,20 @@ mod tests { let output_cost = calculate_output_cost("deepseek-v4-pro", 1_000_000); let cache_cost = calculate_cache_cost("deepseek-v4-pro", 0, 1_000_000); - approx_eq(input_cost, 1.74); - approx_eq(output_cost, 3.48); - approx_eq(cache_cost, 0.0145); + approx_eq(input_cost, 1.32); + approx_eq(output_cost, 3.96); + approx_eq(cache_cost, 0.044); + } + + #[test] + fn deepseek_flash_alias_matches_legacy_v4_flash_pricing() { + // The page asks callers to use `deepseek-flash`; `deepseek-v4-flash` + // is the legacy name for the same DeepSeek-V4.1-Flash model. + for model in ["deepseek-flash", "deepseek-v4-flash"] { + approx_eq(calculate_input_cost(model, 1_000_000), 0.30); + approx_eq(calculate_output_cost(model, 1_000_000), 1.20); + approx_eq(calculate_cache_cost(model, 0, 1_000_000), 0.006); + } } #[test] @@ -4696,6 +4981,10 @@ mod tests { approx_eq(calculate_input_cost("minimax-m3", 1_000_000), 0.60); approx_eq(calculate_output_cost("minimax-m3", 1_000_000), 2.40); approx_eq(calculate_cache_cost("minimax-m3", 0, 1_000_000), 0.12); + // At or below 512k input tokens the permanent 50% discount applies. + approx_eq(calculate_input_cost("minimax-m3", 512_000), 0.30 * 0.512); + approx_eq(calculate_output_cost("minimax-m3", 512_000), 1.20 * 0.512); + approx_eq(calculate_cache_cost("minimax-m3", 0, 512_000), 0.06 * 0.512); approx_eq(calculate_input_cost("glm-5.1", 1_000_000), 1.40); approx_eq(calculate_output_cost("glm-5.1", 1_000_000), 4.40); @@ -4715,11 +5004,11 @@ mod tests { approx_eq(calculate_input_cost("deepseek.v3.2", 1_000_000), 0.62); approx_eq(calculate_output_cost("deepseek.v3.2", 1_000_000), 1.85); - approx_eq(calculate_input_cost("deepseek-v4-flash", 1_000_000), 0.14); - approx_eq(calculate_output_cost("deepseek-v4-flash", 1_000_000), 0.28); + approx_eq(calculate_input_cost("deepseek-v4-flash", 1_000_000), 0.30); + approx_eq(calculate_output_cost("deepseek-v4-flash", 1_000_000), 1.20); approx_eq( calculate_cache_cost("deepseek-v4-flash", 0, 1_000_000), - 0.0028, + 0.006, ); approx_eq(