[codex] Propagate rate limit reached type (#18227)

## Summary

First PR in the split from #17956.

- adds the core/app-server `RateLimitReachedType` shape
- maps backend `rate_limit_reached_type` into Codex rate-limit snapshots
- carries the field through app-server notifications/responses and
generated schemas
- updates existing constructors/tests for the new optional field

## Validation

- `cargo test -p codex-backend-client`
- `cargo test -p codex-app-server-protocol`
- `cargo test -p codex-app-server rate_limits`
- `cargo test -p codex-tui workspace_`
- `cargo test -p codex-tui status_`
- `just fmt`
- `just fix -p codex-backend-client`
- `just fix -p codex-app-server-protocol`
- `just fix -p codex-app-server`
- `just fix -p codex-tui`
This commit is contained in:
richardopenai
2026-04-17 13:37:25 -07:00
committed by GitHub
parent f017a23835
commit 139fa8b8f2
27 changed files with 371 additions and 6 deletions
+100
View File
@@ -1,6 +1,7 @@
use crate::types::CodeTaskDetailsResponse;
use crate::types::ConfigFileResponse;
use crate::types::PaginatedListTaskListItem;
use crate::types::RateLimitReachedKind as BackendRateLimitReachedKind;
use crate::types::RateLimitStatusPayload;
use crate::types::TurnAttemptsSiblingTurnsResponse;
use anyhow::Result;
@@ -9,6 +10,7 @@ use codex_login::CodexAuth;
use codex_login::default_client::get_codex_user_agent;
use codex_protocol::account::PlanType as AccountPlanType;
use codex_protocol::protocol::CreditsSnapshot;
use codex_protocol::protocol::RateLimitReachedType;
use codex_protocol::protocol::RateLimitSnapshot;
use codex_protocol::protocol::RateLimitWindow;
use reqwest::StatusCode;
@@ -412,12 +414,17 @@ impl Client {
payload: RateLimitStatusPayload,
) -> Vec<RateLimitSnapshot> {
let plan_type = Some(Self::map_plan_type(payload.plan_type));
let rate_limit_reached_type = payload
.rate_limit_reached_type
.flatten()
.and_then(|details| Self::map_rate_limit_reached_type(details.kind));
let mut snapshots = vec![Self::make_rate_limit_snapshot(
Some("codex".to_string()),
/*limit_name*/ None,
payload.rate_limit.flatten().map(|details| *details),
payload.credits.flatten().map(|details| *details),
plan_type,
rate_limit_reached_type,
)];
if let Some(additional) = payload.additional_rate_limits.flatten() {
snapshots.extend(additional.into_iter().map(|details| {
@@ -427,6 +434,7 @@ impl Client {
details.rate_limit.flatten().map(|rate_limit| *rate_limit),
/*credits*/ None,
plan_type,
/*rate_limit_reached_type*/ None,
)
}));
}
@@ -439,6 +447,7 @@ impl Client {
rate_limit: Option<crate::types::RateLimitStatusDetails>,
credits: Option<crate::types::CreditStatusDetails>,
plan_type: Option<AccountPlanType>,
rate_limit_reached_type: Option<RateLimitReachedType>,
) -> RateLimitSnapshot {
let (primary, secondary) = match rate_limit {
Some(details) => (
@@ -454,6 +463,30 @@ impl Client {
secondary,
credits: Self::map_credits(credits),
plan_type,
rate_limit_reached_type,
}
}
fn map_rate_limit_reached_type(
kind: BackendRateLimitReachedKind,
) -> Option<RateLimitReachedType> {
match kind {
BackendRateLimitReachedKind::RateLimitReached => {
Some(RateLimitReachedType::RateLimitReached)
}
BackendRateLimitReachedKind::WorkspaceOwnerCreditsDepleted => {
Some(RateLimitReachedType::WorkspaceOwnerCreditsDepleted)
}
BackendRateLimitReachedKind::WorkspaceMemberCreditsDepleted => {
Some(RateLimitReachedType::WorkspaceMemberCreditsDepleted)
}
BackendRateLimitReachedKind::WorkspaceOwnerUsageLimitReached => {
Some(RateLimitReachedType::WorkspaceOwnerUsageLimitReached)
}
BackendRateLimitReachedKind::WorkspaceMemberUsageLimitReached => {
Some(RateLimitReachedType::WorkspaceMemberUsageLimitReached)
}
BackendRateLimitReachedKind::Unknown => None,
}
}
@@ -521,6 +554,8 @@ impl Client {
mod tests {
use super::*;
use codex_backend_openapi_models::models::AdditionalRateLimitDetails;
use codex_backend_openapi_models::models::RateLimitReachedKind;
use codex_backend_openapi_models::models::RateLimitReachedType as BackendRateLimitReachedType;
use pretty_assertions::assert_eq;
#[test]
@@ -574,6 +609,9 @@ mod tests {
balance: Some(Some("9.99".to_string())),
..Default::default()
}))),
rate_limit_reached_type: Some(Some(BackendRateLimitReachedType {
kind: RateLimitReachedKind::WorkspaceMemberCreditsDepleted,
})),
};
let snapshots = Client::rate_limit_snapshots_from_payload(payload);
@@ -598,6 +636,10 @@ mod tests {
})
);
assert_eq!(snapshots[0].plan_type, Some(AccountPlanType::Pro));
assert_eq!(
snapshots[0].rate_limit_reached_type,
Some(RateLimitReachedType::WorkspaceMemberCreditsDepleted)
);
assert_eq!(snapshots[1].limit_id.as_deref(), Some("codex_other"));
assert_eq!(snapshots[1].limit_name.as_deref(), Some("codex_other"));
@@ -607,6 +649,7 @@ mod tests {
);
assert_eq!(snapshots[1].credits, None);
assert_eq!(snapshots[1].plan_type, Some(AccountPlanType::Pro));
assert_eq!(snapshots[1].rate_limit_reached_type, None);
}
#[test]
@@ -620,6 +663,7 @@ mod tests {
rate_limit: None,
}])),
credits: None,
rate_limit_reached_type: None,
};
let snapshots = Client::rate_limit_snapshots_from_payload(payload);
@@ -645,6 +689,7 @@ mod tests {
secondary: None,
credits: None,
plan_type: Some(AccountPlanType::Pro),
rate_limit_reached_type: None,
},
RateLimitSnapshot {
limit_id: Some("codex".to_string()),
@@ -657,6 +702,7 @@ mod tests {
secondary: None,
credits: None,
plan_type: Some(AccountPlanType::Pro),
rate_limit_reached_type: None,
},
];
@@ -667,4 +713,58 @@ mod tests {
.unwrap_or_else(|| snapshots[0].clone());
assert_eq!(preferred.limit_id.as_deref(), Some("codex"));
}
#[test]
fn usage_payload_maps_every_rate_limit_reached_type() {
let cases = [
(
RateLimitReachedKind::RateLimitReached,
Some(RateLimitReachedType::RateLimitReached),
),
(
RateLimitReachedKind::WorkspaceOwnerCreditsDepleted,
Some(RateLimitReachedType::WorkspaceOwnerCreditsDepleted),
),
(
RateLimitReachedKind::WorkspaceMemberCreditsDepleted,
Some(RateLimitReachedType::WorkspaceMemberCreditsDepleted),
),
(
RateLimitReachedKind::WorkspaceOwnerUsageLimitReached,
Some(RateLimitReachedType::WorkspaceOwnerUsageLimitReached),
),
(
RateLimitReachedKind::WorkspaceMemberUsageLimitReached,
Some(RateLimitReachedType::WorkspaceMemberUsageLimitReached),
),
(RateLimitReachedKind::Unknown, None),
];
for (kind, expected) in cases {
let payload = RateLimitStatusPayload {
plan_type: crate::types::PlanType::Plus,
rate_limit: None,
credits: None,
additional_rate_limits: None,
rate_limit_reached_type: Some(Some(BackendRateLimitReachedType { kind })),
};
let snapshots = Client::rate_limit_snapshots_from_payload(payload);
assert_eq!(snapshots[0].rate_limit_reached_type, expected);
}
}
#[test]
fn usage_payload_preserves_absent_rate_limit_reached_type() {
let payload = RateLimitStatusPayload {
plan_type: crate::types::PlanType::Plus,
rate_limit: None,
credits: None,
additional_rate_limits: None,
rate_limit_reached_type: None,
};
let snapshots = Client::rate_limit_snapshots_from_payload(payload);
assert_eq!(snapshots[0].rate_limit_reached_type, None);
}
}
+1
View File
@@ -2,6 +2,7 @@ pub use codex_backend_openapi_models::models::ConfigFileResponse;
pub use codex_backend_openapi_models::models::CreditStatusDetails;
pub use codex_backend_openapi_models::models::PaginatedListTaskListItem;
pub use codex_backend_openapi_models::models::PlanType;
pub use codex_backend_openapi_models::models::RateLimitReachedKind;
pub use codex_backend_openapi_models::models::RateLimitStatusDetails;
pub use codex_backend_openapi_models::models::RateLimitStatusPayload;
pub use codex_backend_openapi_models::models::RateLimitWindowSnapshot;