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}
657
658#[derive(Debug, Clone, PartialEq, Eq, Hash)]
660pub enum RateLimitErrorCode {
661 CreditsRequired,
663 Unknown(String),
665}
666
667impl RateLimitErrorCode {
668 pub fn as_str(&self) -> &str {
669 match self {
670 Self::CreditsRequired => "credits_required",
671 Self::Unknown(s) => s.as_str(),
672 }
673 }
674}
675
676impl fmt::Display for RateLimitErrorCode {
677 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
678 f.write_str(self.as_str())
679 }
680}
681
682impl From<&str> for RateLimitErrorCode {
683 fn from(s: &str) -> Self {
684 match s {
685 "credits_required" => Self::CreditsRequired,
686 other => Self::Unknown(other.to_string()),
687 }
688 }
689}
690
691impl Serialize for RateLimitErrorCode {
692 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
693 serializer.serialize_str(self.as_str())
694 }
695}
696
697impl<'de> Deserialize<'de> for RateLimitErrorCode {
698 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
699 let s = String::deserialize(deserializer)?;
700 Ok(Self::from(s.as_str()))
701 }
702}
703
704#[cfg(test)]
705mod tests {
706 use super::{
707 OverageDisabledReason, OveragePeriodUtilization, OverageStatus, RateLimitErrorCode,
708 RateLimitStatus, RateLimitWindow, UsageCreditOffer, UsageCreditOfferKind,
709 };
710 use crate::io::ClaudeOutput;
711
712 #[test]
713 fn test_deserialize_rate_limit_event() {
714 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"}"#;
715
716 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
717 assert!(output.is_rate_limit_event());
718 assert_eq!(output.message_type(), "rate_limit_event");
719 assert_eq!(
720 output.session_id(),
721 Some("1ae0af5b-89fa-4075-8156-d5d3702f6505")
722 );
723
724 let evt = output.as_rate_limit_event().unwrap();
725 assert_eq!(evt.rate_limit_info.status, RateLimitStatus::Allowed);
726 assert_eq!(evt.rate_limit_info.resets_at, Some(1771390800));
727 assert_eq!(
728 evt.rate_limit_info.rate_limit_type,
729 Some(RateLimitWindow::FiveHour)
730 );
731 assert_eq!(evt.rate_limit_info.utilization, None);
732 assert_eq!(
733 evt.rate_limit_info.overage_status,
734 Some(OverageStatus::Rejected)
735 );
736 assert_eq!(
737 evt.rate_limit_info.overage_disabled_reason,
738 Some(OverageDisabledReason::OrgLevelDisabled)
739 );
740 assert_eq!(evt.rate_limit_info.is_using_overage, Some(false));
741 assert_eq!(
742 evt.uuid,
743 Some("76258cfb-0dc8-4d4b-8682-77082b59c03f".to_string())
744 );
745 }
746
747 #[test]
748 fn test_deserialize_rate_limit_event_minimal() {
749 let json = r#"{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"session_id":"abc"}"#;
750
751 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
752 let evt = output.as_rate_limit_event().unwrap();
753 assert_eq!(evt.rate_limit_info.overage_disabled_reason, None);
754 assert_eq!(evt.rate_limit_info.is_using_overage, None);
755 assert!(evt.uuid.is_none());
756 }
757
758 #[test]
759 fn test_deserialize_rate_limit_event_allowed_warning() {
760 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"}"#;
761
762 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
763 let evt = output.as_rate_limit_event().unwrap();
764 assert_eq!(evt.rate_limit_info.status, RateLimitStatus::AllowedWarning);
765 assert_eq!(evt.rate_limit_info.utilization, Some(0.85));
766 assert_eq!(evt.rate_limit_info.overage_status, None);
767 assert_eq!(evt.rate_limit_info.overage_disabled_reason, None);
768 assert_eq!(evt.rate_limit_info.is_using_overage, Some(false));
769 }
770
771 #[test]
772 fn test_deserialize_rate_limit_event_no_resets_at() {
773 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"}"#;
774
775 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
776 let evt = output.as_rate_limit_event().unwrap();
777 assert_eq!(evt.rate_limit_info.status, RateLimitStatus::Allowed);
778 assert_eq!(evt.rate_limit_info.resets_at, None);
779 assert_eq!(evt.rate_limit_info.rate_limit_type, None);
780 assert_eq!(evt.rate_limit_info.is_using_overage, Some(false));
781 }
782
783 #[test]
784 fn test_deserialize_rate_limit_event_rejected() {
785 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"}"#;
786
787 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
788 let evt = output.as_rate_limit_event().unwrap();
789 assert_eq!(evt.rate_limit_info.status, RateLimitStatus::Rejected);
790 assert_eq!(
791 evt.rate_limit_info.rate_limit_type,
792 Some(RateLimitWindow::SevenDay)
793 );
794 assert_eq!(
795 evt.rate_limit_info.overage_status,
796 Some(OverageStatus::Rejected)
797 );
798 assert_eq!(
799 evt.rate_limit_info.overage_disabled_reason,
800 Some(OverageDisabledReason::OutOfCredits)
801 );
802 }
803
804 #[test]
805 fn test_deserialize_rate_limit_event_full_2_1_205() {
806 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"}"#;
807
808 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
809 let evt = output.as_rate_limit_event().unwrap();
810 let info = &evt.rate_limit_info;
811 assert_eq!(
812 info.rate_limit_type,
813 Some(RateLimitWindow::SevenDayOverageIncluded)
814 );
815 assert_eq!(info.overage_status, Some(OverageStatus::AllowedWarning));
816 assert_eq!(info.overage_resets_at, Some(1700007200));
817 assert_eq!(
818 info.overage_disabled_reason,
819 Some(OverageDisabledReason::SeatTierZeroCreditLimit)
820 );
821 assert_eq!(info.is_using_overage, Some(true));
822 assert_eq!(info.overage_in_use, Some(true));
823 assert_eq!(info.surpassed_threshold, Some(0.8));
824 assert_eq!(
825 info.overage_period_monthly,
826 Some(OveragePeriodUtilization { utilization: 0.5 })
827 );
828 assert_eq!(
829 info.overage_period_channel,
830 Some(OveragePeriodUtilization { utilization: 0.25 })
831 );
832 assert_eq!(info.error_code, Some(RateLimitErrorCode::CreditsRequired));
833 assert_eq!(info.can_user_purchase_credits, Some(true));
834 assert_eq!(info.has_chargeable_saved_payment_method, Some(false));
835
836 let round_trip = serde_json::to_value(output).unwrap();
837 let original: serde_json::Value = serde_json::from_str(json).unwrap();
838 assert_eq!(round_trip, original);
839 }
840
841 #[test]
842 fn test_rate_limit_usage_credit_offer_round_trip() {
843 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"}"#;
844
845 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
846 let evt = output.as_rate_limit_event().unwrap();
847 assert_eq!(
848 evt.rate_limit_info.usage_credit_offer,
849 Some(UsageCreditOffer {
850 kind: UsageCreditOfferKind::PctOff,
851 headline_pct: 20,
852 days_left: 3,
853 label: "20% off usage credits for 3 more days".to_string(),
854 })
855 );
856
857 let round_trip = serde_json::to_value(output).unwrap();
858 let original: serde_json::Value = serde_json::from_str(json).unwrap();
859 assert_eq!(round_trip, original);
860 }
861
862 #[test]
863 fn test_usage_credit_offer_kind_round_trip() {
864 for s in ["pct_off", "extra_credit", "future_kind"] {
865 let kind: UsageCreditOfferKind = serde_json::from_value(serde_json::json!(s)).unwrap();
866 assert_eq!(kind.as_str(), s);
867 assert_eq!(serde_json::to_value(&kind).unwrap(), serde_json::json!(s));
868 }
869 assert_eq!(
870 UsageCreditOfferKind::from("future_kind"),
871 UsageCreditOfferKind::Unknown("future_kind".to_string())
872 );
873 }
874
875 #[test]
876 fn test_rate_limit_window_round_trip() {
877 for s in [
878 "five_hour",
879 "seven_day",
880 "seven_day_opus",
881 "seven_day_sonnet",
882 "seven_day_overage_included",
883 "overage",
884 "some_future_window",
885 ] {
886 assert_eq!(RateLimitWindow::from(s).as_str(), s);
887 }
888 }
889
890 #[test]
891 fn test_overage_disabled_reason_round_trip() {
892 for s in [
893 "overage_not_provisioned",
894 "org_level_disabled",
895 "org_level_disabled_until",
896 "out_of_credits",
897 "seat_tier_level_disabled",
898 "member_level_disabled",
899 "seat_tier_zero_credit_limit",
900 "group_zero_credit_limit",
901 "member_zero_credit_limit",
902 "org_service_level_disabled",
903 "no_limits_configured",
904 "fetch_error",
905 "unknown",
906 ] {
907 assert_eq!(OverageDisabledReason::from(s).as_str(), s);
908 }
909 }
910}