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}
463
464#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
467pub struct UsageCreditOffer {
468 pub kind: UsageCreditOfferKind,
470 #[serde(rename = "headlinePct")]
472 pub headline_pct: i64,
473 #[serde(rename = "daysLeft")]
475 pub days_left: i64,
476 pub label: String,
478}
479
480#[derive(Debug, Clone, PartialEq, Eq, Hash)]
482pub enum UsageCreditOfferKind {
483 PctOff,
485 ExtraCredit,
487 Unknown(String),
489}
490
491impl UsageCreditOfferKind {
492 pub fn as_str(&self) -> &str {
493 match self {
494 Self::PctOff => "pct_off",
495 Self::ExtraCredit => "extra_credit",
496 Self::Unknown(s) => s.as_str(),
497 }
498 }
499}
500
501impl fmt::Display for UsageCreditOfferKind {
502 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
503 f.write_str(self.as_str())
504 }
505}
506
507impl From<&str> for UsageCreditOfferKind {
508 fn from(s: &str) -> Self {
509 match s {
510 "pct_off" => Self::PctOff,
511 "extra_credit" => Self::ExtraCredit,
512 other => Self::Unknown(other.to_string()),
513 }
514 }
515}
516
517impl Serialize for UsageCreditOfferKind {
518 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
519 serializer.serialize_str(self.as_str())
520 }
521}
522
523impl<'de> Deserialize<'de> for UsageCreditOfferKind {
524 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
525 let s = String::deserialize(deserializer)?;
526 Ok(Self::from(s.as_str()))
527 }
528}
529
530#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
532pub struct OveragePeriodUtilization {
533 pub utilization: f64,
535}
536
537#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
541pub struct UnifiedRateLimitWindows {
542 #[serde(default, skip_serializing_if = "Option::is_none")]
544 pub five_hour: Option<UnifiedWindowUsage>,
545 #[serde(default, skip_serializing_if = "Option::is_none")]
547 pub seven_day: Option<UnifiedWindowUsage>,
548 #[serde(default, skip_serializing_if = "Option::is_none")]
551 pub seven_day_overage_included: Option<UnifiedWindowUsage>,
552}
553
554#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
557pub struct UnifiedWindowUsage {
558 pub utilization: f64,
561 #[serde(rename = "resetsAt")]
563 pub resets_at: u64,
564}
565
566#[derive(Debug, Clone, PartialEq, Eq, Hash)]
568pub enum RateLimitErrorCode {
569 CreditsRequired,
571 Unknown(String),
573}
574
575impl RateLimitErrorCode {
576 pub fn as_str(&self) -> &str {
577 match self {
578 Self::CreditsRequired => "credits_required",
579 Self::Unknown(s) => s.as_str(),
580 }
581 }
582}
583
584impl fmt::Display for RateLimitErrorCode {
585 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
586 f.write_str(self.as_str())
587 }
588}
589
590impl From<&str> for RateLimitErrorCode {
591 fn from(s: &str) -> Self {
592 match s {
593 "credits_required" => Self::CreditsRequired,
594 other => Self::Unknown(other.to_string()),
595 }
596 }
597}
598
599impl Serialize for RateLimitErrorCode {
600 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
601 serializer.serialize_str(self.as_str())
602 }
603}
604
605impl<'de> Deserialize<'de> for RateLimitErrorCode {
606 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
607 let s = String::deserialize(deserializer)?;
608 Ok(Self::from(s.as_str()))
609 }
610}
611
612#[cfg(test)]
613mod tests {
614 use super::{
615 OverageDisabledReason, OveragePeriodUtilization, OverageStatus, RateLimitErrorCode,
616 RateLimitStatus, RateLimitWindow, UsageCreditOffer, UsageCreditOfferKind,
617 };
618 use crate::io::ClaudeOutput;
619
620 #[test]
621 fn test_deserialize_rate_limit_event() {
622 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"}"#;
623
624 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
625 assert!(output.is_rate_limit_event());
626 assert_eq!(output.message_type(), "rate_limit_event");
627 assert_eq!(
628 output.session_id(),
629 Some("1ae0af5b-89fa-4075-8156-d5d3702f6505")
630 );
631
632 let evt = output.as_rate_limit_event().unwrap();
633 assert_eq!(evt.rate_limit_info.status, RateLimitStatus::Allowed);
634 assert_eq!(evt.rate_limit_info.resets_at, Some(1771390800));
635 assert_eq!(
636 evt.rate_limit_info.rate_limit_type,
637 Some(RateLimitWindow::FiveHour)
638 );
639 assert_eq!(evt.rate_limit_info.utilization, None);
640 assert_eq!(
641 evt.rate_limit_info.overage_status,
642 Some(OverageStatus::Rejected)
643 );
644 assert_eq!(
645 evt.rate_limit_info.overage_disabled_reason,
646 Some(OverageDisabledReason::OrgLevelDisabled)
647 );
648 assert_eq!(evt.rate_limit_info.is_using_overage, Some(false));
649 assert_eq!(
650 evt.uuid,
651 Some("76258cfb-0dc8-4d4b-8682-77082b59c03f".to_string())
652 );
653 }
654
655 #[test]
656 fn test_deserialize_rate_limit_event_minimal() {
657 let json = r#"{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"session_id":"abc"}"#;
658
659 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
660 let evt = output.as_rate_limit_event().unwrap();
661 assert_eq!(evt.rate_limit_info.overage_disabled_reason, None);
662 assert_eq!(evt.rate_limit_info.is_using_overage, None);
663 assert!(evt.uuid.is_none());
664 }
665
666 #[test]
667 fn test_deserialize_rate_limit_event_allowed_warning() {
668 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"}"#;
669
670 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
671 let evt = output.as_rate_limit_event().unwrap();
672 assert_eq!(evt.rate_limit_info.status, RateLimitStatus::AllowedWarning);
673 assert_eq!(evt.rate_limit_info.utilization, Some(0.85));
674 assert_eq!(evt.rate_limit_info.overage_status, None);
675 assert_eq!(evt.rate_limit_info.overage_disabled_reason, None);
676 assert_eq!(evt.rate_limit_info.is_using_overage, Some(false));
677 }
678
679 #[test]
680 fn test_deserialize_rate_limit_event_no_resets_at() {
681 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"}"#;
682
683 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
684 let evt = output.as_rate_limit_event().unwrap();
685 assert_eq!(evt.rate_limit_info.status, RateLimitStatus::Allowed);
686 assert_eq!(evt.rate_limit_info.resets_at, None);
687 assert_eq!(evt.rate_limit_info.rate_limit_type, None);
688 assert_eq!(evt.rate_limit_info.is_using_overage, Some(false));
689 }
690
691 #[test]
692 fn test_deserialize_rate_limit_event_rejected() {
693 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"}"#;
694
695 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
696 let evt = output.as_rate_limit_event().unwrap();
697 assert_eq!(evt.rate_limit_info.status, RateLimitStatus::Rejected);
698 assert_eq!(
699 evt.rate_limit_info.rate_limit_type,
700 Some(RateLimitWindow::SevenDay)
701 );
702 assert_eq!(
703 evt.rate_limit_info.overage_status,
704 Some(OverageStatus::Rejected)
705 );
706 assert_eq!(
707 evt.rate_limit_info.overage_disabled_reason,
708 Some(OverageDisabledReason::OutOfCredits)
709 );
710 }
711
712 #[test]
713 fn test_deserialize_rate_limit_event_full_2_1_205() {
714 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"}"#;
715
716 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
717 let evt = output.as_rate_limit_event().unwrap();
718 let info = &evt.rate_limit_info;
719 assert_eq!(
720 info.rate_limit_type,
721 Some(RateLimitWindow::SevenDayOverageIncluded)
722 );
723 assert_eq!(info.overage_status, Some(OverageStatus::AllowedWarning));
724 assert_eq!(info.overage_resets_at, Some(1700007200));
725 assert_eq!(
726 info.overage_disabled_reason,
727 Some(OverageDisabledReason::SeatTierZeroCreditLimit)
728 );
729 assert_eq!(info.is_using_overage, Some(true));
730 assert_eq!(info.overage_in_use, Some(true));
731 assert_eq!(info.surpassed_threshold, Some(0.8));
732 assert_eq!(
733 info.overage_period_monthly,
734 Some(OveragePeriodUtilization { utilization: 0.5 })
735 );
736 assert_eq!(
737 info.overage_period_channel,
738 Some(OveragePeriodUtilization { utilization: 0.25 })
739 );
740 assert_eq!(info.error_code, Some(RateLimitErrorCode::CreditsRequired));
741 assert_eq!(info.can_user_purchase_credits, Some(true));
742 assert_eq!(info.has_chargeable_saved_payment_method, Some(false));
743
744 let round_trip = serde_json::to_value(output).unwrap();
745 let original: serde_json::Value = serde_json::from_str(json).unwrap();
746 assert_eq!(round_trip, original);
747 }
748
749 #[test]
750 fn test_rate_limit_usage_credit_offer_round_trip() {
751 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"}"#;
752
753 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
754 let evt = output.as_rate_limit_event().unwrap();
755 assert_eq!(
756 evt.rate_limit_info.usage_credit_offer,
757 Some(UsageCreditOffer {
758 kind: UsageCreditOfferKind::PctOff,
759 headline_pct: 20,
760 days_left: 3,
761 label: "20% off usage credits for 3 more days".to_string(),
762 })
763 );
764
765 let round_trip = serde_json::to_value(output).unwrap();
766 let original: serde_json::Value = serde_json::from_str(json).unwrap();
767 assert_eq!(round_trip, original);
768 }
769
770 #[test]
771 fn test_usage_credit_offer_kind_round_trip() {
772 for s in ["pct_off", "extra_credit", "future_kind"] {
773 let kind: UsageCreditOfferKind = serde_json::from_value(serde_json::json!(s)).unwrap();
774 assert_eq!(kind.as_str(), s);
775 assert_eq!(serde_json::to_value(&kind).unwrap(), serde_json::json!(s));
776 }
777 assert_eq!(
778 UsageCreditOfferKind::from("future_kind"),
779 UsageCreditOfferKind::Unknown("future_kind".to_string())
780 );
781 }
782
783 #[test]
784 fn test_rate_limit_window_round_trip() {
785 for s in [
786 "five_hour",
787 "seven_day",
788 "seven_day_opus",
789 "seven_day_sonnet",
790 "seven_day_overage_included",
791 "overage",
792 "some_future_window",
793 ] {
794 assert_eq!(RateLimitWindow::from(s).as_str(), s);
795 }
796 }
797
798 #[test]
799 fn test_overage_disabled_reason_round_trip() {
800 for s in [
801 "overage_not_provisioned",
802 "org_level_disabled",
803 "org_level_disabled_until",
804 "out_of_credits",
805 "seat_tier_level_disabled",
806 "member_level_disabled",
807 "seat_tier_zero_credit_limit",
808 "group_zero_credit_limit",
809 "member_zero_credit_limit",
810 "org_service_level_disabled",
811 "no_limits_configured",
812 "fetch_error",
813 "unknown",
814 ] {
815 assert_eq!(OverageDisabledReason::from(s).as_str(), s);
816 }
817 }
818}