From 5ab13a3ce1fedf4afcb48f33716cbc727b82aa46 Mon Sep 17 00:00:00 2001 From: Dmitry Starkov <21260939+starkdmi@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:22:53 +0300 Subject: [PATCH 01/20] Add Grok 4.7 pricing ruleset version --- crates/statsai-pricing/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/statsai-pricing/src/lib.rs b/crates/statsai-pricing/src/lib.rs index 4b3c50e..48c2d86 100644 --- a/crates/statsai-pricing/src/lib.rs +++ b/crates/statsai-pricing/src/lib.rs @@ -14,10 +14,10 @@ /// 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 = 2; +pub const PRICING_RULESET_VERSION: u64 = 3; /// Descriptive identifier for the compiled price list. Not an ordering key. -pub const PRICING_CATALOG_VERSION: &str = "official:2026-09-05"; +pub const PRICING_CATALOG_VERSION: &str = "official:2026-09-22"; mod catalog; mod cost; From 81184bbb663cebc90f53f49ab6f36435b0346f3c Mon Sep 17 00:00:00 2001 From: Dmitry Starkov <21260939+starkdmi@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:23:05 +0300 Subject: [PATCH 02/20] Normalize Grok 4.7 model IDs --- crates/statsai-pricing/src/normalize.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/statsai-pricing/src/normalize.rs b/crates/statsai-pricing/src/normalize.rs index 8b00e60..916556d 100644 --- a/crates/statsai-pricing/src/normalize.rs +++ b/crates/statsai-pricing/src/normalize.rs @@ -144,6 +144,9 @@ fn normalize_proxy_wrapped_model_name(lower: &str) -> Option<&'static str> { if lower.contains("grok-composer-2.5") || lower.contains("composer-2.5") { return Some("composer-2.5"); } + if lower.contains("grok-4.7") { + return Some("grok-4.7"); + } if lower.contains("grok-4.6") { return Some("grok-4.6"); } @@ -267,6 +270,7 @@ pub fn normalize_model_name(name: &str) -> String { "composer-2.5" | "grok-composer-2.5" => "composer-2.5".to_string(), "composer-2.5-fast" | "grok-composer-2.5-fast" => "composer-2.5-fast".to_string(), "grok-build" | "grok-build-0.1" => "grok-build-0.1".to_string(), + "grok-4.7" | "grok-4.7-build-fast" => "grok-4.7".to_string(), "grok-4.6" | "grok-4.6-build" | "grok-4.6-latest" => "grok-4.6".to_string(), "grok-4.5" | "grok-4.5-latest" | "grok-build-latest" => "grok-4.5".to_string(), "grok-4.3" | "grok-4.3-latest" => "grok-4.3".to_string(), From 4d423648ee3ae03e1bdb65a1eb3eaeb358aef10e Mon Sep 17 00:00:00 2001 From: Dmitry Starkov <21260939+starkdmi@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:23:09 +0300 Subject: [PATCH 03/20] Add Grok 4.7 catalog pricing --- crates/statsai-pricing/src/catalog.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/statsai-pricing/src/catalog.rs b/crates/statsai-pricing/src/catalog.rs index f2c5ca1..a6afa96 100644 --- a/crates/statsai-pricing/src/catalog.rs +++ b/crates/statsai-pricing/src/catalog.rs @@ -62,8 +62,8 @@ pub(crate) fn pricing_for_effective_speed( "claude-opus-4-6" | "claude-opus-4-7" => { pricing_with_cache_creation(30.0, 37.5, 3.0, 150.0) } - // Cursor's fast tier for Grok 4.6 is a flat 2x on input, cached input, and output. - "grok-4.6" => pricing(4.0, 1.0, 12.0), + // Grok 4.6/4.7 Fast is 2x the standard short-context token rates. + "grok-4.6" | "grok-4.7" => pricing(4.0, 1.0, 12.0), // Grok 4.5 fast doubles input and cached input but triples output. // Applied as multipliers against the standard rates above rather than as // absolutes: xAI documents 4.5 cached input at $0.30/M, not 4.6's $0.50/M @@ -190,9 +190,9 @@ pub(crate) fn pricing_for_model_on( | "grok-4.20-0309-reasoning" | "grok-4.20-0309-non-reasoning" => Some(pricing(1.25, 0.2, 2.5)), "grok-4.5" => Some(pricing(2.0, 0.3, 6.0)), - // Official Grok 4.6 cached-input rate is $0.50/M below 200k prompt tokens - // ($1.00/M at or above 200k): https://docs.x.ai/developers/models/grok-4.6 - "grok-4.6" => Some(pricing(2.0, 0.5, 6.0)), + // Official Grok 4.6/4.7 short-context rates are $2/M input, + // $0.50/M cached input, and $6/M output. + "grok-4.6" | "grok-4.7" => Some(pricing(2.0, 0.5, 6.0)), _ => None, } } From 0d87c68fcd78079746de2a728ffb732a02b78990 Mon Sep 17 00:00:00 2001 From: Dmitry Starkov <21260939+starkdmi@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:23:27 +0300 Subject: [PATCH 04/20] Price Grok 4.7 fast and provider long context --- crates/statsai-pricing/src/cost.rs | 38 +++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/crates/statsai-pricing/src/cost.rs b/crates/statsai-pricing/src/cost.rs index fa20a38..a9e8d50 100644 --- a/crates/statsai-pricing/src/cost.rs +++ b/crates/statsai-pricing/src/cost.rs @@ -37,10 +37,18 @@ pub fn estimate_cost_at( let Some(standard_pricing) = pricing_for_model_on(&model_name, usage_date) else { return unknown_cost(); }; + let effective_speed = model.speed.as_deref().or_else(|| { + [model.name.as_deref(), model.provider_model_id.as_deref()] + .into_iter() + .flatten() + .any(|name| name.trim().to_ascii_lowercase().ends_with("-fast")) + .then_some("fast") + }); let (pricing, uses_fast_mode_pricing) = - pricing_for_effective_speed(&model_name, model.speed.as_deref(), standard_pricing); + pricing_for_effective_speed(&model_name, effective_speed, standard_pricing); - let (input_multiplier, output_multiplier) = pricing_multipliers(&model_name, usage); + let (input_multiplier, output_multiplier) = + pricing_multipliers(provider, &model_name, usage, uses_fast_mode_pricing); let mut numerator = component_cost_numerator( i128::from(usage.input_tokens.unwrap_or(0)), pricing.input_per_million, @@ -74,6 +82,7 @@ pub fn estimate_cost_at( | "grok-4.3" | "grok-4.5" | "grok-4.6" + | "grok-4.7" | "grok-4.20-multi-agent-0309" | "grok-4.20-0309-reasoning" | "grok-4.20-0309-non-reasoning" => { @@ -146,15 +155,37 @@ fn cache_creation_cost_numerator( )) } -fn pricing_multipliers(model_name: &str, usage: &UsageCounts) -> (i128, i128) { +fn pricing_multipliers( + provider: &str, + model_name: &str, + usage: &UsageCounts, + uses_fast_mode_pricing: bool, +) -> (i128, i128) { const OPENAI_LONG_CONTEXT_THRESHOLD: u64 = 272_000; const XAI_LONG_CONTEXT_THRESHOLD: u64 = 200_000; + const CURSOR_GROK_4_7_LONG_CONTEXT_THRESHOLD: u64 = 256_000; let prompt_tokens = usage .input_tokens .unwrap_or(0) .saturating_add(usage.cache_creation_tokens.unwrap_or(0)) .saturating_add(usage.cache_read_tokens.unwrap_or(0)); + + if model_name == "grok-4.7" + && provider.eq_ignore_ascii_case("cursor") + && usage.requests == Some(1) + && prompt_tokens > CURSOR_GROK_4_7_LONG_CONTEXT_THRESHOLD + { + // Cursor publishes 2x standard rates above 256k, while Fast is 3x the + // short-context standard rate. Fast base pricing is already 2x here, + // so its long-context multiplier is 1.5x rather than 2x. + return if uses_fast_mode_pricing { + (15_000, 15_000) + } else { + (20_000, 20_000) + }; + } + 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" @@ -165,6 +196,7 @@ fn pricing_multipliers(model_name: &str, usage: &UsageCounts) -> (i128, i128) { | "grok-4.3" | "grok-4.5" | "grok-4.6" + | "grok-4.7" | "grok-4.20-multi-agent-0309" | "grok-4.20-0309-reasoning" | "grok-4.20-0309-non-reasoning" From 9e26f13db649e5aecec118eafb4679a1846ef7b0 Mon Sep 17 00:00:00 2001 From: Dmitry Starkov <21260939+starkdmi@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:24:09 +0300 Subject: [PATCH 05/20] Test Grok 4.7 model normalization --- crates/statsai-pricing/src/tests/normalize.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/statsai-pricing/src/tests/normalize.rs b/crates/statsai-pricing/src/tests/normalize.rs index f0af5cd..1b1d3fa 100644 --- a/crates/statsai-pricing/src/tests/normalize.rs +++ b/crates/statsai-pricing/src/tests/normalize.rs @@ -142,6 +142,19 @@ fn normalizes_grok_build_aliases() { "grok-4.6" ); assert_eq!(normalize_model_name("relay/xai-grok-4.6"), "grok-4.6"); + assert_eq!(normalize_model_name("grok-4.7"), "grok-4.7"); + assert_eq!( + normalize_model_name("grok-4.7-build-fast"), + "grok-4.7" + ); + assert_eq!( + normalize_model_name("cursor-grok-4.7-xhigh"), + "grok-4.7" + ); + assert_eq!( + normalize_model_name("cursor-grok-4.7-high-fast"), + "grok-4.7" + ); } #[test] From d631597ea388873cd9fdfcf4f6a8eb649f27697b Mon Sep 17 00:00:00 2001 From: Dmitry Starkov <21260939+starkdmi@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:24:12 +0300 Subject: [PATCH 06/20] Test Grok 4.7 pricing tiers --- crates/statsai-pricing/src/tests/cost.rs | 110 +++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/crates/statsai-pricing/src/tests/cost.rs b/crates/statsai-pricing/src/tests/cost.rs index 88962fa..6ddce1f 100644 --- a/crates/statsai-pricing/src/tests/cost.rs +++ b/crates/statsai-pricing/src/tests/cost.rs @@ -1337,6 +1337,116 @@ fn grok_4_6_mixed_requests_sum_short_and_long_context_costs() { ); } +#[test] +fn grok_4_7_build_uses_standard_and_fast_rates() { + let standard = test_model("grok-4.7"); + let fast = test_model("grok-4.7-build-fast"); + let usage = UsageCounts { + input_tokens: Some(100_000), + cache_read_tokens: Some(100_000), + output_tokens: Some(10_000), + requests: Some(1), + ..UsageCounts::default() + }; + + let standard_cost = estimate_cost("grok_build", Some(&standard), &usage); + let fast_cost = estimate_cost("grok_build", Some(&fast), &usage); + + assert_eq!( + standard_cost.estimated_api_equivalent_micro_usd, + Some(310_000) + ); + assert_eq!( + fast_cost.estimated_api_equivalent_micro_usd, + Some(620_000) + ); + assert_eq!( + standard_cost.pricing_source.as_deref(), + Some("xai_api_pricing:grok-4.7") + ); + assert_eq!( + fast_cost.pricing_source.as_deref(), + Some("xai_api_pricing:grok-4.7:fast") + ); +} + +#[test] +fn grok_4_7_build_long_context_starts_at_200k_prompt_tokens() { + let standard = test_model("grok-4.7"); + let fast = test_model("grok-4.7-build-fast"); + let usage = UsageCounts { + input_tokens: Some(120_000), + cache_read_tokens: Some(80_000), + output_tokens: Some(10_000), + requests: Some(1), + ..UsageCounts::default() + }; + + let standard_cost = estimate_cost("grok_build", Some(&standard), &usage); + let fast_cost = estimate_cost("grok_build", Some(&fast), &usage); + + assert_eq!( + standard_cost.estimated_api_equivalent_micro_usd, + Some(680_000) + ); + assert_eq!( + fast_cost.estimated_api_equivalent_micro_usd, + Some(1_360_000) + ); +} + +#[test] +fn cursor_grok_4_7_uses_256k_boundary_and_three_times_fast_long_context_rates() { + let standard = statsai_core::ModelInfo { + name: Some("cursor-grok-4.7-xhigh".to_string()), + normalized_name: Some("grok-4.7".to_string()), + provider_model_id: Some("cursor-grok-4.7-xhigh".to_string()), + speed: Some("standard".to_string()), + reasoning_level: None, + reasoning_level_raw: None, + }; + let fast = statsai_core::ModelInfo { + name: Some("cursor-grok-4.7-xhigh-fast".to_string()), + provider_model_id: Some("cursor-grok-4.7-xhigh-fast".to_string()), + speed: Some("fast".to_string()), + ..standard.clone() + }; + let at_threshold = UsageCounts { + input_tokens: Some(256_000), + output_tokens: Some(10_000), + requests: Some(1), + ..UsageCounts::default() + }; + let above_threshold = UsageCounts { + input_tokens: Some(256_001), + output_tokens: Some(10_000), + requests: Some(1), + ..UsageCounts::default() + }; + + let standard_at = estimate_cost("cursor", Some(&standard), &at_threshold); + let standard_above = estimate_cost("cursor", Some(&standard), &above_threshold); + let fast_at = estimate_cost("cursor", Some(&fast), &at_threshold); + let fast_above = estimate_cost("cursor", Some(&fast), &above_threshold); + + assert_eq!( + standard_at.estimated_api_equivalent_micro_usd, + Some(572_000) + ); + assert_eq!( + standard_above.estimated_api_equivalent_micro_usd, + Some(1_144_004) + ); + assert_eq!( + fast_at.estimated_api_equivalent_micro_usd, + Some(1_144_000) + ); + assert_eq!( + fast_above.estimated_api_equivalent_micro_usd, + Some(1_716_006) + ); +} + #[test] fn a_stale_cached_normalization_does_not_shadow_the_observed_name() { // Stored before Fable 5.1 existed in the catalog, so the cached From 52090581599c76153df3335174cc398939e41532 Mon Sep 17 00:00:00 2001 From: Dmitry Starkov <21260939+starkdmi@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:24:27 +0300 Subject: [PATCH 07/20] Keep Cursor Grok 4.7 on Cursor context tiers --- crates/statsai-pricing/src/cost.rs | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/crates/statsai-pricing/src/cost.rs b/crates/statsai-pricing/src/cost.rs index a9e8d50..8969d4f 100644 --- a/crates/statsai-pricing/src/cost.rs +++ b/crates/statsai-pricing/src/cost.rs @@ -171,19 +171,21 @@ fn pricing_multipliers( .saturating_add(usage.cache_creation_tokens.unwrap_or(0)) .saturating_add(usage.cache_read_tokens.unwrap_or(0)); - if model_name == "grok-4.7" - && provider.eq_ignore_ascii_case("cursor") - && usage.requests == Some(1) - && prompt_tokens > CURSOR_GROK_4_7_LONG_CONTEXT_THRESHOLD - { - // Cursor publishes 2x standard rates above 256k, while Fast is 3x the - // short-context standard rate. Fast base pricing is already 2x here, - // so its long-context multiplier is 1.5x rather than 2x. - return if uses_fast_mode_pricing { - (15_000, 15_000) - } else { - (20_000, 20_000) - }; + if model_name == "grok-4.7" && provider.eq_ignore_ascii_case("cursor") { + if usage.requests == Some(1) + && prompt_tokens > CURSOR_GROK_4_7_LONG_CONTEXT_THRESHOLD + { + // Cursor publishes 2x standard rates above 256k, while Fast is 3x the + // short-context standard rate. Fast base pricing is already 2x here, + // so its long-context multiplier is 1.5x rather than 2x. + return if uses_fast_mode_pricing { + (15_000, 15_000) + } else { + (20_000, 20_000) + }; + } + // Never fall through to xAI's >=200k API tier for Cursor usage. + return (MULTIPLIER_SCALE, MULTIPLIER_SCALE); } let is_openai_long_context_model = matches!( From 51214b068a6bba5bf5fb0e20f25d5476075ed647 Mon Sep 17 00:00:00 2001 From: Dmitry Starkov <21260939+starkdmi@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:25:34 +0300 Subject: [PATCH 08/20] Attribute Cursor Grok 4.7 pricing to Cursor --- crates/statsai-pricing/src/cost.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/statsai-pricing/src/cost.rs b/crates/statsai-pricing/src/cost.rs index 8969d4f..6691e80 100644 --- a/crates/statsai-pricing/src/cost.rs +++ b/crates/statsai-pricing/src/cost.rs @@ -78,6 +78,9 @@ pub fn estimate_cost_at( let mut pricing_source = match model_name.as_str() { "composer-2.5" | "composer-2.5-fast" => format!("cursor_model_pricing:{observed_name}"), + "grok-4.7" if provider.eq_ignore_ascii_case("cursor") => { + format!("cursor_model_pricing:{observed_name}") + } "grok-build-0.1" | "grok-4.3" | "grok-4.5" From aa98277a525906174817922c927c533c1a3470bf Mon Sep 17 00:00:00 2001 From: Dmitry Starkov <21260939+starkdmi@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:25:42 +0300 Subject: [PATCH 09/20] Assert Cursor Grok 4.7 pricing provenance --- crates/statsai-pricing/src/tests/cost.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/statsai-pricing/src/tests/cost.rs b/crates/statsai-pricing/src/tests/cost.rs index 6ddce1f..c1e2bd6 100644 --- a/crates/statsai-pricing/src/tests/cost.rs +++ b/crates/statsai-pricing/src/tests/cost.rs @@ -1445,6 +1445,14 @@ fn cursor_grok_4_7_uses_256k_boundary_and_three_times_fast_long_context_rates() fast_above.estimated_api_equivalent_micro_usd, Some(1_716_006) ); + assert_eq!( + standard_above.pricing_source.as_deref(), + Some("cursor_model_pricing:grok-4.7") + ); + assert_eq!( + fast_above.pricing_source.as_deref(), + Some("cursor_model_pricing:grok-4.7:fast") + ); } #[test] From d14ef6386b68778c23d72b8cebd604e38e9f280b Mon Sep 17 00:00:00 2001 From: Dmitry Starkov <21260939+starkdmi@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:33:52 +0300 Subject: [PATCH 10/20] Format Grok 4.7 pricing code --- crates/statsai-pricing/src/cost.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/statsai-pricing/src/cost.rs b/crates/statsai-pricing/src/cost.rs index 6691e80..00f780c 100644 --- a/crates/statsai-pricing/src/cost.rs +++ b/crates/statsai-pricing/src/cost.rs @@ -175,9 +175,7 @@ fn pricing_multipliers( .saturating_add(usage.cache_read_tokens.unwrap_or(0)); if model_name == "grok-4.7" && provider.eq_ignore_ascii_case("cursor") { - if usage.requests == Some(1) - && prompt_tokens > CURSOR_GROK_4_7_LONG_CONTEXT_THRESHOLD - { + if usage.requests == Some(1) && prompt_tokens > CURSOR_GROK_4_7_LONG_CONTEXT_THRESHOLD { // Cursor publishes 2x standard rates above 256k, while Fast is 3x the // short-context standard rate. Fast base pricing is already 2x here, // so its long-context multiplier is 1.5x rather than 2x. From 8d255b72d55b6c30fa5ffacdfd31fc1a488c0a82 Mon Sep 17 00:00:00 2001 From: Dmitry Starkov <21260939+starkdmi@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:33:56 +0300 Subject: [PATCH 11/20] Format Grok 4.7 pricing tests --- crates/statsai-pricing/src/tests/cost.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/statsai-pricing/src/tests/cost.rs b/crates/statsai-pricing/src/tests/cost.rs index c1e2bd6..656f811 100644 --- a/crates/statsai-pricing/src/tests/cost.rs +++ b/crates/statsai-pricing/src/tests/cost.rs @@ -1356,10 +1356,7 @@ fn grok_4_7_build_uses_standard_and_fast_rates() { standard_cost.estimated_api_equivalent_micro_usd, Some(310_000) ); - assert_eq!( - fast_cost.estimated_api_equivalent_micro_usd, - Some(620_000) - ); + assert_eq!(fast_cost.estimated_api_equivalent_micro_usd, Some(620_000)); assert_eq!( standard_cost.pricing_source.as_deref(), Some("xai_api_pricing:grok-4.7") @@ -1437,10 +1434,7 @@ fn cursor_grok_4_7_uses_256k_boundary_and_three_times_fast_long_context_rates() standard_above.estimated_api_equivalent_micro_usd, Some(1_144_004) ); - assert_eq!( - fast_at.estimated_api_equivalent_micro_usd, - Some(1_144_000) - ); + assert_eq!(fast_at.estimated_api_equivalent_micro_usd, Some(1_144_000)); assert_eq!( fast_above.estimated_api_equivalent_micro_usd, Some(1_716_006) From 64d689f450bf374e33e1a660e88c982a49cd7d21 Mon Sep 17 00:00:00 2001 From: Dmitry Starkov <21260939+starkdmi@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:33:59 +0300 Subject: [PATCH 12/20] Format Grok 4.7 normalization tests --- crates/statsai-pricing/src/tests/normalize.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/statsai-pricing/src/tests/normalize.rs b/crates/statsai-pricing/src/tests/normalize.rs index 1b1d3fa..ba51fc1 100644 --- a/crates/statsai-pricing/src/tests/normalize.rs +++ b/crates/statsai-pricing/src/tests/normalize.rs @@ -143,14 +143,8 @@ fn normalizes_grok_build_aliases() { ); assert_eq!(normalize_model_name("relay/xai-grok-4.6"), "grok-4.6"); assert_eq!(normalize_model_name("grok-4.7"), "grok-4.7"); - assert_eq!( - normalize_model_name("grok-4.7-build-fast"), - "grok-4.7" - ); - assert_eq!( - normalize_model_name("cursor-grok-4.7-xhigh"), - "grok-4.7" - ); + assert_eq!(normalize_model_name("grok-4.7-build-fast"), "grok-4.7"); + assert_eq!(normalize_model_name("cursor-grok-4.7-xhigh"), "grok-4.7"); assert_eq!( normalize_model_name("cursor-grok-4.7-high-fast"), "grok-4.7" From 53d2172c905d7c2033bb3721621d76fb13570d8d Mon Sep 17 00:00:00 2001 From: Dmitry Starkov <21260939+starkdmi@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:43:36 +0300 Subject: [PATCH 13/20] Preserve Grok Build speed in model attribution --- .../statsai-adapters/src/grok/parse/models.rs | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/crates/statsai-adapters/src/grok/parse/models.rs b/crates/statsai-adapters/src/grok/parse/models.rs index 268b715..e176736 100644 --- a/crates/statsai-adapters/src/grok/parse/models.rs +++ b/crates/statsai-adapters/src/grok/parse/models.rs @@ -52,17 +52,27 @@ pub(crate) fn grok_normalized_model_id(model_id: &str) -> String { normalize_model_name(model_id) } +fn grok_model_equivalence_key(model_id: &str) -> String { + let model_id = model_id.trim(); + let normalized = grok_normalized_model_id(model_id); + if model_id.to_ascii_lowercase().ends_with("-fast") { + format!("{normalized}:fast") + } else { + normalized + } +} + pub(crate) fn grok_models_equivalent(left: &str, right: &str) -> bool { - grok_normalized_model_id(left) == grok_normalized_model_id(right) + grok_model_equivalence_key(left) == grok_model_equivalence_key(right) } -pub(crate) fn unique_grok_normalized_models<'a>( +pub(crate) fn unique_grok_model_keys<'a>( ids: impl IntoIterator, ) -> HashSet { ids.into_iter() .map(str::trim) .filter(|model_id| !model_id.is_empty()) - .map(grok_normalized_model_id) + .map(grok_model_equivalence_key) .collect() } @@ -108,10 +118,10 @@ pub(crate) fn resolve_grok_inference_sample_model( .iter() .map(|observation| observation.model_id.as_str()), ); - let assignable = unique_grok_normalized_models(assignable_ids); + let assignable = unique_grok_model_keys(assignable_ids); if assignable.len() == 1 { let models_used = - unique_grok_normalized_models(session_models_used.iter().map(String::as_str)); + unique_grok_model_keys(session_models_used.iter().map(String::as_str)); // A lone prompt/turn observation cannot cover every inference when // modelsUsed reports another model: request-level attribution is // incomplete, so do not silently price the missing model as this one. @@ -148,7 +158,7 @@ pub(crate) fn resolve_grok_inference_sample_model( .iter() .map(String::as_str) .chain(grok_current_model_id(current_model)); - if unique_grok_normalized_models(session_ids).len() == 1 { + if unique_grok_model_keys(session_ids).len() == 1 { return current_model.cloned().or_else(|| { session_models_used .first() From 96fd2e617283ab9038214d44e03428be05db173a Mon Sep 17 00:00:00 2001 From: Dmitry Starkov <21260939+starkdmi@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:43:39 +0300 Subject: [PATCH 14/20] Keep Grok 4.7 short-context test below threshold --- crates/statsai-pricing/src/tests/cost.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/statsai-pricing/src/tests/cost.rs b/crates/statsai-pricing/src/tests/cost.rs index 656f811..0673b8e 100644 --- a/crates/statsai-pricing/src/tests/cost.rs +++ b/crates/statsai-pricing/src/tests/cost.rs @@ -1343,7 +1343,7 @@ fn grok_4_7_build_uses_standard_and_fast_rates() { let fast = test_model("grok-4.7-build-fast"); let usage = UsageCounts { input_tokens: Some(100_000), - cache_read_tokens: Some(100_000), + cache_read_tokens: Some(90_000), output_tokens: Some(10_000), requests: Some(1), ..UsageCounts::default() @@ -1354,9 +1354,9 @@ fn grok_4_7_build_uses_standard_and_fast_rates() { assert_eq!( standard_cost.estimated_api_equivalent_micro_usd, - Some(310_000) + Some(305_000) ); - assert_eq!(fast_cost.estimated_api_equivalent_micro_usd, Some(620_000)); + assert_eq!(fast_cost.estimated_api_equivalent_micro_usd, Some(610_000)); assert_eq!( standard_cost.pricing_source.as_deref(), Some("xai_api_pricing:grok-4.7") From 74a1289b526e7348141d013365c6b63e42eb009d Mon Sep 17 00:00:00 2001 From: Dmitry Starkov <21260939+starkdmi@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:43:58 +0300 Subject: [PATCH 15/20] Test Grok 4.7 standard to fast attribution --- .../src/grok/tests/parse/models.rs | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/crates/statsai-adapters/src/grok/tests/parse/models.rs b/crates/statsai-adapters/src/grok/tests/parse/models.rs index d0c8960..3e3d7d4 100644 --- a/crates/statsai-adapters/src/grok/tests/parse/models.rs +++ b/crates/statsai-adapters/src/grok/tests/parse/models.rs @@ -611,6 +611,83 @@ fn grok_inference_model_resolution_joins_prompt_model_id_by_timestamp() { assert_eq!(unresolved, None); } +#[test] +fn grok_inference_model_resolution_distinguishes_grok_4_7_fast_by_timestamp() { + let first = DateTime::parse_from_rfc3339("2026-09-22T10:00:10Z") + .expect("first") + .with_timezone(&Utc); + let second = DateTime::parse_from_rfc3339("2026-09-22T10:01:10Z") + .expect("second") + .with_timezone(&Utc); + let prompt_models = [ + GrokModelObservation { + model_id: "grok-4.7".to_string(), + observed_at: Some( + DateTime::parse_from_rfc3339("2026-09-22T10:00:00Z") + .expect("standard") + .with_timezone(&Utc), + ), + }, + GrokModelObservation { + model_id: "grok-4.7-build-fast".to_string(), + observed_at: Some( + DateTime::parse_from_rfc3339("2026-09-22T10:01:00Z") + .expect("fast") + .with_timezone(&Utc), + ), + }, + ]; + let current = model_info("grok-4.7-build-fast"); + + let standard = resolve_grok_inference_sample_model( + &GrokInferenceSample { + usage: UsageCounts { + input_tokens: Some(100_000), + requests: Some(1), + ..UsageCounts::default() + }, + observed_at: Some(first), + }, + &prompt_models, + &[], + &["grok-4.7".to_string(), "grok-4.7-build-fast".to_string()], + Some(¤t), + ) + .expect("standard model"); + let fast = resolve_grok_inference_sample_model( + &GrokInferenceSample { + usage: UsageCounts { + input_tokens: Some(100_000), + requests: Some(1), + ..UsageCounts::default() + }, + observed_at: Some(second), + }, + &prompt_models, + &[], + &["grok-4.7".to_string(), "grok-4.7-build-fast".to_string()], + Some(¤t), + ) + .expect("fast model"); + + assert_eq!(standard.name.as_deref(), Some("grok-4.7")); + assert_eq!(fast.name.as_deref(), Some("grok-4.7-build-fast")); + + let standard_cost = estimate_cost(GROK_BUILD_PROVIDER, Some(&standard), &UsageCounts { + input_tokens: Some(100_000), + requests: Some(1), + ..UsageCounts::default() + }); + let fast_cost = estimate_cost(GROK_BUILD_PROVIDER, Some(&fast), &UsageCounts { + input_tokens: Some(100_000), + requests: Some(1), + ..UsageCounts::default() + }); + + assert_eq!(standard_cost.estimated_api_equivalent_micro_usd, Some(200_000)); + assert_eq!(fast_cost.estimated_api_equivalent_micro_usd, Some(400_000)); +} + #[test] fn grok_inference_model_resolution_rejects_partial_observation_when_models_used_is_mixed() { let observed_at = DateTime::parse_from_rfc3339("2026-08-16T18:32:10Z") From 75057c975dc06238abce28c2b809242893526cbc Mon Sep 17 00:00:00 2001 From: Dmitry Starkov <21260939+starkdmi@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:47:53 +0300 Subject: [PATCH 16/20] Format Grok speed-aware attribution --- crates/statsai-adapters/src/grok/parse/models.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/statsai-adapters/src/grok/parse/models.rs b/crates/statsai-adapters/src/grok/parse/models.rs index e176736..a53630b 100644 --- a/crates/statsai-adapters/src/grok/parse/models.rs +++ b/crates/statsai-adapters/src/grok/parse/models.rs @@ -120,8 +120,7 @@ pub(crate) fn resolve_grok_inference_sample_model( ); let assignable = unique_grok_model_keys(assignable_ids); if assignable.len() == 1 { - let models_used = - unique_grok_model_keys(session_models_used.iter().map(String::as_str)); + let models_used = unique_grok_model_keys(session_models_used.iter().map(String::as_str)); // A lone prompt/turn observation cannot cover every inference when // modelsUsed reports another model: request-level attribution is // incomplete, so do not silently price the missing model as this one. From 5dd80f930337eee29afa24c7c54d793eb18ee3fb Mon Sep 17 00:00:00 2001 From: Dmitry Starkov <21260939+starkdmi@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:47:56 +0300 Subject: [PATCH 17/20] Compile and format Grok 4.7 speed regression test --- .../src/grok/tests/parse/models.rs | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/crates/statsai-adapters/src/grok/tests/parse/models.rs b/crates/statsai-adapters/src/grok/tests/parse/models.rs index 3e3d7d4..9c202bf 100644 --- a/crates/statsai-adapters/src/grok/tests/parse/models.rs +++ b/crates/statsai-adapters/src/grok/tests/parse/models.rs @@ -673,19 +673,33 @@ fn grok_inference_model_resolution_distinguishes_grok_4_7_fast_by_timestamp() { assert_eq!(standard.name.as_deref(), Some("grok-4.7")); assert_eq!(fast.name.as_deref(), Some("grok-4.7-build-fast")); - let standard_cost = estimate_cost(GROK_BUILD_PROVIDER, Some(&standard), &UsageCounts { - input_tokens: Some(100_000), - requests: Some(1), - ..UsageCounts::default() - }); - let fast_cost = estimate_cost(GROK_BUILD_PROVIDER, Some(&fast), &UsageCounts { - input_tokens: Some(100_000), - requests: Some(1), - ..UsageCounts::default() - }); + let standard_cost = statsai_pricing::estimate_cost( + GROK_BUILD_PROVIDER, + Some(&standard), + &UsageCounts { + input_tokens: Some(100_000), + requests: Some(1), + ..UsageCounts::default() + }, + ); + let fast_cost = statsai_pricing::estimate_cost( + GROK_BUILD_PROVIDER, + Some(&fast), + &UsageCounts { + input_tokens: Some(100_000), + requests: Some(1), + ..UsageCounts::default() + }, + ); - assert_eq!(standard_cost.estimated_api_equivalent_micro_usd, Some(200_000)); - assert_eq!(fast_cost.estimated_api_equivalent_micro_usd, Some(400_000)); + assert_eq!( + standard_cost.estimated_api_equivalent_micro_usd, + Some(200_000) + ); + assert_eq!( + fast_cost.estimated_api_equivalent_micro_usd, + Some(400_000) + ); } #[test] From 284e34db6bbc3db13db0eff6667d739c078aada3 Mon Sep 17 00:00:00 2001 From: Dmitry Starkov <21260939+starkdmi@users.noreply.github.com> Date: Tue, 22 Sep 2026 23:06:14 +0300 Subject: [PATCH 18/20] Use speed-aware Grok keys for fallback pricing --- crates/statsai-adapters/src/grok/parse/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/statsai-adapters/src/grok/parse/mod.rs b/crates/statsai-adapters/src/grok/parse/mod.rs index a9cf041..901f57c 100644 --- a/crates/statsai-adapters/src/grok/parse/mod.rs +++ b/crates/statsai-adapters/src/grok/parse/mod.rs @@ -242,7 +242,7 @@ pub(crate) fn parse_grok_summary( &session_models_used, &summary.observed_at, ) - } else if unique_grok_normalized_models( + } else if unique_grok_model_keys( session_models_used .iter() .map(String::as_str) From 52add4a45da6e1b03a9ad46baa3fa7d0ece340ff Mon Sep 17 00:00:00 2001 From: Dmitry Starkov <21260939+starkdmi@users.noreply.github.com> Date: Tue, 22 Sep 2026 23:06:17 +0300 Subject: [PATCH 19/20] Format Grok 4.7 speed regression test --- crates/statsai-adapters/src/grok/tests/parse/models.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/statsai-adapters/src/grok/tests/parse/models.rs b/crates/statsai-adapters/src/grok/tests/parse/models.rs index 9c202bf..f3ee964 100644 --- a/crates/statsai-adapters/src/grok/tests/parse/models.rs +++ b/crates/statsai-adapters/src/grok/tests/parse/models.rs @@ -696,10 +696,7 @@ fn grok_inference_model_resolution_distinguishes_grok_4_7_fast_by_timestamp() { standard_cost.estimated_api_equivalent_micro_usd, Some(200_000) ); - assert_eq!( - fast_cost.estimated_api_equivalent_micro_usd, - Some(400_000) - ); + assert_eq!(fast_cost.estimated_api_equivalent_micro_usd, Some(400_000)); } #[test] From 832dde575da9e40f80e23fa5406cb64aad1a0568 Mon Sep 17 00:00:00 2001 From: Dmitry Starkov <21260939+starkdmi@users.noreply.github.com> Date: Tue, 22 Sep 2026 23:47:19 +0300 Subject: [PATCH 20/20] Price single Grok 4.7 Fast inference with corroborated model --- crates/statsai-adapters/src/cache.rs | 4 +- crates/statsai-adapters/src/grok/parse/mod.rs | 1 + .../statsai-adapters/src/grok/parse/models.rs | 22 +++- .../src/grok/tests/parse/mod.rs | 2 +- .../src/grok/tests/parse/models.rs | 100 +++++++++++++++++- 5 files changed, 122 insertions(+), 7 deletions(-) diff --git a/crates/statsai-adapters/src/cache.rs b/crates/statsai-adapters/src/cache.rs index 8dbdd3e..98f4a99 100644 --- a/crates/statsai-adapters/src/cache.rs +++ b/crates/statsai-adapters/src/cache.rs @@ -15,7 +15,9 @@ pub(crate) const CLAUDE_SCAN_CACHE_PARSER_REVISION: &str = "activity-invocations // activity-invocations.v20: session rows report their message count as requests and // are priced per message, so long-context tiers stop being decided session-wide. pub(crate) const OPENCODE_SCAN_CACHE_PARSER_REVISION: &str = "activity-invocations.v20"; -pub(crate) const GROK_BUILD_SCAN_CACHE_PARSER_REVISION: &str = "activity-invocations.v22"; +// Revisit Grok sessions whose Fast request was left unpriced because modelsUsed +// also retained a selected standard model that made no inference. +pub(crate) const GROK_BUILD_SCAN_CACHE_PARSER_REVISION: &str = "grok-fast-pricing.v23"; pub(crate) fn scan_candidate( path: PathBuf, diff --git a/crates/statsai-adapters/src/grok/parse/mod.rs b/crates/statsai-adapters/src/grok/parse/mod.rs index 901f57c..d575f54 100644 --- a/crates/statsai-adapters/src/grok/parse/mod.rs +++ b/crates/statsai-adapters/src/grok/parse/mod.rs @@ -27,6 +27,7 @@ pub(crate) fn estimate_grok_inference_sample_costs( for sample in samples { let Some(model) = resolve_grok_inference_sample_model( sample, + samples.len(), prompt_models, turn_models, session_models_used, diff --git a/crates/statsai-adapters/src/grok/parse/models.rs b/crates/statsai-adapters/src/grok/parse/models.rs index a53630b..5403efc 100644 --- a/crates/statsai-adapters/src/grok/parse/models.rs +++ b/crates/statsai-adapters/src/grok/parse/models.rs @@ -105,6 +105,7 @@ pub(crate) fn last_grok_model_at_or_before( pub(crate) fn resolve_grok_inference_sample_model( sample: &GrokInferenceSample, + sample_count: usize, prompt_models: &[GrokModelObservation], turn_models: &[GrokModelObservation], session_models_used: &[String], @@ -121,10 +122,23 @@ pub(crate) fn resolve_grok_inference_sample_model( let assignable = unique_grok_model_keys(assignable_ids); if assignable.len() == 1 { let models_used = unique_grok_model_keys(session_models_used.iter().map(String::as_str)); - // A lone prompt/turn observation cannot cover every inference when - // modelsUsed reports another model: request-level attribution is - // incomplete, so do not silently price the missing model as this one. - if !models_used.is_empty() && models_used != assignable { + // modelsUsed may include a selected model that made no request. When + // there is exactly one inference, matching prompt and turn observations + // before it identify its model despite that extra selection. With more + // inferences, one observed model cannot account for an unobserved one. + let confirmed_single_sample = sample_count == 1 + && sample.observed_at.is_some_and(|at| { + prompt_models.iter().any(|observation| { + observation + .observed_at + .is_some_and(|observed_at| observed_at <= at) + }) && turn_models.iter().any(|observation| { + observation + .observed_at + .is_some_and(|observed_at| observed_at <= at) + }) + }); + if !models_used.is_empty() && models_used != assignable && !confirmed_single_sample { return None; } let model_id = prompt_models diff --git a/crates/statsai-adapters/src/grok/tests/parse/mod.rs b/crates/statsai-adapters/src/grok/tests/parse/mod.rs index 54239a6..8a95350 100644 --- a/crates/statsai-adapters/src/grok/tests/parse/mod.rs +++ b/crates/statsai-adapters/src/grok/tests/parse/mod.rs @@ -10,7 +10,7 @@ fn grok_request_level_pricing_upgrade_advances_parser_revision() { .and_then(|(_, value)| value.parse::().ok()) .expect("Grok parser revision"); - assert!(revision > 19); + assert!(revision > 22); } #[test] diff --git a/crates/statsai-adapters/src/grok/tests/parse/models.rs b/crates/statsai-adapters/src/grok/tests/parse/models.rs index f3ee964..f9ddd53 100644 --- a/crates/statsai-adapters/src/grok/tests/parse/models.rs +++ b/crates/statsai-adapters/src/grok/tests/parse/models.rs @@ -576,6 +576,7 @@ fn grok_inference_model_resolution_joins_prompt_model_id_by_timestamp() { usage: UsageCounts::default(), observed_at: Some(first), }, + 2, &prompt_models, &[], &["grok-4.5".to_string(), "grok-4.6".to_string()], @@ -587,6 +588,7 @@ fn grok_inference_model_resolution_joins_prompt_model_id_by_timestamp() { usage: UsageCounts::default(), observed_at: Some(second), }, + 2, &prompt_models, &[], &["grok-4.5".to_string(), "grok-4.6".to_string()], @@ -598,6 +600,7 @@ fn grok_inference_model_resolution_joins_prompt_model_id_by_timestamp() { usage: UsageCounts::default(), observed_at: Some(first), }, + 2, &[], &[], &["grok-4.5".to_string(), "grok-4.6".to_string()], @@ -648,6 +651,7 @@ fn grok_inference_model_resolution_distinguishes_grok_4_7_fast_by_timestamp() { }, observed_at: Some(first), }, + 2, &prompt_models, &[], &["grok-4.7".to_string(), "grok-4.7-build-fast".to_string()], @@ -663,6 +667,7 @@ fn grok_inference_model_resolution_distinguishes_grok_4_7_fast_by_timestamp() { }, observed_at: Some(second), }, + 2, &prompt_models, &[], &["grok-4.7".to_string(), "grok-4.7-build-fast".to_string()], @@ -699,6 +704,97 @@ fn grok_inference_model_resolution_distinguishes_grok_4_7_fast_by_timestamp() { assert_eq!(fast_cost.estimated_api_equivalent_micro_usd, Some(400_000)); } +#[test] +fn grok_build_prices_single_fast_inference_when_signals_list_both_speeds() { + let dir = tempfile::tempdir().expect("tempdir"); + let session = dir + .path() + .join("sessions") + .join("%2Fworkspace") + .join("session-fast-only"); + std::fs::create_dir_all(&session).expect("session dir"); + std::fs::create_dir_all(dir.path().join("logs")).expect("logs dir"); + std::fs::write( + session.join("summary.json"), + serde_json::json!({ + "info": {"id": "session-fast-only", "cwd": dir.path()}, + "updated_at": "2026-09-22T20:32:05Z", + "current_model_id": "grok-4.7", + "chat_format_version": 1 + }) + .to_string(), + ) + .expect("summary"); + std::fs::write( + session.join("signals.json"), + serde_json::json!({ + "modelsUsed": ["grok-4.7", "grok-4.7-build-fast"], + "primaryModelId": "grok-4.7", + "turnCount": 1 + }) + .to_string(), + ) + .expect("signals"); + std::fs::write( + session.join("updates.jsonl"), + serde_json::json!({ + "timestamp": 1_790_109_116, + "params": { + "update": {"_meta": {"modelId": "grok-4.7-build-fast", "promptIndex": 0}}, + "_meta": {"agentTimestampMs": 1_790_109_116_000i64} + } + }) + .to_string(), + ) + .expect("updates"); + std::fs::write( + session.join("events.jsonl"), + serde_json::json!({ + "ts": "2026-09-22T20:31:56Z", + "type": "turn_started", + "model_id": "grok-4.7-build-fast" + }) + .to_string(), + ) + .expect("events"); + std::fs::write( + dir.path().join("logs/unified.jsonl"), + serde_json::json!({ + "ts": "2026-09-22T20:31:58Z", + "sid": "session-fast-only", + "msg": "shell.turn.inference_done", + "ctx": { + "prompt_tokens": 19_323, + "cached_prompt_tokens": 1_152, + "completion_tokens": 39, + "reasoning_tokens": 29 + } + }) + .to_string(), + ) + .expect("unified log"); + let source = SourceLocation::local_adapter( + GROK_BUILD_PROVIDER, + "test", + "0", + dir.path(), + LocationOrigin::Configured, + ); + + let scan = scan_grok_build_source(&GrokBuildAdapter, &source, &options()).expect("scan"); + let summary = &scan.summaries[0]; + + assert_eq!(summary.usage.requests, Some(1)); + assert_eq!( + summary.cost.estimated_api_equivalent_micro_usd, + Some(74_652) + ); + assert_eq!( + summary.cost.pricing_source.as_deref(), + Some("xai_api_pricing:grok-4.7:fast:unified_log_inference_usage") + ); +} + #[test] fn grok_inference_model_resolution_rejects_partial_observation_when_models_used_is_mixed() { let observed_at = DateTime::parse_from_rfc3339("2026-08-16T18:32:10Z") @@ -720,6 +816,7 @@ fn grok_inference_model_resolution_rejects_partial_observation_when_models_used_ let mixed = resolve_grok_inference_sample_model( &sample, + 2, &prompt_models, &[], &["grok-4.5".to_string(), "grok-4.6".to_string()], @@ -727,6 +824,7 @@ fn grok_inference_model_resolution_rejects_partial_observation_when_models_used_ ); let matching = resolve_grok_inference_sample_model( &sample, + 2, &prompt_models, &[], &["grok-4.5".to_string()], @@ -734,7 +832,7 @@ fn grok_inference_model_resolution_rejects_partial_observation_when_models_used_ ) .expect("matching modelsUsed"); let empty_used = - resolve_grok_inference_sample_model(&sample, &prompt_models, &[], &[], Some(¤t)) + resolve_grok_inference_sample_model(&sample, 2, &prompt_models, &[], &[], Some(¤t)) .expect("empty modelsUsed"); assert_eq!(mixed, None);