Skip to main content

seher/zai/
types.rs

1use serde::Deserialize;
2
3/// Top-level response from the z.ai quota API.
4///
5/// The structure is intentionally similar to [`crate::glm::types::GlmUsageResponse`]
6/// but kept separate so that field-name drift between the two APIs does not leak
7/// into shared code without an explicit test.
8#[derive(Debug, Deserialize)]
9pub struct ZaiUsageResponse {
10    pub code: i32,
11    pub msg: String,
12    pub data: Option<ZaiQuotaData>,
13    pub success: bool,
14}
15
16#[derive(Debug, Deserialize)]
17pub struct ZaiQuotaData {
18    pub limits: Vec<ZaiLimitRaw>,
19}
20
21impl ZaiQuotaData {
22    /// Returns `true` when any single limit has been exhausted.
23    #[must_use]
24    pub fn is_limited(&self) -> bool {
25        self.limits.iter().any(|l| l.percentage >= 100)
26    }
27}
28
29#[derive(Debug, Deserialize)]
30pub struct ZaiLimitRaw {
31    #[serde(rename = "type")]
32    pub limit_type: String,
33    pub unit: i32,
34    pub number: i32,
35    pub usage: Option<i64>,
36    pub remaining: Option<i64>,
37    pub percentage: i32,
38    #[serde(rename = "nextResetTime")]
39    pub next_reset_time: Option<i64>,
40}
41
42#[cfg(test)]
43#[expect(clippy::unwrap_used)]
44mod tests {
45    use super::*;
46
47    type TestResult = Result<(), Box<dyn std::error::Error>>;
48
49    #[test]
50    fn test_deserialize_full_response() -> TestResult {
51        let json = r#"{
52            "code": 200,
53            "msg": "ok",
54            "data": {
55                "limits": [
56                    {
57                        "type": "TOKENS_LIMIT",
58                        "unit": 3,
59                        "number": 5,
60                        "usage": 40000000,
61                        "remaining": 26371635,
62                        "percentage": 34,
63                        "nextResetTime": 1768507567547
64                    }
65                ]
66            },
67            "success": true
68        }"#;
69
70        let response: ZaiUsageResponse = serde_json::from_str(json)?;
71        assert!(response.success);
72        assert_eq!(response.code, 200);
73
74        let data = response.data.unwrap();
75        assert_eq!(data.limits.len(), 1);
76        assert!(!data.is_limited());
77        Ok(())
78    }
79
80    #[test]
81    fn test_is_limited_when_percentage_100() -> TestResult {
82        let json = r#"{
83            "code": 200,
84            "msg": "ok",
85            "data": {
86                "limits": [
87                    {
88                        "type": "TOKENS_LIMIT",
89                        "unit": 3,
90                        "number": 5,
91                        "usage": 50000000,
92                        "remaining": 0,
93                        "percentage": 100,
94                        "nextResetTime": null
95                    }
96                ]
97            },
98            "success": true
99        }"#;
100
101        let response: ZaiUsageResponse = serde_json::from_str(json)?;
102        assert!(response.data.unwrap().is_limited());
103        Ok(())
104    }
105
106    #[test]
107    fn test_is_not_limited_when_all_below_100() -> TestResult {
108        let json = r#"{
109            "code": 200,
110            "msg": "ok",
111            "data": {
112                "limits": [
113                    {
114                        "type": "TOKENS_LIMIT",
115                        "unit": 3,
116                        "number": 5,
117                        "usage": 10000000,
118                        "remaining": 40000000,
119                        "percentage": 20,
120                        "nextResetTime": 1768507567547
121                    },
122                    {
123                        "type": "REQUESTS_LIMIT",
124                        "unit": 1,
125                        "number": 100,
126                        "usage": 30,
127                        "remaining": 70,
128                        "percentage": 30,
129                        "nextResetTime": null
130                    }
131                ]
132            },
133            "success": true
134        }"#;
135
136        let response: ZaiUsageResponse = serde_json::from_str(json)?;
137        assert!(!response.data.unwrap().is_limited());
138        Ok(())
139    }
140
141    #[test]
142    fn test_error_response() -> TestResult {
143        let json =
144            r#"{"code": 1302, "msg": "rate limit exceeded", "data": null, "success": false}"#;
145        let response: ZaiUsageResponse = serde_json::from_str(json)?;
146        assert!(!response.success);
147        assert_eq!(response.code, 1302);
148        assert!(response.data.is_none());
149        Ok(())
150    }
151
152    #[test]
153    fn test_empty_limits_is_not_limited() -> TestResult {
154        let json = r#"{"code": 200, "msg": "ok", "data": {"limits": []}, "success": true}"#;
155        let response: ZaiUsageResponse = serde_json::from_str(json)?;
156        let data = response.data.unwrap();
157        assert!(!data.is_limited());
158        Ok(())
159    }
160
161    #[test]
162    fn test_next_reset_time_parsed_as_i64() -> TestResult {
163        let json = r#"{
164            "code": 200,
165            "msg": "ok",
166            "data": {
167                "limits": [{
168                    "type": "TOKENS_LIMIT",
169                    "unit": 3,
170                    "number": 5,
171                    "usage": 100,
172                    "remaining": 50,
173                    "percentage": 50,
174                    "nextResetTime": 1768507567547
175                }]
176            },
177            "success": true
178        }"#;
179
180        let response: ZaiUsageResponse = serde_json::from_str(json)?;
181        let limit = &response.data.unwrap().limits[0];
182        assert_eq!(limit.next_reset_time, Some(1_768_507_567_547_i64));
183        Ok(())
184    }
185}