diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index c7fcefae02..a1c60e241a 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -1,7 +1,6 @@ use codex_protocol::ConversationId; use codex_protocol::account::PlanType; use codex_protocol::config_types::ReasoningEffort; -use codex_protocol::protocol::RateLimitSnapshot; use mcp_types::ContentBlock as McpContentBlock; use schemars::JsonSchema; use serde::Deserialize; @@ -435,7 +434,48 @@ pub struct TodoItem { #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct AccountRateLimitsUpdatedNotification { - // TODO: create our own RateLimitSnapshot type that doesn't depend on codex_protocol - // so we can camelcase that bad boy. pub rate_limits: RateLimitSnapshot, } + +// CamelCased copy of codex_protocol::protocol::{RateLimitSnapshot, RateLimitWindow} +// for the v2 surface. This avoids leaking snake_case into the v2 wire format. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct RateLimitSnapshot { + pub primary: Option, + pub secondary: Option, +} + +impl From for RateLimitSnapshot { + fn from(value: codex_protocol::protocol::RateLimitSnapshot) -> Self { + Self { + primary: value.primary.map(RateLimitWindow::from), + secondary: value.secondary.map(RateLimitWindow::from), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct RateLimitWindow { + /// Percentage (0-100) of the window that has been consumed. + pub used_percent: f64, + /// Rolling window duration, in minutes. + #[ts(type = "number | null")] + pub window_minutes: Option, + /// Unix timestamp (seconds since epoch) when the window resets. + #[ts(type = "number | null")] + pub resets_at: Option, +} + +impl From for RateLimitWindow { + fn from(value: codex_protocol::protocol::RateLimitWindow) -> Self { + Self { + used_percent: value.used_percent, + window_minutes: value.window_minutes, + resets_at: value.resets_at, + } + } +} diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 7940c5231d..2758399dcc 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -4,6 +4,7 @@ use crate::fuzzy_file_search::run_fuzzy_file_search; use crate::models::supported_models; use crate::outgoing_message::OutgoingMessageSender; use crate::outgoing_message::OutgoingNotification; +use codex_app_server_protocol::AccountRateLimitsUpdatedNotification; use codex_app_server_protocol::AddConversationListenerParams; use codex_app_server_protocol::AddConversationSubscriptionResponse; use codex_app_server_protocol::ApplyPatchApprovalParams; @@ -42,6 +43,7 @@ use codex_app_server_protocol::ModelListParams; use codex_app_server_protocol::ModelListResponse; use codex_app_server_protocol::NewConversationParams; use codex_app_server_protocol::NewConversationResponse; +use codex_app_server_protocol::RateLimitSnapshot; use codex_app_server_protocol::RemoveConversationListenerParams; use codex_app_server_protocol::RemoveConversationSubscriptionResponse; use codex_app_server_protocol::RequestId; @@ -646,7 +648,9 @@ impl CodexMessageProcessor { async fn get_account_rate_limits(&self, request_id: RequestId) { match self.fetch_account_rate_limits().await { Ok(rate_limits) => { - let response = GetAccountRateLimitsResponse { rate_limits }; + let response = GetAccountRateLimitsResponse { + rate_limits: rate_limits.into(), + }; self.outgoing.send_response(request_id, response).await; } Err(error) => { @@ -1786,9 +1790,12 @@ async fn apply_bespoke_event_handling( } EventMsg::TokenCount(token_count_event) => { if let Some(rate_limits) = token_count_event.rate_limits { + let snapshot: RateLimitSnapshot = rate_limits.into(); outgoing .send_server_notification(ServerNotification::AccountRateLimitsUpdated( - rate_limits, + AccountRateLimitsUpdatedNotification { + rate_limits: snapshot, + }, )) .await; } diff --git a/codex-rs/app-server/src/outgoing_message.rs b/codex-rs/app-server/src/outgoing_message.rs index 3f4497fb23..d148c1ee38 100644 --- a/codex-rs/app-server/src/outgoing_message.rs +++ b/codex-rs/app-server/src/outgoing_message.rs @@ -142,8 +142,8 @@ pub(crate) struct OutgoingError { #[cfg(test)] mod tests { use codex_app_server_protocol::LoginChatGptCompleteNotification; - use codex_protocol::protocol::RateLimitSnapshot; - use codex_protocol::protocol::RateLimitWindow; + use codex_app_server_protocol::RateLimitSnapshot; + use codex_app_server_protocol::RateLimitWindow; use pretty_assertions::assert_eq; use serde_json::json; use uuid::Uuid; @@ -177,26 +177,32 @@ mod tests { #[test] fn verify_account_rate_limits_notification_serialization() { - let notification = ServerNotification::AccountRateLimitsUpdated(RateLimitSnapshot { - primary: Some(RateLimitWindow { - used_percent: 25.0, - window_minutes: Some(15), - resets_at: Some(123), - }), - secondary: None, - }); + let notification = ServerNotification::AccountRateLimitsUpdated( + codex_app_server_protocol::AccountRateLimitsUpdatedNotification { + rate_limits: RateLimitSnapshot { + primary: Some(RateLimitWindow { + used_percent: 25.0, + window_minutes: Some(15), + resets_at: Some(123), + }), + secondary: None, + }, + }, + ); let jsonrpc_notification = OutgoingMessage::AppServerNotification(notification); assert_eq!( json!({ "method": "account/rateLimits/updated", "params": { - "primary": { - "used_percent": 25.0, - "window_minutes": 15, - "resets_at": 123, - }, - "secondary": null, + "rateLimits": { + "primary": { + "usedPercent": 25.0, + "windowMinutes": 15, + "resetsAt": 123 + }, + "secondary": null + } }, }), serde_json::to_value(jsonrpc_notification) diff --git a/codex-rs/app-server/tests/suite/rate_limits.rs b/codex-rs/app-server/tests/suite/rate_limits.rs index 16cbf153e9..5fc85eacff 100644 --- a/codex-rs/app-server/tests/suite/rate_limits.rs +++ b/codex-rs/app-server/tests/suite/rate_limits.rs @@ -7,10 +7,10 @@ use codex_app_server_protocol::GetAccountRateLimitsResponse; use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::LoginApiKeyParams; +use codex_app_server_protocol::RateLimitSnapshot; +use codex_app_server_protocol::RateLimitWindow; use codex_app_server_protocol::RequestId; use codex_core::auth::AuthCredentialsStoreMode; -use codex_protocol::protocol::RateLimitSnapshot; -use codex_protocol::protocol::RateLimitWindow; use pretty_assertions::assert_eq; use serde_json::json; use std::path::Path;