Skip to main content

claude_codes/io/
rate_limit.rs

1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use std::fmt;
3
4/// Current rate limit disposition.
5#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6pub enum RateLimitStatus {
7    /// Request is within limits.
8    Allowed,
9    /// Request is within limits but approaching the cap.
10    AllowedWarning,
11    /// Request was rejected due to rate limiting.
12    Rejected,
13    /// A status not yet known to this version of the crate.
14    Unknown(String),
15}
16
17impl RateLimitStatus {
18    pub fn as_str(&self) -> &str {
19        match self {
20            Self::Allowed => "allowed",
21            Self::AllowedWarning => "allowed_warning",
22            Self::Rejected => "rejected",
23            Self::Unknown(s) => s.as_str(),
24        }
25    }
26}
27
28impl fmt::Display for RateLimitStatus {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        f.write_str(self.as_str())
31    }
32}
33
34impl From<&str> for RateLimitStatus {
35    fn from(s: &str) -> Self {
36        match s {
37            "allowed" => Self::Allowed,
38            "allowed_warning" => Self::AllowedWarning,
39            "rejected" => Self::Rejected,
40            other => Self::Unknown(other.to_string()),
41        }
42    }
43}
44
45impl Serialize for RateLimitStatus {
46    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
47        serializer.serialize_str(self.as_str())
48    }
49}
50
51impl<'de> Deserialize<'de> for RateLimitStatus {
52    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
53        let s = String::deserialize(deserializer)?;
54        Ok(Self::from(s.as_str()))
55    }
56}
57
58/// The time window a rate limit applies to.
59#[derive(Debug, Clone, PartialEq, Eq, Hash)]
60pub enum RateLimitWindow {
61    /// Five-hour rolling window.
62    FiveHour,
63    /// Hourly rolling window.
64    Hourly,
65    /// Seven-day rolling window.
66    SevenDay,
67    /// A window type not yet known to this version of the crate.
68    Unknown(String),
69}
70
71impl RateLimitWindow {
72    pub fn as_str(&self) -> &str {
73        match self {
74            Self::FiveHour => "five_hour",
75            Self::Hourly => "hourly",
76            Self::SevenDay => "seven_day",
77            Self::Unknown(s) => s.as_str(),
78        }
79    }
80}
81
82impl fmt::Display for RateLimitWindow {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        f.write_str(self.as_str())
85    }
86}
87
88impl From<&str> for RateLimitWindow {
89    fn from(s: &str) -> Self {
90        match s {
91            "five_hour" => Self::FiveHour,
92            "hourly" => Self::Hourly,
93            "seven_day" => Self::SevenDay,
94            other => Self::Unknown(other.to_string()),
95        }
96    }
97}
98
99impl Serialize for RateLimitWindow {
100    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
101        serializer.serialize_str(self.as_str())
102    }
103}
104
105impl<'de> Deserialize<'de> for RateLimitWindow {
106    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
107        let s = String::deserialize(deserializer)?;
108        Ok(Self::from(s.as_str()))
109    }
110}
111
112/// Whether overage billing was accepted or rejected.
113#[derive(Debug, Clone, PartialEq, Eq, Hash)]
114pub enum OverageStatus {
115    /// Overage was accepted.
116    Allowed,
117    /// Overage was rejected.
118    Rejected,
119    /// A status not yet known to this version of the crate.
120    Unknown(String),
121}
122
123impl OverageStatus {
124    pub fn as_str(&self) -> &str {
125        match self {
126            Self::Allowed => "allowed",
127            Self::Rejected => "rejected",
128            Self::Unknown(s) => s.as_str(),
129        }
130    }
131}
132
133impl fmt::Display for OverageStatus {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        f.write_str(self.as_str())
136    }
137}
138
139impl From<&str> for OverageStatus {
140    fn from(s: &str) -> Self {
141        match s {
142            "allowed" => Self::Allowed,
143            "rejected" => Self::Rejected,
144            other => Self::Unknown(other.to_string()),
145        }
146    }
147}
148
149impl Serialize for OverageStatus {
150    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
151        serializer.serialize_str(self.as_str())
152    }
153}
154
155impl<'de> Deserialize<'de> for OverageStatus {
156    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
157        let s = String::deserialize(deserializer)?;
158        Ok(Self::from(s.as_str()))
159    }
160}
161
162/// Why overage billing is disabled.
163#[derive(Debug, Clone, PartialEq, Eq, Hash)]
164pub enum OverageDisabledReason {
165    /// Overage is disabled at the organization level.
166    OrgLevelDisabled,
167    /// The account is out of credits.
168    OutOfCredits,
169    /// A reason not yet known to this version of the crate.
170    Unknown(String),
171}
172
173impl OverageDisabledReason {
174    pub fn as_str(&self) -> &str {
175        match self {
176            Self::OrgLevelDisabled => "org_level_disabled",
177            Self::OutOfCredits => "out_of_credits",
178            Self::Unknown(s) => s.as_str(),
179        }
180    }
181}
182
183impl fmt::Display for OverageDisabledReason {
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        f.write_str(self.as_str())
186    }
187}
188
189impl From<&str> for OverageDisabledReason {
190    fn from(s: &str) -> Self {
191        match s {
192            "org_level_disabled" => Self::OrgLevelDisabled,
193            "out_of_credits" => Self::OutOfCredits,
194            other => Self::Unknown(other.to_string()),
195        }
196    }
197}
198
199impl Serialize for OverageDisabledReason {
200    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
201        serializer.serialize_str(self.as_str())
202    }
203}
204
205impl<'de> Deserialize<'de> for OverageDisabledReason {
206    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
207        let s = String::deserialize(deserializer)?;
208        Ok(Self::from(s.as_str()))
209    }
210}
211
212/// Rate limit event from Claude CLI.
213///
214/// Sent periodically to inform consumers about current rate limit status,
215/// including overage eligibility and reset timing.
216///
217/// # Example JSON
218///
219/// ```json
220/// {
221///   "type": "rate_limit_event",
222///   "rate_limit_info": {
223///     "status": "allowed",
224///     "resetsAt": 1771390800,
225///     "rateLimitType": "five_hour",
226///     "overageStatus": "rejected",
227///     "overageDisabledReason": "org_level_disabled",
228///     "isUsingOverage": false
229///   },
230///   "uuid": "76258cfb-0dc8-4d4b-8682-77082b59c03f",
231///   "session_id": "1ae0af5b-89fa-4075-8156-d5d3702f6505"
232/// }
233/// ```
234///
235/// # Example
236///
237/// ```
238/// use claude_codes::ClaudeOutput;
239///
240/// let json = r#"{"type":"rate_limit_event","rate_limit_info":{"status":"allowed","resetsAt":1771390800,"rateLimitType":"five_hour","overageStatus":"rejected","overageDisabledReason":"org_level_disabled","isUsingOverage":false},"uuid":"abc","session_id":"def"}"#;
241/// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
242///
243/// if let Some(evt) = output.as_rate_limit_event() {
244///     println!("Rate limit status: {}", evt.rate_limit_info.status);
245///     println!("Resets at: {}", evt.rate_limit_info.resets_at);
246/// }
247/// ```
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub struct RateLimitEvent {
250    /// Rate limit status details
251    pub rate_limit_info: RateLimitInfo,
252    /// Session identifier
253    pub session_id: String,
254    /// Unique identifier for this message
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub uuid: Option<String>,
257}
258
259/// Rate limit status information.
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct RateLimitInfo {
262    /// Current rate limit status
263    pub status: RateLimitStatus,
264    /// Unix timestamp when the rate limit resets
265    #[serde(rename = "resetsAt")]
266    pub resets_at: u64,
267    /// Type of rate limit window
268    #[serde(rename = "rateLimitType")]
269    pub rate_limit_type: RateLimitWindow,
270    /// Utilization of the rate limit (0.0 to 1.0)
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub utilization: Option<f64>,
273    /// Overage status (e.g., rejected, allowed)
274    #[serde(skip_serializing_if = "Option::is_none", rename = "overageStatus")]
275    pub overage_status: Option<OverageStatus>,
276    /// Reason overage is disabled, if applicable
277    #[serde(rename = "overageDisabledReason")]
278    pub overage_disabled_reason: Option<OverageDisabledReason>,
279    /// Whether overage billing is active
280    #[serde(rename = "isUsingOverage")]
281    pub is_using_overage: bool,
282}
283
284#[cfg(test)]
285mod tests {
286    use super::{OverageDisabledReason, OverageStatus, RateLimitStatus, RateLimitWindow};
287    use crate::io::ClaudeOutput;
288
289    #[test]
290    fn test_deserialize_rate_limit_event() {
291        let json = r#"{"type":"rate_limit_event","rate_limit_info":{"status":"allowed","resetsAt":1771390800,"rateLimitType":"five_hour","overageStatus":"rejected","overageDisabledReason":"org_level_disabled","isUsingOverage":false},"uuid":"76258cfb-0dc8-4d4b-8682-77082b59c03f","session_id":"1ae0af5b-89fa-4075-8156-d5d3702f6505"}"#;
292
293        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
294        assert!(output.is_rate_limit_event());
295        assert_eq!(output.message_type(), "rate_limit_event");
296        assert_eq!(
297            output.session_id(),
298            Some("1ae0af5b-89fa-4075-8156-d5d3702f6505")
299        );
300
301        let evt = output.as_rate_limit_event().unwrap();
302        assert_eq!(evt.rate_limit_info.status, RateLimitStatus::Allowed);
303        assert_eq!(evt.rate_limit_info.resets_at, 1771390800);
304        assert_eq!(
305            evt.rate_limit_info.rate_limit_type,
306            RateLimitWindow::FiveHour
307        );
308        assert_eq!(evt.rate_limit_info.utilization, None);
309        assert_eq!(
310            evt.rate_limit_info.overage_status,
311            Some(OverageStatus::Rejected)
312        );
313        assert_eq!(
314            evt.rate_limit_info.overage_disabled_reason,
315            Some(OverageDisabledReason::OrgLevelDisabled)
316        );
317        assert!(!evt.rate_limit_info.is_using_overage);
318        assert_eq!(
319            evt.uuid,
320            Some("76258cfb-0dc8-4d4b-8682-77082b59c03f".to_string())
321        );
322    }
323
324    #[test]
325    fn test_deserialize_rate_limit_event_minimal() {
326        let json = r#"{"type":"rate_limit_event","rate_limit_info":{"status":"allowed","resetsAt":0,"rateLimitType":"hourly","overageStatus":"allowed","isUsingOverage":true},"session_id":"abc"}"#;
327
328        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
329        let evt = output.as_rate_limit_event().unwrap();
330        assert_eq!(evt.rate_limit_info.overage_disabled_reason, None);
331        assert!(evt.rate_limit_info.is_using_overage);
332        assert!(evt.uuid.is_none());
333    }
334
335    #[test]
336    fn test_deserialize_rate_limit_event_allowed_warning() {
337        let json = r#"{"type":"rate_limit_event","rate_limit_info":{"status":"allowed_warning","resetsAt":1700000000,"rateLimitType":"five_hour","utilization":0.85,"isUsingOverage":false},"uuid":"550e8400-e29b-41d4-a716-446655440000","session_id":"test-session-id"}"#;
338
339        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
340        let evt = output.as_rate_limit_event().unwrap();
341        assert_eq!(evt.rate_limit_info.status, RateLimitStatus::AllowedWarning);
342        assert_eq!(evt.rate_limit_info.utilization, Some(0.85));
343        assert_eq!(evt.rate_limit_info.overage_status, None);
344        assert_eq!(evt.rate_limit_info.overage_disabled_reason, None);
345        assert!(!evt.rate_limit_info.is_using_overage);
346    }
347
348    #[test]
349    fn test_deserialize_rate_limit_event_rejected() {
350        let json = r#"{"type":"rate_limit_event","rate_limit_info":{"status":"rejected","resetsAt":1700003600,"rateLimitType":"seven_day","isUsingOverage":false,"overageStatus":"rejected","overageDisabledReason":"out_of_credits"},"uuid":"660e8400-e29b-41d4-a716-446655440001","session_id":"test-session-id"}"#;
351
352        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
353        let evt = output.as_rate_limit_event().unwrap();
354        assert_eq!(evt.rate_limit_info.status, RateLimitStatus::Rejected);
355        assert_eq!(
356            evt.rate_limit_info.rate_limit_type,
357            RateLimitWindow::SevenDay
358        );
359        assert_eq!(
360            evt.rate_limit_info.overage_status,
361            Some(OverageStatus::Rejected)
362        );
363        assert_eq!(
364            evt.rate_limit_info.overage_disabled_reason,
365            Some(OverageDisabledReason::OutOfCredits)
366        );
367    }
368}