@@ -9,6 +9,7 @@ use codex_app_server_protocol::ApplyPatchApprovalParams;
99use codex_app_server_protocol:: ApplyPatchApprovalResponse ;
1010use codex_app_server_protocol:: ArchiveConversationParams ;
1111use codex_app_server_protocol:: ArchiveConversationResponse ;
12+ use codex_app_server_protocol:: AuthMode ;
1213use codex_app_server_protocol:: AuthStatusChangeNotification ;
1314use codex_app_server_protocol:: ClientRequest ;
1415use codex_app_server_protocol:: ConversationSummary ;
@@ -18,6 +19,7 @@ use codex_app_server_protocol::ExecOneOffCommandParams;
1819use codex_app_server_protocol:: ExecOneOffCommandResponse ;
1920use codex_app_server_protocol:: FuzzyFileSearchParams ;
2021use codex_app_server_protocol:: FuzzyFileSearchResponse ;
22+ use codex_app_server_protocol:: GetAccountRateLimitsResponse ;
2123use codex_app_server_protocol:: GetUserAgentResponse ;
2224use codex_app_server_protocol:: GetUserSavedConfigResponse ;
2325use codex_app_server_protocol:: GitDiffToRemoteResponse ;
@@ -49,6 +51,9 @@ use codex_app_server_protocol::SetDefaultModelParams;
4951use codex_app_server_protocol:: SetDefaultModelResponse ;
5052use codex_app_server_protocol:: UserInfoResponse ;
5153use codex_app_server_protocol:: UserSavedConfig ;
54+ use codex_backend_client:: Client as BackendClient ;
55+ use codex_backend_client:: types:: RateLimitStatusPayload ;
56+ use codex_backend_client:: types:: RateLimitWindowSnapshot ;
5257use codex_core:: AuthManager ;
5358use codex_core:: CodexConversation ;
5459use codex_core:: ConversationManager ;
@@ -87,6 +92,8 @@ use codex_protocol::ConversationId;
8792use codex_protocol:: models:: ContentItem ;
8893use codex_protocol:: models:: ResponseItem ;
8994use codex_protocol:: protocol:: InputMessageKind ;
95+ use codex_protocol:: protocol:: RateLimitSnapshot ;
96+ use codex_protocol:: protocol:: RateLimitWindow ;
9097use codex_protocol:: protocol:: USER_MESSAGE_BEGIN ;
9198use codex_utils_json_to_toml:: json_to_toml;
9299use std:: collections:: HashMap ;
@@ -239,6 +246,12 @@ impl CodexMessageProcessor {
239246 ClientRequest :: ExecOneOffCommand { request_id, params } => {
240247 self . exec_one_off_command ( request_id, params) . await ;
241248 }
249+ ClientRequest :: GetAccountRateLimits {
250+ request_id,
251+ params : _,
252+ } => {
253+ self . get_account_rate_limits ( request_id) . await ;
254+ }
242255 }
243256 }
244257
@@ -499,6 +512,70 @@ impl CodexMessageProcessor {
499512 self . outgoing . send_response ( request_id, response) . await ;
500513 }
501514
515+ async fn get_account_rate_limits ( & self , request_id : RequestId ) {
516+ match self . fetch_account_rate_limits ( ) . await {
517+ Ok ( rate_limits) => {
518+ let response = GetAccountRateLimitsResponse { rate_limits } ;
519+ self . outgoing . send_response ( request_id, response) . await ;
520+ }
521+ Err ( error) => {
522+ self . outgoing . send_error ( request_id, error) . await ;
523+ }
524+ }
525+ }
526+
527+ async fn fetch_account_rate_limits ( & self ) -> Result < RateLimitSnapshot , JSONRPCErrorError > {
528+ let Some ( auth) = self . auth_manager . auth ( ) else {
529+ return Err ( JSONRPCErrorError {
530+ code : INVALID_REQUEST_ERROR_CODE ,
531+ message : "codex account authentication required to read rate limits" . to_string ( ) ,
532+ data : None ,
533+ } ) ;
534+ } ;
535+
536+ if auth. mode != AuthMode :: ChatGPT {
537+ return Err ( JSONRPCErrorError {
538+ code : INVALID_REQUEST_ERROR_CODE ,
539+ message : "chatgpt authentication required to read rate limits" . to_string ( ) ,
540+ data : None ,
541+ } ) ;
542+ }
543+
544+ let token = auth. get_token ( ) . await . map_err ( |err| JSONRPCErrorError {
545+ code : INTERNAL_ERROR_CODE ,
546+ message : format ! ( "failed to read codex auth token: {err}" ) ,
547+ data : None ,
548+ } ) ?;
549+
550+ let mut client =
551+ BackendClient :: new ( self . config . chatgpt_base_url . clone ( ) ) . map_err ( |err| {
552+ JSONRPCErrorError {
553+ code : INTERNAL_ERROR_CODE ,
554+ message : format ! ( "failed to construct backend client: {err}" ) ,
555+ data : None ,
556+ }
557+ } ) ?;
558+
559+ client = client
560+ . with_user_agent ( get_codex_user_agent ( ) )
561+ . with_bearer_token ( token) ;
562+
563+ if let Some ( account_id) = auth. get_account_id ( ) {
564+ client = client. with_chatgpt_account_id ( account_id) ;
565+ }
566+
567+ let payload = client
568+ . get_rate_limits ( )
569+ . await
570+ . map_err ( |err| JSONRPCErrorError {
571+ code : INTERNAL_ERROR_CODE ,
572+ message : format ! ( "failed to fetch codex rate limits: {err}" ) ,
573+ data : None ,
574+ } ) ?;
575+
576+ Ok ( rate_limit_snapshot_from_payload ( payload) )
577+ }
578+
502579 async fn get_user_saved_config ( & self , request_id : RequestId ) {
503580 let toml_value = match load_config_as_toml ( & self . config . codex_home ) . await {
504581 Ok ( val) => val,
@@ -1364,6 +1441,50 @@ async fn derive_config_from_params(
13641441 Config :: load_with_cli_overrides ( cli_overrides, overrides) . await
13651442}
13661443
1444+ fn rate_limit_snapshot_from_payload ( payload : RateLimitStatusPayload ) -> RateLimitSnapshot {
1445+ let Some ( details) = payload
1446+ . rate_limit
1447+ . and_then ( |inner| inner. map ( |boxed| * boxed) )
1448+ else {
1449+ return RateLimitSnapshot {
1450+ primary : None ,
1451+ secondary : None ,
1452+ } ;
1453+ } ;
1454+
1455+ RateLimitSnapshot {
1456+ primary : map_rate_limit_window ( details. primary_window ) ,
1457+ secondary : map_rate_limit_window ( details. secondary_window ) ,
1458+ }
1459+ }
1460+
1461+ fn map_rate_limit_window (
1462+ window : Option < Option < Box < RateLimitWindowSnapshot > > > ,
1463+ ) -> Option < RateLimitWindow > {
1464+ let snapshot = match window {
1465+ Some ( Some ( snapshot) ) => * snapshot,
1466+ _ => return None ,
1467+ } ;
1468+
1469+ let used_percent = f64:: from ( snapshot. used_percent ) ;
1470+ let window_minutes = window_minutes_from_seconds ( snapshot. limit_window_seconds ) ;
1471+ let resets_at = snapshot. reset_at ;
1472+ Some ( RateLimitWindow {
1473+ used_percent,
1474+ window_minutes,
1475+ resets_at,
1476+ } )
1477+ }
1478+
1479+ fn window_minutes_from_seconds ( seconds : i32 ) -> Option < u64 > {
1480+ if seconds <= 0 {
1481+ return None ;
1482+ }
1483+
1484+ let seconds_u64 = seconds as u64 ;
1485+ Some ( seconds_u64. div_ceil ( 60 ) )
1486+ }
1487+
13671488async fn on_patch_approval_response (
13681489 event_id : String ,
13691490 receiver : oneshot:: Receiver < JsonRpcResult > ,
0 commit comments