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(skip_serializing_if = "Option::is_none", rename = "overageStatus")]
335 pub overage_status: Option<OverageStatus>,
336 #[serde(rename = "overageResetsAt", skip_serializing_if = "Option::is_none")]
338 pub overage_resets_at: Option<u64>,
339 #[serde(
341 rename = "overageDisabledReason",
342 skip_serializing_if = "Option::is_none"
343 )]
344 pub overage_disabled_reason: Option<OverageDisabledReason>,
345 #[serde(rename = "isUsingOverage", skip_serializing_if = "Option::is_none")]
347 pub is_using_overage: Option<bool>,
348 #[serde(rename = "overageInUse", skip_serializing_if = "Option::is_none")]
350 pub overage_in_use: Option<bool>,
351 #[serde(rename = "surpassedThreshold", skip_serializing_if = "Option::is_none")]
353 pub surpassed_threshold: Option<f64>,
354 #[serde(
356 rename = "overagePeriodMonthly",
357 skip_serializing_if = "Option::is_none"
358 )]
359 pub overage_period_monthly: Option<OveragePeriodUtilization>,
360 #[serde(
362 rename = "overagePeriodChannel",
363 skip_serializing_if = "Option::is_none"
364 )]
365 pub overage_period_channel: Option<OveragePeriodUtilization>,
366 #[serde(rename = "errorCode", skip_serializing_if = "Option::is_none")]
368 pub error_code: Option<RateLimitErrorCode>,
369 #[serde(
371 rename = "canUserPurchaseCredits",
372 skip_serializing_if = "Option::is_none"
373 )]
374 pub can_user_purchase_credits: Option<bool>,
375 #[serde(
377 rename = "hasChargeableSavedPaymentMethod",
378 skip_serializing_if = "Option::is_none"
379 )]
380 pub has_chargeable_saved_payment_method: Option<bool>,
381}
382
383#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
385pub struct OveragePeriodUtilization {
386 pub utilization: f64,
388}
389
390#[derive(Debug, Clone, PartialEq, Eq, Hash)]
392pub enum RateLimitErrorCode {
393 CreditsRequired,
395 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}