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 #[serde(rename = "usageCreditOffer", skip_serializing_if = "Option::is_none")]
461 pub usage_credit_offer: Option<UsageCreditOffer>,
462 #[serde(rename = "adminRequestOffer", skip_serializing_if = "Option::is_none")]
467 pub admin_request_offer: Option<AdminRequestOffer>,
468}
469
470#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
473pub struct AdminRequestOffer {
474 pub state: AdminRequestOfferState,
476 pub label: String,
478 #[serde(rename = "doneLabel")]
480 pub done_label: String,
481 pub confirm: bool,
483 #[serde(rename = "confirmPrompt")]
485 pub confirm_prompt: String,
486 #[serde(
488 rename = "lastFileFailed",
489 default,
490 skip_serializing_if = "Option::is_none"
491 )]
492 pub last_file_failed: Option<bool>,
493 #[serde(default, skip_serializing_if = "Option::is_none")]
495 pub revision: Option<i64>,
496}
497
498#[derive(Debug, Clone, PartialEq, Eq, Hash)]
500pub enum AdminRequestOfferState {
501 CanAsk,
503 Pending,
505 Sending,
507 Sent,
509 Unknown(String),
511}
512
513impl AdminRequestOfferState {
514 pub fn as_str(&self) -> &str {
515 match self {
516 Self::CanAsk => "can_ask",
517 Self::Pending => "pending",
518 Self::Sending => "sending",
519 Self::Sent => "sent",
520 Self::Unknown(s) => s.as_str(),
521 }
522 }
523}
524
525impl fmt::Display for AdminRequestOfferState {
526 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
527 f.write_str(self.as_str())
528 }
529}
530
531impl From<&str> for AdminRequestOfferState {
532 fn from(s: &str) -> Self {
533 match s {
534 "can_ask" => Self::CanAsk,
535 "pending" => Self::Pending,
536 "sending" => Self::Sending,
537 "sent" => Self::Sent,
538 other => Self::Unknown(other.to_string()),
539 }
540 }
541}
542
543impl Serialize for AdminRequestOfferState {
544 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
545 serializer.serialize_str(self.as_str())
546 }
547}
548
549impl<'de> Deserialize<'de> for AdminRequestOfferState {
550 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
551 let s = String::deserialize(deserializer)?;
552 Ok(Self::from(s.as_str()))
553 }
554}
555
556#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
559pub struct UsageCreditOffer {
560 pub kind: UsageCreditOfferKind,
562 #[serde(rename = "headlinePct")]
564 pub headline_pct: i64,
565 #[serde(rename = "daysLeft")]
567 pub days_left: i64,
568 pub label: String,
570}
571
572#[derive(Debug, Clone, PartialEq, Eq, Hash)]
574pub enum UsageCreditOfferKind {
575 PctOff,
577 ExtraCredit,
579 Unknown(String),
581}
582
583impl UsageCreditOfferKind {
584 pub fn as_str(&self) -> &str {
585 match self {
586 Self::PctOff => "pct_off",
587 Self::ExtraCredit => "extra_credit",
588 Self::Unknown(s) => s.as_str(),
589 }
590 }
591}
592
593impl fmt::Display for UsageCreditOfferKind {
594 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
595 f.write_str(self.as_str())
596 }
597}
598
599impl From<&str> for UsageCreditOfferKind {
600 fn from(s: &str) -> Self {
601 match s {
602 "pct_off" => Self::PctOff,
603 "extra_credit" => Self::ExtraCredit,
604 other => Self::Unknown(other.to_string()),
605 }
606 }
607}
608
609impl Serialize for UsageCreditOfferKind {
610 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
611 serializer.serialize_str(self.as_str())
612 }
613}
614
615impl<'de> Deserialize<'de> for UsageCreditOfferKind {
616 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
617 let s = String::deserialize(deserializer)?;
618 Ok(Self::from(s.as_str()))
619 }
620}
621
622#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
624pub struct OveragePeriodUtilization {
625 pub utilization: f64,
627}
628
629#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
633pub struct UnifiedRateLimitWindows {
634 #[serde(default, skip_serializing_if = "Option::is_none")]
636 pub five_hour: Option<UnifiedWindowUsage>,
637 #[serde(default, skip_serializing_if = "Option::is_none")]
639 pub seven_day: Option<UnifiedWindowUsage>,
640 #[serde(default, skip_serializing_if = "Option::is_none")]
643 pub seven_day_overage_included: Option<UnifiedWindowUsage>,
644}
645
646#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
649pub struct UnifiedWindowUsage {
650 pub utilization: f64,
653 #[serde(rename = "resetsAt")]
655 pub resets_at: u64,
656 #[serde(default, skip_serializing_if = "Option::is_none")]
662 pub status: Option<RateLimitStatus>,
663 #[serde(
666 rename = "surpassedThreshold",
667 default,
668 skip_serializing_if = "Option::is_none"
669 )]
670 pub surpassed_threshold: Option<f64>,
671}
672
673#[derive(Debug, Clone, PartialEq, Eq, Hash)]
675pub enum RateLimitErrorCode {
676 CreditsRequired,
678 Unknown(String),
680}
681
682impl RateLimitErrorCode {
683 pub fn as_str(&self) -> &str {
684 match self {
685 Self::CreditsRequired => "credits_required",
686 Self::Unknown(s) => s.as_str(),
687 }
688 }
689}
690
691impl fmt::Display for RateLimitErrorCode {
692 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
693 f.write_str(self.as_str())
694 }
695}
696
697impl From<&str> for RateLimitErrorCode {
698 fn from(s: &str) -> Self {
699 match s {
700 "credits_required" => Self::CreditsRequired,
701 other => Self::Unknown(other.to_string()),
702 }
703 }
704}
705
706impl Serialize for RateLimitErrorCode {
707 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
708 serializer.serialize_str(self.as_str())
709 }
710}
711
712impl<'de> Deserialize<'de> for RateLimitErrorCode {
713 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
714 let s = String::deserialize(deserializer)?;
715 Ok(Self::from(s.as_str()))
716 }
717}
718
719#[cfg(test)]
720mod tests {
721 use super::{
722 OverageDisabledReason, OveragePeriodUtilization, OverageStatus, RateLimitErrorCode,
723 RateLimitStatus, RateLimitWindow, UsageCreditOffer, UsageCreditOfferKind,
724 };
725 use crate::io::ClaudeOutput;
726
727 #[test]
728 fn test_deserialize_rate_limit_event() {
729 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"}"#;
730
731 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
732 assert!(output.is_rate_limit_event());
733 assert_eq!(output.message_type(), "rate_limit_event");
734 assert_eq!(
735 output.session_id(),
736 Some("1ae0af5b-89fa-4075-8156-d5d3702f6505")
737 );
738
739 let evt = output.as_rate_limit_event().unwrap();
740 assert_eq!(evt.rate_limit_info.status, RateLimitStatus::Allowed);
741 assert_eq!(evt.rate_limit_info.resets_at, Some(1771390800));
742 assert_eq!(
743 evt.rate_limit_info.rate_limit_type,
744 Some(RateLimitWindow::FiveHour)
745 );
746 assert_eq!(evt.rate_limit_info.utilization, None);
747 assert_eq!(
748 evt.rate_limit_info.overage_status,
749 Some(OverageStatus::Rejected)
750 );
751 assert_eq!(
752 evt.rate_limit_info.overage_disabled_reason,
753 Some(OverageDisabledReason::OrgLevelDisabled)
754 );
755 assert_eq!(evt.rate_limit_info.is_using_overage, Some(false));
756 assert_eq!(
757 evt.uuid,
758 Some("76258cfb-0dc8-4d4b-8682-77082b59c03f".to_string())
759 );
760 }
761
762 #[test]
763 fn test_deserialize_rate_limit_event_minimal() {
764 let json = r#"{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"session_id":"abc"}"#;
765
766 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
767 let evt = output.as_rate_limit_event().unwrap();
768 assert_eq!(evt.rate_limit_info.overage_disabled_reason, None);
769 assert_eq!(evt.rate_limit_info.is_using_overage, None);
770 assert!(evt.uuid.is_none());
771 }
772
773 #[test]
774 fn test_deserialize_rate_limit_event_allowed_warning() {
775 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"}"#;
776
777 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
778 let evt = output.as_rate_limit_event().unwrap();
779 assert_eq!(evt.rate_limit_info.status, RateLimitStatus::AllowedWarning);
780 assert_eq!(evt.rate_limit_info.utilization, Some(0.85));
781 assert_eq!(evt.rate_limit_info.overage_status, None);
782 assert_eq!(evt.rate_limit_info.overage_disabled_reason, None);
783 assert_eq!(evt.rate_limit_info.is_using_overage, Some(false));
784 }
785
786 #[test]
787 fn test_deserialize_rate_limit_event_no_resets_at() {
788 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"}"#;
789
790 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
791 let evt = output.as_rate_limit_event().unwrap();
792 assert_eq!(evt.rate_limit_info.status, RateLimitStatus::Allowed);
793 assert_eq!(evt.rate_limit_info.resets_at, None);
794 assert_eq!(evt.rate_limit_info.rate_limit_type, None);
795 assert_eq!(evt.rate_limit_info.is_using_overage, Some(false));
796 }
797
798 #[test]
799 fn test_deserialize_rate_limit_event_rejected() {
800 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"}"#;
801
802 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
803 let evt = output.as_rate_limit_event().unwrap();
804 assert_eq!(evt.rate_limit_info.status, RateLimitStatus::Rejected);
805 assert_eq!(
806 evt.rate_limit_info.rate_limit_type,
807 Some(RateLimitWindow::SevenDay)
808 );
809 assert_eq!(
810 evt.rate_limit_info.overage_status,
811 Some(OverageStatus::Rejected)
812 );
813 assert_eq!(
814 evt.rate_limit_info.overage_disabled_reason,
815 Some(OverageDisabledReason::OutOfCredits)
816 );
817 }
818
819 #[test]
820 fn test_deserialize_rate_limit_event_full_2_1_205() {
821 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"}"#;
822
823 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
824 let evt = output.as_rate_limit_event().unwrap();
825 let info = &evt.rate_limit_info;
826 assert_eq!(
827 info.rate_limit_type,
828 Some(RateLimitWindow::SevenDayOverageIncluded)
829 );
830 assert_eq!(info.overage_status, Some(OverageStatus::AllowedWarning));
831 assert_eq!(info.overage_resets_at, Some(1700007200));
832 assert_eq!(
833 info.overage_disabled_reason,
834 Some(OverageDisabledReason::SeatTierZeroCreditLimit)
835 );
836 assert_eq!(info.is_using_overage, Some(true));
837 assert_eq!(info.overage_in_use, Some(true));
838 assert_eq!(info.surpassed_threshold, Some(0.8));
839 assert_eq!(
840 info.overage_period_monthly,
841 Some(OveragePeriodUtilization { utilization: 0.5 })
842 );
843 assert_eq!(
844 info.overage_period_channel,
845 Some(OveragePeriodUtilization { utilization: 0.25 })
846 );
847 assert_eq!(info.error_code, Some(RateLimitErrorCode::CreditsRequired));
848 assert_eq!(info.can_user_purchase_credits, Some(true));
849 assert_eq!(info.has_chargeable_saved_payment_method, Some(false));
850
851 let round_trip = serde_json::to_value(output).unwrap();
852 let original: serde_json::Value = serde_json::from_str(json).unwrap();
853 assert_eq!(round_trip, original);
854 }
855
856 #[test]
857 fn test_rate_limit_usage_credit_offer_round_trip() {
858 let json = r#"{"type":"rate_limit_event","rate_limit_info":{"status":"rejected","errorCode":"credits_required","usageCreditOffer":{"kind":"pct_off","headlinePct":20,"daysLeft":3,"label":"20% off usage credits for 3 more days"}},"session_id":"abc"}"#;
859
860 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
861 let evt = output.as_rate_limit_event().unwrap();
862 assert_eq!(
863 evt.rate_limit_info.usage_credit_offer,
864 Some(UsageCreditOffer {
865 kind: UsageCreditOfferKind::PctOff,
866 headline_pct: 20,
867 days_left: 3,
868 label: "20% off usage credits for 3 more days".to_string(),
869 })
870 );
871
872 let round_trip = serde_json::to_value(output).unwrap();
873 let original: serde_json::Value = serde_json::from_str(json).unwrap();
874 assert_eq!(round_trip, original);
875 }
876
877 #[test]
878 fn test_usage_credit_offer_kind_round_trip() {
879 for s in ["pct_off", "extra_credit", "future_kind"] {
880 let kind: UsageCreditOfferKind = serde_json::from_value(serde_json::json!(s)).unwrap();
881 assert_eq!(kind.as_str(), s);
882 assert_eq!(serde_json::to_value(&kind).unwrap(), serde_json::json!(s));
883 }
884 assert_eq!(
885 UsageCreditOfferKind::from("future_kind"),
886 UsageCreditOfferKind::Unknown("future_kind".to_string())
887 );
888 }
889
890 #[test]
891 fn test_rate_limit_window_round_trip() {
892 for s in [
893 "five_hour",
894 "seven_day",
895 "seven_day_opus",
896 "seven_day_sonnet",
897 "seven_day_overage_included",
898 "overage",
899 "some_future_window",
900 ] {
901 assert_eq!(RateLimitWindow::from(s).as_str(), s);
902 }
903 }
904
905 #[test]
906 fn test_overage_disabled_reason_round_trip() {
907 for s in [
908 "overage_not_provisioned",
909 "org_level_disabled",
910 "org_level_disabled_until",
911 "out_of_credits",
912 "seat_tier_level_disabled",
913 "member_level_disabled",
914 "seat_tier_zero_credit_limit",
915 "group_zero_credit_limit",
916 "member_zero_credit_limit",
917 "org_service_level_disabled",
918 "no_limits_configured",
919 "fetch_error",
920 "unknown",
921 ] {
922 assert_eq!(OverageDisabledReason::from(s).as_str(), s);
923 }
924 }
925}