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
7 changes: 4 additions & 3 deletions crates/statsai-store/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
9 changes: 5 additions & 4 deletions crates/statsai-store/src/rollups/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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,
},
Expand Down
2 changes: 1 addition & 1 deletion crates/statsai-store/src/rollups/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
"#,
Expand Down
10 changes: 6 additions & 4 deletions crates/statsai-store/src/tests/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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);
}

Expand Down
38 changes: 38 additions & 0 deletions crates/statsai-store/src/tests/rollups/build.rs
Original file line number Diff line number Diff line change
@@ -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");
Expand Down
5 changes: 3 additions & 2 deletions crates/statsai-store/src/tests/rollups/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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");
Expand All @@ -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));
Expand Down
Loading