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    /// Seven-day rolling window covering all models.
64    SevenDay,
65    /// Seven-day rolling window scoped to Opus usage.
66    SevenDayOpus,
67    /// Seven-day rolling window scoped to Sonnet usage.
68    SevenDaySonnet,
69    /// Seven-day rolling window for models included in overage billing.
70    SevenDayOverageIncluded,
71    /// Overage (extra usage / usage credits) window.
72    Overage,
73    /// A window type not yet known to this version of the crate.
74    Unknown(String),
75}
76
77impl RateLimitWindow {
78    pub fn as_str(&self) -> &str {
79        match self {
80            Self::FiveHour => "five_hour",
81            Self::SevenDay => "seven_day",
82            Self::SevenDayOpus => "seven_day_opus",
83            Self::SevenDaySonnet => "seven_day_sonnet",
84            Self::SevenDayOverageIncluded => "seven_day_overage_included",
85            Self::Overage => "overage",
86            Self::Unknown(s) => s.as_str(),
87        }
88    }
89}
90
91impl fmt::Display for RateLimitWindow {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        f.write_str(self.as_str())
94    }
95}
96
97impl From<&str> for RateLimitWindow {
98    fn from(s: &str) -> Self {
99        match s {
100            "five_hour" => Self::FiveHour,
101            "seven_day" => Self::SevenDay,
102            "seven_day_opus" => Self::SevenDayOpus,
103            "seven_day_sonnet" => Self::SevenDaySonnet,
104            "seven_day_overage_included" => Self::SevenDayOverageIncluded,
105            "overage" => Self::Overage,
106            other => Self::Unknown(other.to_string()),
107        }
108    }
109}
110
111impl Serialize for RateLimitWindow {
112    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
113        serializer.serialize_str(self.as_str())
114    }
115}
116
117impl<'de> Deserialize<'de> for RateLimitWindow {
118    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
119        let s = String::deserialize(deserializer)?;
120        Ok(Self::from(s.as_str()))
121    }
122}
123
124/// Whether overage billing was accepted or rejected.
125#[derive(Debug, Clone, PartialEq, Eq, Hash)]
126pub enum OverageStatus {
127    /// Overage was accepted.
128    Allowed,
129    /// Overage was accepted but usage is approaching the cap.
130    AllowedWarning,
131    /// Overage was rejected.
132    Rejected,
133    /// A status not yet known to this version of the crate.
134    Unknown(String),
135}
136
137impl OverageStatus {
138    pub fn as_str(&self) -> &str {
139        match self {
140            Self::Allowed => "allowed",
141            Self::AllowedWarning => "allowed_warning",
142            Self::Rejected => "rejected",
143            Self::Unknown(s) => s.as_str(),
144        }
145    }
146}
147
148impl fmt::Display for OverageStatus {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        f.write_str(self.as_str())
151    }
152}
153
154impl From<&str> for OverageStatus {
155    fn from(s: &str) -> Self {
156        match s {
157            "allowed" => Self::Allowed,
158            "allowed_warning" => Self::AllowedWarning,
159            "rejected" => Self::Rejected,
160            other => Self::Unknown(other.to_string()),
161        }
162    }
163}
164
165impl Serialize for OverageStatus {
166    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
167        serializer.serialize_str(self.as_str())
168    }
169}
170
171impl<'de> Deserialize<'de> for OverageStatus {
172    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
173        let s = String::deserialize(deserializer)?;
174        Ok(Self::from(s.as_str()))
175    }
176}
177
178/// Why overage billing is disabled.
179#[derive(Debug, Clone, PartialEq, Eq, Hash)]
180pub enum OverageDisabledReason {
181    /// Overage has not been provisioned for the account.
182    OverageNotProvisioned,
183    /// Overage is disabled at the organization level.
184    OrgLevelDisabled,
185    /// Overage is disabled at the organization level until a later time.
186    OrgLevelDisabledUntil,
187    /// The account is out of credits.
188    OutOfCredits,
189    /// Overage is disabled for this seat tier.
190    SeatTierLevelDisabled,
191    /// Overage is disabled for this member.
192    MemberLevelDisabled,
193    /// The seat tier has a zero credit limit.
194    SeatTierZeroCreditLimit,
195    /// The member's group has a zero credit limit.
196    GroupZeroCreditLimit,
197    /// The member has a zero credit limit.
198    MemberZeroCreditLimit,
199    /// Overage is disabled at the organization service level.
200    OrgServiceLevelDisabled,
201    /// No overage limits are configured.
202    NoLimitsConfigured,
203    /// The server failed to fetch the overage configuration.
204    FetchError,
205    /// A reason not yet known to this version of the crate (including the
206    /// server's own literal `"unknown"` value).
207    Unknown(String),
208}
209
210impl OverageDisabledReason {
211    pub fn as_str(&self) -> &str {
212        match self {
213            Self::OverageNotProvisioned => "overage_not_provisioned",
214            Self::OrgLevelDisabled => "org_level_disabled",
215            Self::OrgLevelDisabledUntil => "org_level_disabled_until",
216            Self::OutOfCredits => "out_of_credits",
217            Self::SeatTierLevelDisabled => "seat_tier_level_disabled",
218            Self::MemberLevelDisabled => "member_level_disabled",
219            Self::SeatTierZeroCreditLimit => "seat_tier_zero_credit_limit",
220            Self::GroupZeroCreditLimit => "group_zero_credit_limit",
221            Self::MemberZeroCreditLimit => "member_zero_credit_limit",
222            Self::OrgServiceLevelDisabled => "org_service_level_disabled",
223            Self::NoLimitsConfigured => "no_limits_configured",
224            Self::FetchError => "fetch_error",
225            Self::Unknown(s) => s.as_str(),
226        }
227    }
228}
229
230impl fmt::Display for OverageDisabledReason {
231    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232        f.write_str(self.as_str())
233    }
234}
235
236impl From<&str> for OverageDisabledReason {
237    fn from(s: &str) -> Self {
238        match s {
239            "overage_not_provisioned" => Self::OverageNotProvisioned,
240            "org_level_disabled" => Self::OrgLevelDisabled,
241            "org_level_disabled_until" => Self::OrgLevelDisabledUntil,
242            "out_of_credits" => Self::OutOfCredits,
243            "seat_tier_level_disabled" => Self::SeatTierLevelDisabled,
244            "member_level_disabled" => Self::MemberLevelDisabled,
245            "seat_tier_zero_credit_limit" => Self::SeatTierZeroCreditLimit,
246            "group_zero_credit_limit" => Self::GroupZeroCreditLimit,
247            "member_zero_credit_limit" => Self::MemberZeroCreditLimit,
248            "org_service_level_disabled" => Self::OrgServiceLevelDisabled,
249            "no_limits_configured" => Self::NoLimitsConfigured,
250            "fetch_error" => Self::FetchError,
251            other => Self::Unknown(other.to_string()),
252        }
253    }
254}
255
256impl Serialize for OverageDisabledReason {
257    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
258        serializer.serialize_str(self.as_str())
259    }
260}
261
262impl<'de> Deserialize<'de> for OverageDisabledReason {
263    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
264        let s = String::deserialize(deserializer)?;
265        Ok(Self::from(s.as_str()))
266    }
267}
268
269/// Rate limit event from Claude CLI.
270///
271/// Sent periodically to inform consumers about current rate limit status,
272/// including overage eligibility and reset timing.
273///
274/// # Example JSON
275///
276/// ```json
277/// {
278///   "type": "rate_limit_event",
279///   "rate_limit_info": {
280///     "status": "allowed",
281///     "resetsAt": 1771390800,
282///     "rateLimitType": "five_hour",
283///     "overageStatus": "rejected",
284///     "overageDisabledReason": "org_level_disabled",
285///     "isUsingOverage": false
286///   },
287///   "uuid": "76258cfb-0dc8-4d4b-8682-77082b59c03f",
288///   "session_id": "1ae0af5b-89fa-4075-8156-d5d3702f6505"
289/// }
290/// ```
291///
292/// # Example
293///
294/// ```
295/// use claude_codes::ClaudeOutput;
296///
297/// 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"}"#;
298/// let output: ClaudeOutput = serde_json::from_str(json).unwrap();
299///
300/// if let Some(evt) = output.as_rate_limit_event() {
301///     println!("Rate limit status: {}", evt.rate_limit_info.status);
302///     if let Some(resets_at) = evt.rate_limit_info.resets_at {
303///         println!("Resets at: {}", resets_at);
304///     }
305/// }
306/// ```
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct RateLimitEvent {
309    /// Rate limit status details
310    pub rate_limit_info: RateLimitInfo,
311    /// Session identifier
312    #[serde(alias = "sessionId")]
313    pub session_id: String,
314    /// Unique identifier for this message
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub uuid: Option<String>,
317}
318
319/// Rate limit status information.
320#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct RateLimitInfo {
322    /// Current rate limit status
323    pub status: RateLimitStatus,
324    /// Unix timestamp when the rate limit resets
325    #[serde(rename = "resetsAt", skip_serializing_if = "Option::is_none")]
326    pub resets_at: Option<u64>,
327    /// Type of rate limit window
328    #[serde(rename = "rateLimitType", skip_serializing_if = "Option::is_none")]
329    pub rate_limit_type: Option<RateLimitWindow>,
330    /// Utilization of the rate limit (0.0 to 1.0)
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub utilization: Option<f64>,
333    /// Overage status (e.g., rejected, allowed)
334    #[serde(skip_serializing_if = "Option::is_none", rename = "overageStatus")]
335    pub overage_status: Option<OverageStatus>,
336    /// Unix timestamp when the overage window resets
337    #[serde(rename = "overageResetsAt", skip_serializing_if = "Option::is_none")]
338    pub overage_resets_at: Option<u64>,
339    /// Reason overage is disabled, if applicable
340    #[serde(
341        rename = "overageDisabledReason",
342        skip_serializing_if = "Option::is_none"
343    )]
344    pub overage_disabled_reason: Option<OverageDisabledReason>,
345    /// Whether overage billing is active for the current request
346    #[serde(rename = "isUsingOverage", skip_serializing_if = "Option::is_none")]
347    pub is_using_overage: Option<bool>,
348    /// Whether overage billing is in use for the account
349    #[serde(rename = "overageInUse", skip_serializing_if = "Option::is_none")]
350    pub overage_in_use: Option<bool>,
351    /// Utilization warning threshold that was crossed (0.0 to 1.0)
352    #[serde(rename = "surpassedThreshold", skip_serializing_if = "Option::is_none")]
353    pub surpassed_threshold: Option<f64>,
354    /// Monthly service spend-cap utilization (Claude-in-Slack surface)
355    #[serde(
356        rename = "overagePeriodMonthly",
357        skip_serializing_if = "Option::is_none"
358    )]
359    pub overage_period_monthly: Option<OveragePeriodUtilization>,
360    /// Per-channel spend-cap utilization (Claude-in-Slack surface)
361    #[serde(
362        rename = "overagePeriodChannel",
363        skip_serializing_if = "Option::is_none"
364    )]
365    pub overage_period_channel: Option<OveragePeriodUtilization>,
366    /// Error code attached when a request was refused outright
367    #[serde(rename = "errorCode", skip_serializing_if = "Option::is_none")]
368    pub error_code: Option<RateLimitErrorCode>,
369    /// Whether the user is able to purchase credits themselves
370    #[serde(
371        rename = "canUserPurchaseCredits",
372        skip_serializing_if = "Option::is_none"
373    )]
374    pub can_user_purchase_credits: Option<bool>,
375    /// Whether the account has a chargeable saved payment method
376    #[serde(
377        rename = "hasChargeableSavedPaymentMethod",
378        skip_serializing_if = "Option::is_none"
379    )]
380    pub has_chargeable_saved_payment_method: Option<bool>,
381}
382
383/// Spend-cap utilization for an overage billing period.
384#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
385pub struct OveragePeriodUtilization {
386    /// Fraction of the spend cap consumed (0.0 to 1.0)
387    pub utilization: f64,
388}
389
390/// Error code carried on a rate limit event when a request was refused.
391#[derive(Debug, Clone, PartialEq, Eq, Hash)]
392pub enum RateLimitErrorCode {
393    /// The request requires purchasing usage credits.
394    CreditsRequired,
395    /// An error code not yet known to this version of the crate.
396    Unknown(String),
397}
398
399impl RateLimitErrorCode {
400    pub fn as_str(&self) -> &str {
401        match self {
402            Self::CreditsRequired => "credits_required",
403            Self::Unknown(s) => s.as_str(),
404        }
405    }
406}
407
408impl fmt::Display for RateLimitErrorCode {
409    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
410        f.write_str(self.as_str())
411    }
412}
413
414impl From<&str> for RateLimitErrorCode {
415    fn from(s: &str) -> Self {
416        match s {
417            "credits_required" => Self::CreditsRequired,
418            other => Self::Unknown(other.to_string()),
419        }
420    }
421}
422
423impl Serialize for RateLimitErrorCode {
424    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
425        serializer.serialize_str(self.as_str())
426    }
427}
428
429impl<'de> Deserialize<'de> for RateLimitErrorCode {
430    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
431        let s = String::deserialize(deserializer)?;
432        Ok(Self::from(s.as_str()))
433    }
434}
435
436#[cfg(test)]
437mod tests {
438    use super::{
439        OverageDisabledReason, OveragePeriodUtilization, OverageStatus, RateLimitErrorCode,
440        RateLimitStatus, RateLimitWindow,
441    };
442    use crate::io::ClaudeOutput;
443
444    #[test]
445    fn test_deserialize_rate_limit_event() {
446        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"}"#;
447
448        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
449        assert!(output.is_rate_limit_event());
450        assert_eq!(output.message_type(), "rate_limit_event");
451        assert_eq!(
452            output.session_id(),
453            Some("1ae0af5b-89fa-4075-8156-d5d3702f6505")
454        );
455
456        let evt = output.as_rate_limit_event().unwrap();
457        assert_eq!(evt.rate_limit_info.status, RateLimitStatus::Allowed);
458        assert_eq!(evt.rate_limit_info.resets_at, Some(1771390800));
459        assert_eq!(
460            evt.rate_limit_info.rate_limit_type,
461            Some(RateLimitWindow::FiveHour)
462        );
463        assert_eq!(evt.rate_limit_info.utilization, None);
464        assert_eq!(
465            evt.rate_limit_info.overage_status,
466            Some(OverageStatus::Rejected)
467        );
468        assert_eq!(
469            evt.rate_limit_info.overage_disabled_reason,
470            Some(OverageDisabledReason::OrgLevelDisabled)
471        );
472        assert_eq!(evt.rate_limit_info.is_using_overage, Some(false));
473        assert_eq!(
474            evt.uuid,
475            Some("76258cfb-0dc8-4d4b-8682-77082b59c03f".to_string())
476        );
477    }
478
479    #[test]
480    fn test_deserialize_rate_limit_event_minimal() {
481        let json = r#"{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"session_id":"abc"}"#;
482
483        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
484        let evt = output.as_rate_limit_event().unwrap();
485        assert_eq!(evt.rate_limit_info.overage_disabled_reason, None);
486        assert_eq!(evt.rate_limit_info.is_using_overage, None);
487        assert!(evt.uuid.is_none());
488    }
489
490    #[test]
491    fn test_deserialize_rate_limit_event_allowed_warning() {
492        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"}"#;
493
494        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
495        let evt = output.as_rate_limit_event().unwrap();
496        assert_eq!(evt.rate_limit_info.status, RateLimitStatus::AllowedWarning);
497        assert_eq!(evt.rate_limit_info.utilization, Some(0.85));
498        assert_eq!(evt.rate_limit_info.overage_status, None);
499        assert_eq!(evt.rate_limit_info.overage_disabled_reason, None);
500        assert_eq!(evt.rate_limit_info.is_using_overage, Some(false));
501    }
502
503    #[test]
504    fn test_deserialize_rate_limit_event_no_resets_at() {
505        let json = r#"{"type":"rate_limit_event","rate_limit_info":{"status":"allowed","isUsingOverage":false},"uuid":"4269273d-3b5e-40ae-9765-cb3c12284c44","session_id":"f9626cf7-4d88-4844-9bb1-cab96909fc7b"}"#;
506
507        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
508        let evt = output.as_rate_limit_event().unwrap();
509        assert_eq!(evt.rate_limit_info.status, RateLimitStatus::Allowed);
510        assert_eq!(evt.rate_limit_info.resets_at, None);
511        assert_eq!(evt.rate_limit_info.rate_limit_type, None);
512        assert_eq!(evt.rate_limit_info.is_using_overage, Some(false));
513    }
514
515    #[test]
516    fn test_deserialize_rate_limit_event_rejected() {
517        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"}"#;
518
519        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
520        let evt = output.as_rate_limit_event().unwrap();
521        assert_eq!(evt.rate_limit_info.status, RateLimitStatus::Rejected);
522        assert_eq!(
523            evt.rate_limit_info.rate_limit_type,
524            Some(RateLimitWindow::SevenDay)
525        );
526        assert_eq!(
527            evt.rate_limit_info.overage_status,
528            Some(OverageStatus::Rejected)
529        );
530        assert_eq!(
531            evt.rate_limit_info.overage_disabled_reason,
532            Some(OverageDisabledReason::OutOfCredits)
533        );
534    }
535
536    #[test]
537    fn test_deserialize_rate_limit_event_full_2_1_205() {
538        let json = r#"{"type":"rate_limit_event","rate_limit_info":{"status":"rejected","resetsAt":1700003600,"rateLimitType":"seven_day_overage_included","utilization":0.97,"overageStatus":"allowed_warning","overageResetsAt":1700007200,"overageDisabledReason":"seat_tier_zero_credit_limit","isUsingOverage":true,"overageInUse":true,"surpassedThreshold":0.8,"overagePeriodMonthly":{"utilization":0.5},"overagePeriodChannel":{"utilization":0.25},"errorCode":"credits_required","canUserPurchaseCredits":true,"hasChargeableSavedPaymentMethod":false},"session_id":"abc"}"#;
539
540        let output: ClaudeOutput = serde_json::from_str(json).unwrap();
541        let evt = output.as_rate_limit_event().unwrap();
542        let info = &evt.rate_limit_info;
543        assert_eq!(
544            info.rate_limit_type,
545            Some(RateLimitWindow::SevenDayOverageIncluded)
546        );
547        assert_eq!(info.overage_status, Some(OverageStatus::AllowedWarning));
548        assert_eq!(info.overage_resets_at, Some(1700007200));
549        assert_eq!(
550            info.overage_disabled_reason,
551            Some(OverageDisabledReason::SeatTierZeroCreditLimit)
552        );
553        assert_eq!(info.is_using_overage, Some(true));
554        assert_eq!(info.overage_in_use, Some(true));
555        assert_eq!(info.surpassed_threshold, Some(0.8));
556        assert_eq!(
557            info.overage_period_monthly,
558            Some(OveragePeriodUtilization { utilization: 0.5 })
559        );
560        assert_eq!(
561            info.overage_period_channel,
562            Some(OveragePeriodUtilization { utilization: 0.25 })
563        );
564        assert_eq!(info.error_code, Some(RateLimitErrorCode::CreditsRequired));
565        assert_eq!(info.can_user_purchase_credits, Some(true));
566        assert_eq!(info.has_chargeable_saved_payment_method, Some(false));
567
568        let round_trip = serde_json::to_value(output).unwrap();
569        let original: serde_json::Value = serde_json::from_str(json).unwrap();
570        assert_eq!(round_trip, original);
571    }
572
573    #[test]
574    fn test_rate_limit_window_round_trip() {
575        for s in [
576            "five_hour",
577            "seven_day",
578            "seven_day_opus",
579            "seven_day_sonnet",
580            "seven_day_overage_included",
581            "overage",
582            "some_future_window",
583        ] {
584            assert_eq!(RateLimitWindow::from(s).as_str(), s);
585        }
586    }
587
588    #[test]
589    fn test_overage_disabled_reason_round_trip() {
590        for s in [
591            "overage_not_provisioned",
592            "org_level_disabled",
593            "org_level_disabled_until",
594            "out_of_credits",
595            "seat_tier_level_disabled",
596            "member_level_disabled",
597            "seat_tier_zero_credit_limit",
598            "group_zero_credit_limit",
599            "member_zero_credit_limit",
600            "org_service_level_disabled",
601            "no_limits_configured",
602            "fetch_error",
603            "unknown",
604        ] {
605            assert_eq!(OverageDisabledReason::from(s).as_str(), s);
606        }
607    }
608}