@@ -11,7 +11,7 @@ use std::sync::Arc;
1111use std:: time:: Duration ;
1212use tokio:: io:: { AsyncReadExt , AsyncWriteExt , BufReader } ;
1313use tokio:: process:: { Child , ChildStdin , Command } ;
14- use tokio:: sync:: { oneshot, Mutex } ;
14+ use tokio:: sync:: { mpsc , oneshot, Mutex } ;
1515use tokio:: time:: timeout;
1616
1717const MAX_RPC_LINE_BYTES : usize = 1024 * 1024 ;
@@ -32,11 +32,26 @@ pub struct CreditsSnapshot {
3232 pub balance : Option < String > ,
3333}
3434
35+ #[ derive( Debug , Clone ) ]
36+ pub struct RateLimitSnapshot {
37+ pub limit_id : Option < String > ,
38+ pub limit_name : Option < String > ,
39+ pub plan_type : Option < String > ,
40+ pub primary : Option < RateLimitWindow > ,
41+ pub secondary : Option < RateLimitWindow > ,
42+ pub credits : Option < CreditsSnapshot > ,
43+ }
44+
3545#[ derive( Debug , Clone ) ]
3646pub struct AccountRateLimits {
47+ pub limit_id : Option < String > ,
48+ pub limit_name : Option < String > ,
49+ pub plan_type : Option < String > ,
3750 pub primary : Option < RateLimitWindow > ,
3851 pub secondary : Option < RateLimitWindow > ,
3952 pub credits : Option < CreditsSnapshot > ,
53+ pub buckets : Vec < RateLimitSnapshot > ,
54+ pub reset_credits_available : Option < i64 > ,
4055}
4156
4257#[ derive( Debug , Clone , PartialEq , Eq ) ]
@@ -108,6 +123,7 @@ pub struct CodexRpc {
108123 child : Mutex < Child > ,
109124 stdin : Mutex < ChildStdin > ,
110125 pending : Mutex < HashMap < u64 , oneshot:: Sender < Value > > > ,
126+ notifications : Mutex < mpsc:: Receiver < Value > > ,
111127 next_id : Mutex < u64 > ,
112128}
113129
@@ -134,10 +150,12 @@ impl CodexRpc {
134150 . take ( )
135151 . ok_or_else ( || anyhow ! ( "missing stderr" ) ) ?;
136152
153+ let ( notification_tx, notification_rx) = mpsc:: channel ( MAX_PENDING_REQUESTS ) ;
137154 let rpc = Arc :: new ( Self {
138155 child : Mutex :: new ( child) ,
139156 stdin : Mutex :: new ( stdin) ,
140157 pending : Mutex :: new ( HashMap :: new ( ) ) ,
158+ notifications : Mutex :: new ( notification_rx) ,
141159 next_id : Mutex :: new ( 1 ) ,
142160 } ) ;
143161
@@ -186,7 +204,11 @@ impl CodexRpc {
186204 if let Some ( tx) = rpc. pending . lock ( ) . await . remove ( & id) {
187205 let _ = tx. send ( value) ;
188206 }
207+ } else if value. get ( "method" ) . is_some ( ) {
208+ let _ = notification_tx. try_send ( value) ;
189209 }
210+ } else if value. get ( "method" ) . is_some ( ) {
211+ let _ = notification_tx. try_send ( value) ;
190212 }
191213 }
192214
@@ -309,13 +331,11 @@ impl CodexRpc {
309331 }
310332
311333 let result = value. get ( "result" ) . cloned ( ) . unwrap_or ( Value :: Null ) ;
312- let rate_limits = result
313- . get ( "rateLimits" )
314- . or_else ( || result. get ( "rate_limits" ) )
315- . cloned ( )
316- . unwrap_or ( Value :: Null ) ;
334+ parse_account_rate_limits_result ( & result)
335+ }
317336
318- parse_rate_limits ( & rate_limits)
337+ pub async fn recv_notification ( & self ) -> Option < Value > {
338+ self . notifications . lock ( ) . await . recv ( ) . await
319339 }
320340
321341 pub async fn kill ( & self ) {
@@ -324,6 +344,10 @@ impl CodexRpc {
324344 }
325345}
326346
347+ pub fn is_account_rate_limits_updated_notification ( value : & Value ) -> bool {
348+ value. get ( "method" ) . and_then ( |v| v. as_str ( ) ) == Some ( "account/rateLimits/updated" )
349+ }
350+
327351fn non_empty_string ( value : String ) -> Option < String > {
328352 let trimmed = value. trim ( ) ;
329353 if trimmed. is_empty ( ) {
@@ -536,11 +560,67 @@ fn find_app_server_recursive(
536560 None
537561}
538562
563+ fn parse_account_rate_limits_result ( result : & Value ) -> Result < AccountRateLimits > {
564+ let rate_limits = result
565+ . get ( "rateLimits" )
566+ . or_else ( || result. get ( "rate_limits" ) )
567+ . cloned ( )
568+ . unwrap_or ( Value :: Null ) ;
569+ let mut limits = parse_rate_limits ( & rate_limits) ?;
570+
571+ limits. reset_credits_available = result
572+ . get ( "rateLimitResetCredits" )
573+ . or_else ( || result. get ( "rate_limit_reset_credits" ) )
574+ . and_then ( |v| v. as_object ( ) )
575+ . and_then ( |v| {
576+ v. get ( "availableCount" )
577+ . or_else ( || v. get ( "available_count" ) )
578+ . and_then ( as_i64_maybe_float)
579+ } ) ;
580+
581+ if let Some ( by_limit_id) = result
582+ . get ( "rateLimitsByLimitId" )
583+ . or_else ( || result. get ( "rate_limits_by_limit_id" ) )
584+ . and_then ( |v| v. as_object ( ) )
585+ {
586+ let mut buckets = Vec :: < RateLimitSnapshot > :: with_capacity ( by_limit_id. len ( ) ) ;
587+ for ( key, value) in by_limit_id {
588+ if let Some ( mut snapshot) = parse_rate_limit_snapshot ( value) {
589+ if snapshot. limit_id . is_none ( ) {
590+ snapshot. limit_id = Some ( key. clone ( ) ) ;
591+ }
592+ buckets. push ( snapshot) ;
593+ }
594+ }
595+ buckets. sort_by ( |a, b| a. limit_id . cmp ( & b. limit_id ) ) ;
596+ limits. buckets = buckets;
597+ }
598+
599+ Ok ( limits)
600+ }
601+
539602fn parse_rate_limits ( value : & Value ) -> Result < AccountRateLimits > {
540- let obj = value
541- . as_object ( )
542- . ok_or_else ( || anyhow ! ( "rate limits missing" ) ) ?;
603+ let snapshot =
604+ parse_rate_limit_snapshot ( value) . ok_or_else ( || anyhow ! ( "rate limits missing" ) ) ?;
605+
606+ Ok ( AccountRateLimits {
607+ limit_id : snapshot. limit_id . clone ( ) ,
608+ limit_name : snapshot. limit_name . clone ( ) ,
609+ plan_type : snapshot. plan_type . clone ( ) ,
610+ primary : snapshot. primary . clone ( ) ,
611+ secondary : snapshot. secondary . clone ( ) ,
612+ credits : snapshot. credits . clone ( ) ,
613+ buckets : Vec :: new ( ) ,
614+ reset_credits_available : None ,
615+ } )
616+ }
617+
618+ fn parse_rate_limit_snapshot ( value : & Value ) -> Option < RateLimitSnapshot > {
619+ let obj = value. as_object ( ) . filter ( |_| !value. is_null ( ) ) ?;
543620
621+ let limit_id = read_string ( obj. get ( "limitId" ) . or_else ( || obj. get ( "limit_id" ) ) ) ;
622+ let limit_name = read_string ( obj. get ( "limitName" ) . or_else ( || obj. get ( "limit_name" ) ) ) ;
623+ let plan_type = read_string ( obj. get ( "planType" ) . or_else ( || obj. get ( "plan_type" ) ) ) ;
544624 let primary = obj. get ( "primary" ) . and_then ( parse_window) ;
545625 let secondary = obj. get ( "secondary" ) . and_then ( parse_window) ;
546626
@@ -567,7 +647,10 @@ fn parse_rate_limits(value: &Value) -> Result<AccountRateLimits> {
567647 }
568648 } ) ;
569649
570- Ok ( AccountRateLimits {
650+ Some ( RateLimitSnapshot {
651+ limit_id,
652+ limit_name,
653+ plan_type,
571654 primary,
572655 secondary,
573656 credits,
@@ -583,6 +666,7 @@ fn parse_window(value: &Value) -> Option<RateLimitWindow> {
583666 let window_duration_mins = obj
584667 . get ( "windowDurationMins" )
585668 . or_else ( || obj. get ( "window_duration_mins" ) )
669+ . or_else ( || obj. get ( "window_minutes" ) )
586670 . and_then ( as_f64) ;
587671 let resets_at = obj
588672 . get ( "resetsAt" )
@@ -596,6 +680,11 @@ fn parse_window(value: &Value) -> Option<RateLimitWindow> {
596680 } )
597681}
598682
683+ fn read_string ( value : Option < & Value > ) -> Option < String > {
684+ let value = value?. as_str ( ) ?. trim ( ) ;
685+ ( !value. is_empty ( ) ) . then ( || value. to_string ( ) )
686+ }
687+
599688fn as_f64 ( value : & Value ) -> Option < f64 > {
600689 value
601690 . as_f64 ( )
@@ -618,12 +707,16 @@ mod tests {
618707 #[ test]
619708 fn parse_rate_limits_accepts_expected_shapes ( ) {
620709 let v = json ! ( {
710+ "limitId" : "codex" ,
711+ "planType" : "prolite" ,
621712 "primary" : { "usedPercent" : 0 , "windowDurationMins" : 300 , "resetsAt" : 1770118764 } ,
622- "secondary" : { "used_percent" : 1.0 , "window_duration_mins " : "10080" , "resets_at" : "1770684472" } ,
713+ "secondary" : { "used_percent" : 1.0 , "window_minutes " : "10080" , "resets_at" : "1770684472" } ,
623714 "credits" : { "hasCredits" : true , "unlimited" : false , "balance" : 900.735 } ,
624715 } ) ;
625716
626717 let limits = parse_rate_limits ( & v) . expect ( "parse_rate_limits" ) ;
718+ assert_eq ! ( limits. limit_id. as_deref( ) , Some ( "codex" ) ) ;
719+ assert_eq ! ( limits. plan_type. as_deref( ) , Some ( "prolite" ) ) ;
627720 let p = limits. primary . expect ( "primary" ) ;
628721 assert_eq ! ( p. used_percent, Some ( 0.0 ) ) ;
629722 assert_eq ! ( p. window_duration_mins, Some ( 300.0 ) ) ;
@@ -640,6 +733,48 @@ mod tests {
640733 assert_eq ! ( c. balance. as_deref( ) , Some ( "900.735" ) ) ;
641734 }
642735
736+ #[ test]
737+ fn parse_account_rate_limits_result_accepts_multi_bucket_response ( ) {
738+ let v = json ! ( {
739+ "rateLimitResetCredits" : { "availableCount" : 2 } ,
740+ "rateLimits" : {
741+ "limitId" : "codex" ,
742+ "primary" : { "usedPercent" : 10 , "windowDurationMins" : 300 } ,
743+ "secondary" : { "usedPercent" : 20 , "windowDurationMins" : 10080 }
744+ } ,
745+ "rateLimitsByLimitId" : {
746+ "codex_extra" : {
747+ "limitName" : "GPT-5.3-Codex-Spark" ,
748+ "primary" : { "usedPercent" : 30 , "windowDurationMins" : 300 }
749+ } ,
750+ "codex" : {
751+ "primary" : { "usedPercent" : 10 , "windowDurationMins" : 300 }
752+ }
753+ }
754+ } ) ;
755+
756+ let limits =
757+ parse_account_rate_limits_result ( & v) . expect ( "parse_account_rate_limits_result" ) ;
758+ assert_eq ! ( limits. limit_id. as_deref( ) , Some ( "codex" ) ) ;
759+ assert_eq ! ( limits. reset_credits_available, Some ( 2 ) ) ;
760+ assert_eq ! ( limits. buckets. len( ) , 2 ) ;
761+ assert_eq ! ( limits. buckets[ 0 ] . limit_id. as_deref( ) , Some ( "codex" ) ) ;
762+ assert_eq ! ( limits. buckets[ 1 ] . limit_id. as_deref( ) , Some ( "codex_extra" ) ) ;
763+ assert_eq ! (
764+ limits. buckets[ 1 ] . limit_name. as_deref( ) ,
765+ Some ( "GPT-5.3-Codex-Spark" )
766+ ) ;
767+ }
768+
769+ #[ test]
770+ fn recognizes_account_rate_limits_updated_notification ( ) {
771+ let v = json ! ( {
772+ "method" : "account/rateLimits/updated" ,
773+ "params" : { "rateLimits" : { "primary" : { "usedPercent" : 3 } } }
774+ } ) ;
775+ assert ! ( is_account_rate_limits_updated_notification( & v) ) ;
776+ }
777+
643778 #[ test]
644779 fn parse_rate_limits_rejects_non_object ( ) {
645780 let err = parse_rate_limits ( & Value :: Null ) . unwrap_err ( ) . to_string ( ) ;
0 commit comments