From b97a1983c3ced32d3a86e9bc84f39ca8097bd844 Mon Sep 17 00:00:00 2001 From: Dmitry Starkov <21260939+starkdmi@users.noreply.github.com> Date: Wed, 23 Sep 2026 01:08:33 +0300 Subject: [PATCH] Count underlying requests in synced rollups --- crates/statsai-store/src/lib.rs | 7 ++-- crates/statsai-store/src/rollups/build.rs | 9 +++-- crates/statsai-store/src/rollups/mod.rs | 2 +- crates/statsai-store/src/tests/events.rs | 10 +++-- .../statsai-store/src/tests/rollups/build.rs | 38 +++++++++++++++++++ crates/statsai-store/src/tests/rollups/mod.rs | 5 ++- 6 files changed, 57 insertions(+), 14 deletions(-) diff --git a/crates/statsai-store/src/lib.rs b/crates/statsai-store/src/lib.rs index 38aba96..f34c59b 100644 --- a/crates/statsai-store/src/lib.rs +++ b/crates/statsai-store/src/lib.rs @@ -106,9 +106,10 @@ pub use tasks::{ TaskDeletionImpact, TaskRebuildReport, TaskRebuildTimings, TaskStats, }; -// 13: project identity dropped the git remote, so every bucket that carries a -// path has to be rebuilt for a repository rename to stop splitting a day. -const SYNC_ROLLUP_SUMMARY_VERSION: &str = "13"; +// 14: rollup request counts sum underlying requests inside each usage event. +// Codex turn events may contain multiple API requests, so old rollups must be +// rebuilt and synced again to make model and provider counts comparable. +const SYNC_ROLLUP_SUMMARY_VERSION: &str = "14"; const SYNC_INCLUDE_PROJECTS_METADATA_KEY: &str = "sync.include_projects"; const SYNC_INCLUDE_TASKS_METADATA_KEY: &str = "sync.include_tasks"; const SYNC_INCLUDE_ACTIVITY_METADATA_KEY: &str = "sync.include_activity"; diff --git a/crates/statsai-store/src/rollups/build.rs b/crates/statsai-store/src/rollups/build.rs index 404e9f4..30bb3a5 100644 --- a/crates/statsai-store/src/rollups/build.rs +++ b/crates/statsai-store/src/rollups/build.rs @@ -63,7 +63,7 @@ pub(crate) fn build_sync_rollup_summary(events: &[UsageEvent]) -> UsageSummary { let mut total_cache_read = 0u64; let mut total_reasoning = 0u64; let mut total_tokens = 0u64; - let mut total_events = 0u64; + let mut total_requests = 0u64; let mut estimated_cost = CostAccumulator::default(); let mut provider_reported_cost = CostAccumulator::default(); let mut has_provider_reported_usd = false; @@ -99,7 +99,8 @@ pub(crate) fn build_sync_rollup_summary(events: &[UsageEvent]) -> UsageSummary { total_cache_read.saturating_add(event.usage.cache_read_tokens.unwrap_or(0)); total_reasoning = total_reasoning.saturating_add(event.usage.reasoning_tokens.unwrap_or(0)); total_tokens = total_tokens.saturating_add(event.usage.computed_total()); - total_events = total_events.saturating_add(1); + let requests = event.usage.requests.unwrap_or(1); + total_requests = total_requests.saturating_add(requests); estimated_cost.add_estimated(&event.cost); if event.cost.provider_reported_micro_usd.is_some() || event.cost.provider_reported_usd.is_some() @@ -235,7 +236,7 @@ pub(crate) fn build_sync_rollup_summary(events: &[UsageEvent]) -> UsageSummary { .1 .total_tokens .saturating_add(event.usage.computed_total()); - entry.1.requests = entry.1.requests.saturating_add(1); + entry.1.requests = entry.1.requests.saturating_add(requests); entry.1.estimated_cost.add_estimated(&event.cost); if event.cost.provider_reported_micro_usd.is_some() || event.cost.provider_reported_usd.is_some() @@ -345,7 +346,7 @@ pub(crate) fn build_sync_rollup_summary(events: &[UsageEvent]) -> UsageSummary { cache_read_tokens: Some(total_cache_read), reasoning_tokens: Some(total_reasoning), total_tokens: Some(total_tokens), - requests: Some(total_events), + requests: Some(total_requests), local_prompt_eval_tokens: None, local_eval_tokens: None, }, diff --git a/crates/statsai-store/src/rollups/mod.rs b/crates/statsai-store/src/rollups/mod.rs index 4f412e4..7dc846f 100644 --- a/crates/statsai-store/src/rollups/mod.rs +++ b/crates/statsai-store/src/rollups/mod.rs @@ -105,7 +105,7 @@ impl Store { r#" SELECT COALESCE(SUM(total_tokens), 0), - COUNT(*) + COALESCE(SUM(COALESCE(CAST(json_extract(payload, '$.usage.requests') AS INTEGER), 1)), 0) FROM usage_events WHERE started_at >= ?1 "#, diff --git a/crates/statsai-store/src/tests/events.rs b/crates/statsai-store/src/tests/events.rs index 1d2a2d5..8a005d0 100644 --- a/crates/statsai-store/src/tests/events.rs +++ b/crates/statsai-store/src/tests/events.rs @@ -348,7 +348,7 @@ fn insert_events_batches_in_one_transaction() { } #[test] -fn usage_event_period_stats_since_counts_recent_events() { +fn usage_event_period_stats_since_counts_requests_inside_recent_events() { let store = Store::in_memory().expect("store"); let source = statsai_core::SourceLocation::local_adapter( "codex", @@ -359,15 +359,17 @@ fn usage_event_period_stats_since_counts_recent_events() { ); store.upsert_source(&source).expect("source"); let now = Utc::now(); - let recent = test_store_event(&source, now - chrono::Duration::minutes(5), "recent"); - let old = test_store_event(&source, now - chrono::Duration::days(2), "old"); + let mut recent = test_store_event(&source, now - chrono::Duration::minutes(5), "recent"); + recent.usage.requests = Some(5); + let mut old = test_store_event(&source, now - chrono::Duration::days(2), "old"); + old.usage.requests = Some(2); store.insert_events(&[recent, old]).expect("insert events"); let stats = store .usage_event_period_stats_since(now - chrono::Duration::hours(1)) .expect("period stats"); - assert_eq!(stats.requests, 1); + assert_eq!(stats.requests, 5); assert_eq!(stats.tokens, 15); } diff --git a/crates/statsai-store/src/tests/rollups/build.rs b/crates/statsai-store/src/tests/rollups/build.rs index 43ed05d..8e58c2d 100644 --- a/crates/statsai-store/src/tests/rollups/build.rs +++ b/crates/statsai-store/src/tests/rollups/build.rs @@ -1,5 +1,43 @@ use super::*; +#[test] +fn sync_rollup_counts_underlying_requests_in_codex_turn_events() { + let source = SourceLocation::local_adapter( + "codex", + "test", + "0", + Path::new("/tmp/codex-multi-request-rollup"), + LocationOrigin::Configured, + ); + let day = Utc + .with_ymd_and_hms(2026, 9, 22, 9, 0, 0) + .single() + .expect("day"); + let mut first = test_store_event(&source, day, "turn-a"); + first.model = Some(ModelInfo { + normalized_name: Some("gpt-6-sol".to_string()), + ..ModelInfo::default() + }); + first.usage.requests = Some(3); + + let mut second = test_store_event(&source, day + chrono::Duration::minutes(1), "turn-b"); + second.model = first.model.clone(); + second.usage.requests = Some(5); + + let mut legacy = test_store_event(&source, day + chrono::Duration::minutes(2), "legacy"); + legacy.model = Some(ModelInfo { + normalized_name: Some("gpt-5.4".to_string()), + ..ModelInfo::default() + }); + + let summary = build_sync_rollup_summary(&[first, second, legacy]); + + assert_eq!(summary.usage.requests, Some(9)); + assert_eq!(summary.models.len(), 2); + assert_eq!(summary.models[0].usage.requests, Some(1)); + assert_eq!(summary.models[1].usage.requests, Some(8)); +} + #[test] fn sync_rollups_export_path_only_project_metadata() { let store = Store::in_memory().expect("store"); diff --git a/crates/statsai-store/src/tests/rollups/mod.rs b/crates/statsai-store/src/tests/rollups/mod.rs index 2bfa43d..ec2a2be 100644 --- a/crates/statsai-store/src/tests/rollups/mod.rs +++ b/crates/statsai-store/src/tests/rollups/mod.rs @@ -174,7 +174,7 @@ fn dirty_sync_rollups_rebuild_stale_summary_versions() { output_tokens: Some(12), reasoning_tokens: Some(3), total_tokens: Some(15), - requests: Some(1), + requests: Some(5), ..UsageCounts::default() }; event.runtime = Some(statsai_core::RuntimeInfo { @@ -203,7 +203,7 @@ fn dirty_sync_rollups_rebuild_stale_summary_versions() { store .conn .execute( - "UPDATE sync_rollups SET payload = json_set(payload, '$.metadata.summary_version', '3'), dirty = 0", + "UPDATE sync_rollups SET payload = json_set(payload, '$.metadata.summary_version', '13', '$.usage.requests', 1), dirty = 0", [], ) .expect("downgrade payload version"); @@ -216,6 +216,7 @@ fn dirty_sync_rollups_rebuild_stale_summary_versions() { rebuilt[0].metadata.summary_version.as_deref(), Some(SYNC_ROLLUP_SUMMARY_VERSION) ); + assert_eq!(rebuilt[0].usage.requests, Some(5)); let metrics = rebuilt[0].metrics.as_ref().expect("metrics"); assert_eq!(metrics.tracked_requests, Some(1)); assert_eq!(metrics.tracked_output_tokens, Some(12));