Skip to main content

seher/codex/
types.rs

1use chrono::{DateTime, TimeZone, Utc};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Deserialize, Serialize, Clone)]
5pub struct CodexWindow {
6    pub used_percent: f64,
7    pub limit_window_seconds: i64,
8    pub reset_after_seconds: i64,
9    pub reset_at: i64,
10}
11
12impl CodexWindow {
13    pub fn is_limited(&self) -> bool {
14        self.used_percent >= 100.0
15    }
16
17    pub fn reset_at_datetime(&self) -> Option<DateTime<Utc>> {
18        Utc.timestamp_opt(self.reset_at, 0).single()
19    }
20}
21
22#[derive(Debug, Deserialize, Serialize, Clone)]
23pub struct CodexRateLimit {
24    pub allowed: bool,
25    pub limit_reached: bool,
26    pub primary_window: Option<CodexWindow>,
27    pub secondary_window: Option<CodexWindow>,
28}
29
30impl CodexRateLimit {
31    pub fn is_limited(&self) -> bool {
32        !self.allowed
33            || self.limit_reached
34            || [self.primary_window.as_ref(), self.secondary_window.as_ref()]
35                .into_iter()
36                .flatten()
37                .any(CodexWindow::is_limited)
38    }
39
40    pub fn next_reset_time(&self) -> Option<DateTime<Utc>> {
41        [self.primary_window.as_ref(), self.secondary_window.as_ref()]
42            .into_iter()
43            .flatten()
44            .filter(|window| !self.allowed || self.limit_reached || window.is_limited())
45            .filter_map(CodexWindow::reset_at_datetime)
46            .max()
47    }
48}
49
50#[derive(Debug, Deserialize, Serialize)]
51pub struct CodexCredits {
52    pub has_credits: bool,
53    pub unlimited: bool,
54    pub balance: String,
55    pub approx_local_messages: Vec<i64>,
56    pub approx_cloud_messages: Vec<i64>,
57}
58
59#[derive(Debug, Deserialize, Serialize)]
60pub struct CodexUsageResponse {
61    pub user_id: String,
62    pub account_id: String,
63    pub email: String,
64    pub plan_type: String,
65    pub rate_limit: CodexRateLimit,
66    pub code_review_rate_limit: CodexRateLimit,
67    pub additional_rate_limits: Option<serde_json::Value>,
68    pub credits: CodexCredits,
69    pub promo: Option<serde_json::Value>,
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn parses_usage_response_and_keeps_unlimited_state() {
78        let json = r#"
79        {
80          "user_id": "user-1",
81          "account_id": "user-1",
82          "email": "user@example.com",
83          "plan_type": "plus",
84          "rate_limit": {
85            "allowed": true,
86            "limit_reached": false,
87            "primary_window": {
88              "used_percent": 6,
89              "limit_window_seconds": 18000,
90              "reset_after_seconds": 13837,
91              "reset_at": 1773200619
92            },
93            "secondary_window": {
94              "used_percent": 2,
95              "limit_window_seconds": 604800,
96              "reset_after_seconds": 600637,
97              "reset_at": 1773787419
98            }
99          },
100          "code_review_rate_limit": {
101            "allowed": true,
102            "limit_reached": false,
103            "primary_window": {
104              "used_percent": 0,
105              "limit_window_seconds": 604800,
106              "reset_after_seconds": 604800,
107              "reset_at": 1773791583
108            },
109            "secondary_window": null
110          },
111          "additional_rate_limits": null,
112          "credits": {
113            "has_credits": false,
114            "unlimited": false,
115            "balance": "0",
116            "approx_local_messages": [0, 0],
117            "approx_cloud_messages": [0, 0]
118          },
119          "promo": null
120        }"#;
121
122        let usage: CodexUsageResponse = serde_json::from_str(json).unwrap();
123
124        assert!(!usage.rate_limit.is_limited());
125        assert_eq!(usage.rate_limit.next_reset_time(), None);
126        assert_eq!(
127            usage
128                .code_review_rate_limit
129                .primary_window
130                .unwrap()
131                .used_percent,
132            0.0
133        );
134    }
135
136    #[test]
137    fn picks_latest_reset_when_multiple_windows_are_limited() {
138        let limit = CodexRateLimit {
139            allowed: false,
140            limit_reached: true,
141            primary_window: Some(CodexWindow {
142                used_percent: 100.0,
143                limit_window_seconds: 18000,
144                reset_after_seconds: 100,
145                reset_at: 1_773_200_619,
146            }),
147            secondary_window: Some(CodexWindow {
148                used_percent: 100.0,
149                limit_window_seconds: 604800,
150                reset_after_seconds: 200,
151                reset_at: 1_773_787_419,
152            }),
153        };
154
155        assert!(limit.is_limited());
156        assert_eq!(
157            limit.next_reset_time().map(|time| time.timestamp()),
158            Some(1_773_787_419)
159        );
160    }
161
162    #[test]
163    fn uses_reset_time_when_allowed_flag_blocks_usage_before_window_hits_100_percent() {
164        let limit = CodexRateLimit {
165            allowed: false,
166            limit_reached: false,
167            primary_window: Some(CodexWindow {
168                used_percent: 85.0,
169                limit_window_seconds: 18000,
170                reset_after_seconds: 100,
171                reset_at: 1_773_200_619,
172            }),
173            secondary_window: Some(CodexWindow {
174                used_percent: 20.0,
175                limit_window_seconds: 604800,
176                reset_after_seconds: 200,
177                reset_at: 1_773_787_419,
178            }),
179        };
180
181        assert!(limit.is_limited());
182        assert_eq!(
183            limit.next_reset_time().map(|time| time.timestamp()),
184            Some(1_773_787_419)
185        );
186    }
187}