1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use std::fmt;
3
4#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6pub enum RateLimitStatus {
7 Allowed,
9 AllowedWarning,
11 Rejected,
13 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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
60pub enum RateLimitWindow {
61 FiveHour,
63 SevenDay,
65 SevenDayOpus,
67 SevenDaySonnet,
69 SevenDayOverageIncluded,
71 Overage,
73 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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
126pub enum OverageStatus {
127 Allowed,
129 AllowedWarning,
131 Rejected,
133 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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
180pub enum OverageDisabledReason {
181 OverageNotProvisioned,
183 OrgLevelDisabled,
185 OrgLevelDisabledUntil,
187 OutOfCredits,
189 SeatTierLevelDisabled,
191 MemberLevelDisabled,
193 SeatTierZeroCreditLimit,
195 GroupZeroCreditLimit,
197 MemberZeroCreditLimit,
199 OrgServiceLevelDisabled,
201 NoLimitsConfigured,
203 FetchError,
205 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#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct RateLimitEvent {
309 pub rate_limit_info: RateLimitInfo,
311 #[serde(alias = "sessionId")]
313 pub session_id: String,
314 #[serde(skip_serializing_if = "Option::is_none")]
316 pub uuid: Option<String>,
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct RateLimitInfo {
322 pub status: RateLimitStatus,
324 #[serde(rename = "resetsAt", skip_serializing_if = "Option::is_none")]
326 pub resets_at: Option<u64>,
327 #[serde(rename = "rateLimitType", skip_serializing_if = "Option::is_none")]
329 pub rate_limit_type: Option<RateLimitWindow>,
330 #[serde(skip_serializing_if = "Option::is_none")]
332 pub utilization: Option<f64>,
333 #[serde(rename = "unifiedWindows", skip_serializing_if = "Option::is_none")]
341 pub unified_windows: Option<UnifiedRateLimitWindows>,
342 #[serde(skip_serializing_if = "Option::is_none", rename = "overageStatus")]
344 pub overage_status: Option<OverageStatus>,
345 #[serde(rename = "overageResetsAt", skip_serializing_if = "Option::is_none")]
347 pub overage_resets_at: Option<u64>,
348 #[serde(
350 rename = "overageDisabledReason",
351 skip_serializing_if = "Option::is_none"
352 )]
353 pub overage_disabled_reason: Option<OverageDisabledReason>,
354 #[serde(rename = "isUsingOverage", skip_serializing_if = "Option::is_none")]
356 pub is_using_overage: Option<bool>,
357 #[serde(rename = "overageInUse", skip_serializing_if = "Option::is_none")]
359 pub overage_in_use: Option<bool>,
360 #[serde(rename = "surpassedThreshold", skip_serializing_if = "Option::is_none")]
362 pub surpassed_threshold: Option<f64>,
363 #[serde(
369 rename = "rateLimitGraceActive",
370 skip_serializing_if = "Option::is_none"
371 )]
372 pub rate_limit_grace_active: Option<bool>,
373 #[serde(
375 rename = "overagePeriodMonthly",
376 skip_serializing_if = "Option::is_none"
377 )]
378 pub overage_period_monthly: Option<OveragePeriodUtilization>,
379 #[serde(
381 rename = "overagePeriodChannel",
382 skip_serializing_if = "Option::is_none"
383 )]
384 pub overage_period_channel: Option<OveragePeriodUtilization>,
385 #[serde(rename = "errorCode", skip_serializing_if = "Option::is_none")]
387 pub error_code: Option<RateLimitErrorCode>,
388 #[serde(
390 rename = "canUserPurchaseCredits",
391 skip_serializing_if = "Option::is_none"
392 )]
393 pub can_user_purchase_credits: Option<bool>,
394 #[serde(
396 rename = "hasChargeableSavedPaymentMethod",
397 skip_serializing_if = "Option::is_none"
398 )]
399 pub has_chargeable_saved_payment_method: Option<bool>,
400}
401
402#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
404pub struct OveragePeriodUtilization {
405 pub utilization: f64,
407}
408
409#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
413pub struct UnifiedRateLimitWindows {
414 #[serde(default, skip_serializing_if = "Option::is_none")]
416 pub five_hour: Option<UnifiedWindowUsage>,
417 #[serde(default, skip_serializing_if = "Option::is_none")]
419 pub seven_day: Option<UnifiedWindowUsage>,
420 #[serde(default, skip_serializing_if = "Option::is_none")]
423 pub seven_day_overage_included: Option<UnifiedWindowUsage>,
424}
425
426#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
429pub struct UnifiedWindowUsage {
430 pub utilization: f64,
433 #[serde(rename = "resetsAt")]
435 pub resets_at: u64,
436}
437
438#[derive(Debug, Clone, PartialEq, Eq, Hash)]
440pub enum RateLimitErrorCode {
441 CreditsRequired,
443 Unknown(String),
445}
446
447impl RateLimitErrorCode {
448 pub fn as_str(&self) -> &str {
449 match self {
450 Self::CreditsRequired => "credits_required",
451 Self::Unknown(s) => s.as_str(),
452 }
453 }
454}
455
456impl fmt::Display for RateLimitErrorCode {
457 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
458 f.write_str(self.as_str())
459 }
460}
461
462impl From<&str> for RateLimitErrorCode {
463 fn from(s: &str) -> Self {
464 match s {
465 "credits_required" => Self::CreditsRequired,
466 other => Self::Unknown(other.to_string()),
467 }
468 }
469}
470
471impl Serialize for RateLimitErrorCode {
472 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
473 serializer.serialize_str(self.as_str())
474 }
475}
476
477impl<'de> Deserialize<'de> for RateLimitErrorCode {
478 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
479 let s = String::deserialize(deserializer)?;
480 Ok(Self::from(s.as_str()))
481 }
482}
483
484#[cfg(test)]
485mod tests {
486 use super::{
487 OverageDisabledReason, OveragePeriodUtilization, OverageStatus, RateLimitErrorCode,
488 RateLimitStatus, RateLimitWindow,
489 };
490 use crate::io::ClaudeOutput;
491
492 #[test]
493 fn test_deserialize_rate_limit_event() {
494 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"}"#;
495
496 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
497 assert!(output.is_rate_limit_event());
498 assert_eq!(output.message_type(), "rate_limit_event");
499 assert_eq!(
500 output.session_id(),
501 Some("1ae0af5b-89fa-4075-8156-d5d3702f6505")
502 );
503
504 let evt = output.as_rate_limit_event().unwrap();
505 assert_eq!(evt.rate_limit_info.status, RateLimitStatus::Allowed);
506 assert_eq!(evt.rate_limit_info.resets_at, Some(1771390800));
507 assert_eq!(
508 evt.rate_limit_info.rate_limit_type,
509 Some(RateLimitWindow::FiveHour)
510 );
511 assert_eq!(evt.rate_limit_info.utilization, None);
512 assert_eq!(
513 evt.rate_limit_info.overage_status,
514 Some(OverageStatus::Rejected)
515 );
516 assert_eq!(
517 evt.rate_limit_info.overage_disabled_reason,
518 Some(OverageDisabledReason::OrgLevelDisabled)
519 );
520 assert_eq!(evt.rate_limit_info.is_using_overage, Some(false));
521 assert_eq!(
522 evt.uuid,
523 Some("76258cfb-0dc8-4d4b-8682-77082b59c03f".to_string())
524 );
525 }
526
527 #[test]
528 fn test_deserialize_rate_limit_event_minimal() {
529 let json = r#"{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"session_id":"abc"}"#;
530
531 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
532 let evt = output.as_rate_limit_event().unwrap();
533 assert_eq!(evt.rate_limit_info.overage_disabled_reason, None);
534 assert_eq!(evt.rate_limit_info.is_using_overage, None);
535 assert!(evt.uuid.is_none());
536 }
537
538 #[test]
539 fn test_deserialize_rate_limit_event_allowed_warning() {
540 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"}"#;
541
542 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
543 let evt = output.as_rate_limit_event().unwrap();
544 assert_eq!(evt.rate_limit_info.status, RateLimitStatus::AllowedWarning);
545 assert_eq!(evt.rate_limit_info.utilization, Some(0.85));
546 assert_eq!(evt.rate_limit_info.overage_status, None);
547 assert_eq!(evt.rate_limit_info.overage_disabled_reason, None);
548 assert_eq!(evt.rate_limit_info.is_using_overage, Some(false));
549 }
550
551 #[test]
552 fn test_deserialize_rate_limit_event_no_resets_at() {
553 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"}"#;
554
555 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
556 let evt = output.as_rate_limit_event().unwrap();
557 assert_eq!(evt.rate_limit_info.status, RateLimitStatus::Allowed);
558 assert_eq!(evt.rate_limit_info.resets_at, None);
559 assert_eq!(evt.rate_limit_info.rate_limit_type, None);
560 assert_eq!(evt.rate_limit_info.is_using_overage, Some(false));
561 }
562
563 #[test]
564 fn test_deserialize_rate_limit_event_rejected() {
565 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"}"#;
566
567 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
568 let evt = output.as_rate_limit_event().unwrap();
569 assert_eq!(evt.rate_limit_info.status, RateLimitStatus::Rejected);
570 assert_eq!(
571 evt.rate_limit_info.rate_limit_type,
572 Some(RateLimitWindow::SevenDay)
573 );
574 assert_eq!(
575 evt.rate_limit_info.overage_status,
576 Some(OverageStatus::Rejected)
577 );
578 assert_eq!(
579 evt.rate_limit_info.overage_disabled_reason,
580 Some(OverageDisabledReason::OutOfCredits)
581 );
582 }
583
584 #[test]
585 fn test_deserialize_rate_limit_event_full_2_1_205() {
586 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"}"#;
587
588 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
589 let evt = output.as_rate_limit_event().unwrap();
590 let info = &evt.rate_limit_info;
591 assert_eq!(
592 info.rate_limit_type,
593 Some(RateLimitWindow::SevenDayOverageIncluded)
594 );
595 assert_eq!(info.overage_status, Some(OverageStatus::AllowedWarning));
596 assert_eq!(info.overage_resets_at, Some(1700007200));
597 assert_eq!(
598 info.overage_disabled_reason,
599 Some(OverageDisabledReason::SeatTierZeroCreditLimit)
600 );
601 assert_eq!(info.is_using_overage, Some(true));
602 assert_eq!(info.overage_in_use, Some(true));
603 assert_eq!(info.surpassed_threshold, Some(0.8));
604 assert_eq!(
605 info.overage_period_monthly,
606 Some(OveragePeriodUtilization { utilization: 0.5 })
607 );
608 assert_eq!(
609 info.overage_period_channel,
610 Some(OveragePeriodUtilization { utilization: 0.25 })
611 );
612 assert_eq!(info.error_code, Some(RateLimitErrorCode::CreditsRequired));
613 assert_eq!(info.can_user_purchase_credits, Some(true));
614 assert_eq!(info.has_chargeable_saved_payment_method, Some(false));
615
616 let round_trip = serde_json::to_value(output).unwrap();
617 let original: serde_json::Value = serde_json::from_str(json).unwrap();
618 assert_eq!(round_trip, original);
619 }
620
621 #[test]
622 fn test_rate_limit_window_round_trip() {
623 for s in [
624 "five_hour",
625 "seven_day",
626 "seven_day_opus",
627 "seven_day_sonnet",
628 "seven_day_overage_included",
629 "overage",
630 "some_future_window",
631 ] {
632 assert_eq!(RateLimitWindow::from(s).as_str(), s);
633 }
634 }
635
636 #[test]
637 fn test_overage_disabled_reason_round_trip() {
638 for s in [
639 "overage_not_provisioned",
640 "org_level_disabled",
641 "org_level_disabled_until",
642 "out_of_credits",
643 "seat_tier_level_disabled",
644 "member_level_disabled",
645 "seat_tier_zero_credit_limit",
646 "group_zero_credit_limit",
647 "member_zero_credit_limit",
648 "org_service_level_disabled",
649 "no_limits_configured",
650 "fetch_error",
651 "unknown",
652 ] {
653 assert_eq!(OverageDisabledReason::from(s).as_str(), s);
654 }
655 }
656}