Skip to main content

simulator_api/
usage.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3
4/// Response body of `GET /usage`.
5#[derive(Debug, Serialize, Deserialize)]
6pub struct UsageReport {
7    pub api_key_name: String,
8    pub since: DateTime<Utc>,
9    pub until: DateTime<Utc>,
10    pub sessions: SessionCounts,
11    pub compute: ComputeTotals,
12    /// Present only for keys with the `build_bundle` feature.
13    #[serde(default, skip_serializing_if = "Option::is_none")]
14    pub bundle_builds: Option<BundleBuildUsage>,
15}
16
17/// Bundle-build usage for the window.
18#[derive(Debug, Default, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
19pub struct BundleBuildUsage {
20    pub requested: u64,
21}
22
23/// Raw `backtest_session_*` point counts for this api key in the window, by
24/// outcome — not session-deduplicated, so the three need not balance (a
25/// pre-start failure has no `started`; a cross-manager handoff re-emits
26/// `started`). Billing uses the compute totals, which derive only from the
27/// terminal completed/failed points.
28#[derive(Debug, Default, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
29pub struct SessionCounts {
30    pub started: u64,
31    pub completed: u64,
32    pub failed: u64,
33}
34
35/// Aggregated compute totals for a usage window.
36#[derive(Debug, Default, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
37pub struct ComputeTotals {
38    pub executed_slot_count: u64,
39    pub session_duration_ms: u64,
40}
41
42impl SessionCounts {
43    pub fn saturating_add(self, other: Self) -> Self {
44        Self {
45            started: self.started.saturating_add(other.started),
46            completed: self.completed.saturating_add(other.completed),
47            failed: self.failed.saturating_add(other.failed),
48        }
49    }
50}
51
52impl ComputeTotals {
53    pub fn saturating_add(self, other: Self) -> Self {
54        Self {
55            executed_slot_count: self
56                .executed_slot_count
57                .saturating_add(other.executed_slot_count),
58            session_duration_ms: self
59                .session_duration_ms
60                .saturating_add(other.session_duration_ms),
61        }
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    fn report(bundle_builds: Option<BundleBuildUsage>) -> UsageReport {
70        UsageReport {
71            api_key_name: "k".to_string(),
72            since: "2026-01-01T00:00:00Z".parse().unwrap(),
73            until: "2026-01-02T00:00:00Z".parse().unwrap(),
74            sessions: SessionCounts::default(),
75            compute: ComputeTotals::default(),
76            bundle_builds,
77        }
78    }
79
80    #[test]
81    fn usage_report_absent_bundle_builds_round_trips_to_none() {
82        let json = serde_json::to_string(&report(None)).unwrap();
83        assert!(!json.contains("bundle_builds"));
84        let parsed: UsageReport = serde_json::from_str(&json).unwrap();
85        assert_eq!(parsed.bundle_builds, None);
86    }
87
88    #[test]
89    fn usage_report_present_bundle_builds_round_trips() {
90        let json = serde_json::to_string(&report(Some(BundleBuildUsage { requested: 3 }))).unwrap();
91        let parsed: UsageReport = serde_json::from_str(&json).unwrap();
92        assert_eq!(
93            parsed.bundle_builds,
94            Some(BundleBuildUsage { requested: 3 })
95        );
96    }
97}