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)]
181pub enum RateLimitScope {
182 Service,
183 Channel,
184 GroupPool,
186 Unknown(String),
188}
189
190impl RateLimitScope {
191 pub fn as_str(&self) -> &str {
192 match self {
193 Self::Service => "service",
194 Self::Channel => "channel",
195 Self::GroupPool => "group_pool",
196 Self::Unknown(s) => s.as_str(),
197 }
198 }
199}
200
201impl fmt::Display for RateLimitScope {
202 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203 f.write_str(self.as_str())
204 }
205}
206
207impl From<&str> for RateLimitScope {
208 fn from(s: &str) -> Self {
209 match s {
210 "service" => Self::Service,
211 "channel" => Self::Channel,
212 "group_pool" => Self::GroupPool,
213 other => Self::Unknown(other.to_string()),
214 }
215 }
216}
217
218impl Serialize for RateLimitScope {
219 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
220 serializer.serialize_str(self.as_str())
221 }
222}
223
224impl<'de> Deserialize<'de> for RateLimitScope {
225 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
226 let s = String::deserialize(deserializer)?;
227 Ok(Self::from(s.as_str()))
228 }
229}
230
231#[derive(Debug, Clone, PartialEq, Eq, Hash)]
233pub enum OverageDisabledReason {
234 OverageNotProvisioned,
236 OrgLevelDisabled,
238 OrgLevelDisabledUntil,
240 OutOfCredits,
242 SeatTierLevelDisabled,
244 MemberLevelDisabled,
246 SeatTierZeroCreditLimit,
248 GroupZeroCreditLimit,
250 MemberZeroCreditLimit,
252 OrgServiceLevelDisabled,
254 NoLimitsConfigured,
256 FetchError,
258 Unknown(String),
261}
262
263impl OverageDisabledReason {
264 pub fn as_str(&self) -> &str {
265 match self {
266 Self::OverageNotProvisioned => "overage_not_provisioned",
267 Self::OrgLevelDisabled => "org_level_disabled",
268 Self::OrgLevelDisabledUntil => "org_level_disabled_until",
269 Self::OutOfCredits => "out_of_credits",
270 Self::SeatTierLevelDisabled => "seat_tier_level_disabled",
271 Self::MemberLevelDisabled => "member_level_disabled",
272 Self::SeatTierZeroCreditLimit => "seat_tier_zero_credit_limit",
273 Self::GroupZeroCreditLimit => "group_zero_credit_limit",
274 Self::MemberZeroCreditLimit => "member_zero_credit_limit",
275 Self::OrgServiceLevelDisabled => "org_service_level_disabled",
276 Self::NoLimitsConfigured => "no_limits_configured",
277 Self::FetchError => "fetch_error",
278 Self::Unknown(s) => s.as_str(),
279 }
280 }
281}
282
283impl fmt::Display for OverageDisabledReason {
284 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285 f.write_str(self.as_str())
286 }
287}
288
289impl From<&str> for OverageDisabledReason {
290 fn from(s: &str) -> Self {
291 match s {
292 "overage_not_provisioned" => Self::OverageNotProvisioned,
293 "org_level_disabled" => Self::OrgLevelDisabled,
294 "org_level_disabled_until" => Self::OrgLevelDisabledUntil,
295 "out_of_credits" => Self::OutOfCredits,
296 "seat_tier_level_disabled" => Self::SeatTierLevelDisabled,
297 "member_level_disabled" => Self::MemberLevelDisabled,
298 "seat_tier_zero_credit_limit" => Self::SeatTierZeroCreditLimit,
299 "group_zero_credit_limit" => Self::GroupZeroCreditLimit,
300 "member_zero_credit_limit" => Self::MemberZeroCreditLimit,
301 "org_service_level_disabled" => Self::OrgServiceLevelDisabled,
302 "no_limits_configured" => Self::NoLimitsConfigured,
303 "fetch_error" => Self::FetchError,
304 other => Self::Unknown(other.to_string()),
305 }
306 }
307}
308
309impl Serialize for OverageDisabledReason {
310 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
311 serializer.serialize_str(self.as_str())
312 }
313}
314
315impl<'de> Deserialize<'de> for OverageDisabledReason {
316 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
317 let s = String::deserialize(deserializer)?;
318 Ok(Self::from(s.as_str()))
319 }
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct RateLimitEvent {
362 pub rate_limit_info: RateLimitInfo,
364 #[serde(alias = "sessionId")]
366 pub session_id: String,
367 #[serde(skip_serializing_if = "Option::is_none")]
369 pub uuid: Option<String>,
370}
371
372#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
374pub struct RateLimitInfo {
375 pub status: RateLimitStatus,
377 #[serde(rename = "resetsAt", skip_serializing_if = "Option::is_none")]
379 pub resets_at: Option<u64>,
380 #[serde(rename = "rateLimitType", skip_serializing_if = "Option::is_none")]
382 pub rate_limit_type: Option<RateLimitWindow>,
383 #[serde(skip_serializing_if = "Option::is_none")]
385 pub utilization: Option<f64>,
386 #[serde(rename = "unifiedWindows", skip_serializing_if = "Option::is_none")]
394 pub unified_windows: Option<UnifiedRateLimitWindows>,
395 #[serde(skip_serializing_if = "Option::is_none", rename = "overageStatus")]
397 pub overage_status: Option<OverageStatus>,
398 #[serde(rename = "overageResetsAt", skip_serializing_if = "Option::is_none")]
400 pub overage_resets_at: Option<u64>,
401 #[serde(
403 rename = "overageDisabledReason",
404 skip_serializing_if = "Option::is_none"
405 )]
406 pub overage_disabled_reason: Option<OverageDisabledReason>,
407 #[serde(rename = "isUsingOverage", skip_serializing_if = "Option::is_none")]
409 pub is_using_overage: Option<bool>,
410 #[serde(rename = "overageInUse", skip_serializing_if = "Option::is_none")]
412 pub overage_in_use: Option<bool>,
413 #[serde(rename = "surpassedThreshold", skip_serializing_if = "Option::is_none")]
415 pub surpassed_threshold: Option<f64>,
416 #[serde(
422 rename = "rateLimitGraceActive",
423 skip_serializing_if = "Option::is_none"
424 )]
425 pub rate_limit_grace_active: Option<bool>,
426 #[serde(
428 rename = "overagePeriodMonthly",
429 skip_serializing_if = "Option::is_none"
430 )]
431 pub overage_period_monthly: Option<OveragePeriodUtilization>,
432 #[serde(
434 rename = "overagePeriodChannel",
435 skip_serializing_if = "Option::is_none"
436 )]
437 pub overage_period_channel: Option<OveragePeriodUtilization>,
438 #[serde(rename = "limitScope", skip_serializing_if = "Option::is_none")]
441 pub limit_scope: Option<RateLimitScope>,
442 #[serde(rename = "errorCode", skip_serializing_if = "Option::is_none")]
444 pub error_code: Option<RateLimitErrorCode>,
445 #[serde(
447 rename = "canUserPurchaseCredits",
448 skip_serializing_if = "Option::is_none"
449 )]
450 pub can_user_purchase_credits: Option<bool>,
451 #[serde(
453 rename = "hasChargeableSavedPaymentMethod",
454 skip_serializing_if = "Option::is_none"
455 )]
456 pub has_chargeable_saved_payment_method: Option<bool>,
457}
458
459#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
461pub struct OveragePeriodUtilization {
462 pub utilization: f64,
464}
465
466#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
470pub struct UnifiedRateLimitWindows {
471 #[serde(default, skip_serializing_if = "Option::is_none")]
473 pub five_hour: Option<UnifiedWindowUsage>,
474 #[serde(default, skip_serializing_if = "Option::is_none")]
476 pub seven_day: Option<UnifiedWindowUsage>,
477 #[serde(default, skip_serializing_if = "Option::is_none")]
480 pub seven_day_overage_included: Option<UnifiedWindowUsage>,
481}
482
483#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
486pub struct UnifiedWindowUsage {
487 pub utilization: f64,
490 #[serde(rename = "resetsAt")]
492 pub resets_at: u64,
493}
494
495#[derive(Debug, Clone, PartialEq, Eq, Hash)]
497pub enum RateLimitErrorCode {
498 CreditsRequired,
500 Unknown(String),
502}
503
504impl RateLimitErrorCode {
505 pub fn as_str(&self) -> &str {
506 match self {
507 Self::CreditsRequired => "credits_required",
508 Self::Unknown(s) => s.as_str(),
509 }
510 }
511}
512
513impl fmt::Display for RateLimitErrorCode {
514 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
515 f.write_str(self.as_str())
516 }
517}
518
519impl From<&str> for RateLimitErrorCode {
520 fn from(s: &str) -> Self {
521 match s {
522 "credits_required" => Self::CreditsRequired,
523 other => Self::Unknown(other.to_string()),
524 }
525 }
526}
527
528impl Serialize for RateLimitErrorCode {
529 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
530 serializer.serialize_str(self.as_str())
531 }
532}
533
534impl<'de> Deserialize<'de> for RateLimitErrorCode {
535 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
536 let s = String::deserialize(deserializer)?;
537 Ok(Self::from(s.as_str()))
538 }
539}
540
541#[cfg(test)]
542mod tests {
543 use super::{
544 OverageDisabledReason, OveragePeriodUtilization, OverageStatus, RateLimitErrorCode,
545 RateLimitStatus, RateLimitWindow,
546 };
547 use crate::io::ClaudeOutput;
548
549 #[test]
550 fn test_deserialize_rate_limit_event() {
551 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"}"#;
552
553 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
554 assert!(output.is_rate_limit_event());
555 assert_eq!(output.message_type(), "rate_limit_event");
556 assert_eq!(
557 output.session_id(),
558 Some("1ae0af5b-89fa-4075-8156-d5d3702f6505")
559 );
560
561 let evt = output.as_rate_limit_event().unwrap();
562 assert_eq!(evt.rate_limit_info.status, RateLimitStatus::Allowed);
563 assert_eq!(evt.rate_limit_info.resets_at, Some(1771390800));
564 assert_eq!(
565 evt.rate_limit_info.rate_limit_type,
566 Some(RateLimitWindow::FiveHour)
567 );
568 assert_eq!(evt.rate_limit_info.utilization, None);
569 assert_eq!(
570 evt.rate_limit_info.overage_status,
571 Some(OverageStatus::Rejected)
572 );
573 assert_eq!(
574 evt.rate_limit_info.overage_disabled_reason,
575 Some(OverageDisabledReason::OrgLevelDisabled)
576 );
577 assert_eq!(evt.rate_limit_info.is_using_overage, Some(false));
578 assert_eq!(
579 evt.uuid,
580 Some("76258cfb-0dc8-4d4b-8682-77082b59c03f".to_string())
581 );
582 }
583
584 #[test]
585 fn test_deserialize_rate_limit_event_minimal() {
586 let json = r#"{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"session_id":"abc"}"#;
587
588 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
589 let evt = output.as_rate_limit_event().unwrap();
590 assert_eq!(evt.rate_limit_info.overage_disabled_reason, None);
591 assert_eq!(evt.rate_limit_info.is_using_overage, None);
592 assert!(evt.uuid.is_none());
593 }
594
595 #[test]
596 fn test_deserialize_rate_limit_event_allowed_warning() {
597 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"}"#;
598
599 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
600 let evt = output.as_rate_limit_event().unwrap();
601 assert_eq!(evt.rate_limit_info.status, RateLimitStatus::AllowedWarning);
602 assert_eq!(evt.rate_limit_info.utilization, Some(0.85));
603 assert_eq!(evt.rate_limit_info.overage_status, None);
604 assert_eq!(evt.rate_limit_info.overage_disabled_reason, None);
605 assert_eq!(evt.rate_limit_info.is_using_overage, Some(false));
606 }
607
608 #[test]
609 fn test_deserialize_rate_limit_event_no_resets_at() {
610 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"}"#;
611
612 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
613 let evt = output.as_rate_limit_event().unwrap();
614 assert_eq!(evt.rate_limit_info.status, RateLimitStatus::Allowed);
615 assert_eq!(evt.rate_limit_info.resets_at, None);
616 assert_eq!(evt.rate_limit_info.rate_limit_type, None);
617 assert_eq!(evt.rate_limit_info.is_using_overage, Some(false));
618 }
619
620 #[test]
621 fn test_deserialize_rate_limit_event_rejected() {
622 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"}"#;
623
624 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
625 let evt = output.as_rate_limit_event().unwrap();
626 assert_eq!(evt.rate_limit_info.status, RateLimitStatus::Rejected);
627 assert_eq!(
628 evt.rate_limit_info.rate_limit_type,
629 Some(RateLimitWindow::SevenDay)
630 );
631 assert_eq!(
632 evt.rate_limit_info.overage_status,
633 Some(OverageStatus::Rejected)
634 );
635 assert_eq!(
636 evt.rate_limit_info.overage_disabled_reason,
637 Some(OverageDisabledReason::OutOfCredits)
638 );
639 }
640
641 #[test]
642 fn test_deserialize_rate_limit_event_full_2_1_205() {
643 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"}"#;
644
645 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
646 let evt = output.as_rate_limit_event().unwrap();
647 let info = &evt.rate_limit_info;
648 assert_eq!(
649 info.rate_limit_type,
650 Some(RateLimitWindow::SevenDayOverageIncluded)
651 );
652 assert_eq!(info.overage_status, Some(OverageStatus::AllowedWarning));
653 assert_eq!(info.overage_resets_at, Some(1700007200));
654 assert_eq!(
655 info.overage_disabled_reason,
656 Some(OverageDisabledReason::SeatTierZeroCreditLimit)
657 );
658 assert_eq!(info.is_using_overage, Some(true));
659 assert_eq!(info.overage_in_use, Some(true));
660 assert_eq!(info.surpassed_threshold, Some(0.8));
661 assert_eq!(
662 info.overage_period_monthly,
663 Some(OveragePeriodUtilization { utilization: 0.5 })
664 );
665 assert_eq!(
666 info.overage_period_channel,
667 Some(OveragePeriodUtilization { utilization: 0.25 })
668 );
669 assert_eq!(info.error_code, Some(RateLimitErrorCode::CreditsRequired));
670 assert_eq!(info.can_user_purchase_credits, Some(true));
671 assert_eq!(info.has_chargeable_saved_payment_method, Some(false));
672
673 let round_trip = serde_json::to_value(output).unwrap();
674 let original: serde_json::Value = serde_json::from_str(json).unwrap();
675 assert_eq!(round_trip, original);
676 }
677
678 #[test]
679 fn test_rate_limit_window_round_trip() {
680 for s in [
681 "five_hour",
682 "seven_day",
683 "seven_day_opus",
684 "seven_day_sonnet",
685 "seven_day_overage_included",
686 "overage",
687 "some_future_window",
688 ] {
689 assert_eq!(RateLimitWindow::from(s).as_str(), s);
690 }
691 }
692
693 #[test]
694 fn test_overage_disabled_reason_round_trip() {
695 for s in [
696 "overage_not_provisioned",
697 "org_level_disabled",
698 "org_level_disabled_until",
699 "out_of_credits",
700 "seat_tier_level_disabled",
701 "member_level_disabled",
702 "seat_tier_zero_credit_limit",
703 "group_zero_credit_limit",
704 "member_zero_credit_limit",
705 "org_service_level_disabled",
706 "no_limits_configured",
707 "fetch_error",
708 "unknown",
709 ] {
710 assert_eq!(OverageDisabledReason::from(s).as_str(), s);
711 }
712 }
713}