[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

View File

@@ -2046,6 +2046,16 @@
],
"type": "string"
},
"RateLimitReachedType": {
"enum": [
"rate_limit_reached",
"workspace_owner_credits_depleted",
"workspace_member_credits_depleted",
"workspace_owner_usage_limit_reached",
"workspace_member_usage_limit_reached"
],
"type": "string"
},
"RateLimitSnapshot": {
"properties": {
"credits": {
@@ -2090,6 +2100,16 @@
}
]
},
"rateLimitReachedType": {
"anyOf": [
{
"$ref": "#/definitions/RateLimitReachedType"
},
{
"type": "null"
}
]
},
"secondary": {
"anyOf": [
{

View File

@@ -10692,6 +10692,16 @@
},
"type": "object"
},
"RateLimitReachedType": {
"enum": [
"rate_limit_reached",
"workspace_owner_credits_depleted",
"workspace_member_credits_depleted",
"workspace_owner_usage_limit_reached",
"workspace_member_usage_limit_reached"
],
"type": "string"
},
"RateLimitSnapshot": {
"properties": {
"credits": {
@@ -10736,6 +10746,16 @@
}
]
},
"rateLimitReachedType": {
"anyOf": [
{
"$ref": "#/definitions/v2/RateLimitReachedType"
},
{
"type": "null"
}
]
},
"secondary": {
"anyOf": [
{

View File

@@ -7444,6 +7444,16 @@
},
"type": "object"
},
"RateLimitReachedType": {
"enum": [
"rate_limit_reached",
"workspace_owner_credits_depleted",
"workspace_member_credits_depleted",
"workspace_owner_usage_limit_reached",
"workspace_member_usage_limit_reached"
],
"type": "string"
},
"RateLimitSnapshot": {
"properties": {
"credits": {
@@ -7488,6 +7498,16 @@
}
]
},
"rateLimitReachedType": {
"anyOf": [
{
"$ref": "#/definitions/RateLimitReachedType"
},
{
"type": "null"
}
]
},
"secondary": {
"anyOf": [
{

View File

@@ -39,6 +39,16 @@
],
"type": "string"
},
"RateLimitReachedType": {
"enum": [
"rate_limit_reached",
"workspace_owner_credits_depleted",
"workspace_member_credits_depleted",
"workspace_owner_usage_limit_reached",
"workspace_member_usage_limit_reached"
],
"type": "string"
},
"RateLimitSnapshot": {
"properties": {
"credits": {
@@ -83,6 +93,16 @@
}
]
},
"rateLimitReachedType": {
"anyOf": [
{
"$ref": "#/definitions/RateLimitReachedType"
},
{
"type": "null"
}
]
},
"secondary": {
"anyOf": [
{

View File

@@ -39,6 +39,16 @@
],
"type": "string"
},
"RateLimitReachedType": {
"enum": [
"rate_limit_reached",
"workspace_owner_credits_depleted",
"workspace_member_credits_depleted",
"workspace_owner_usage_limit_reached",
"workspace_member_usage_limit_reached"
],
"type": "string"
},
"RateLimitSnapshot": {
"properties": {
"credits": {
@@ -83,6 +93,16 @@
}
]
},
"rateLimitReachedType": {
"anyOf": [
{
"$ref": "#/definitions/RateLimitReachedType"
},
{
"type": "null"
}
]
},
"secondary": {
"anyOf": [
{

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type RateLimitReachedType = "rate_limit_reached" | "workspace_owner_credits_depleted" | "workspace_member_credits_depleted" | "workspace_owner_usage_limit_reached" | "workspace_member_usage_limit_reached";

View File

@@ -3,6 +3,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PlanType } from "../PlanType";
import type { CreditsSnapshot } from "./CreditsSnapshot";
import type { RateLimitReachedType } from "./RateLimitReachedType";
import type { RateLimitWindow } from "./RateLimitWindow";
export type RateLimitSnapshot = { limitId: string | null, limitName: string | null, primary: RateLimitWindow | null, secondary: RateLimitWindow | null, credits: CreditsSnapshot | null, planType: PlanType | null, };
export type RateLimitSnapshot = { limitId: string | null, limitName: string | null, primary: RateLimitWindow | null, secondary: RateLimitWindow | null, credits: CreditsSnapshot | null, planType: PlanType | null, rateLimitReachedType: RateLimitReachedType | null, };

View File

@@ -241,6 +241,7 @@ export type { PluginUninstallParams } from "./PluginUninstallParams";
export type { PluginUninstallResponse } from "./PluginUninstallResponse";
export type { PluginsMigration } from "./PluginsMigration";
export type { ProfileV2 } from "./ProfileV2";
export type { RateLimitReachedType } from "./RateLimitReachedType";
export type { RateLimitSnapshot } from "./RateLimitSnapshot";
export type { RateLimitWindow } from "./RateLimitWindow";
export type { RawResponseItemCompletedNotification } from "./RawResponseItemCompletedNotification";

View File

@@ -70,6 +70,7 @@ use codex_protocol::protocol::ModelRerouteReason as CoreModelRerouteReason;
use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess;
use codex_protocol::protocol::NonSteerableTurnKind as CoreNonSteerableTurnKind;
use codex_protocol::protocol::PatchApplyStatus as CorePatchApplyStatus;
use codex_protocol::protocol::RateLimitReachedType as CoreRateLimitReachedType;
use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot;
use codex_protocol::protocol::RateLimitWindow as CoreRateLimitWindow;
use codex_protocol::protocol::ReadOnlyAccess as CoreReadOnlyAccess;
@@ -6529,6 +6530,7 @@ pub struct RateLimitSnapshot {
pub secondary: Option<RateLimitWindow>,
pub credits: Option<CreditsSnapshot>,
pub plan_type: Option<PlanType>,
pub rate_limit_reached_type: Option<RateLimitReachedType>,
}
impl From<CoreRateLimitSnapshot> for RateLimitSnapshot {
@@ -6540,6 +6542,60 @@ impl From<CoreRateLimitSnapshot> for RateLimitSnapshot {
secondary: value.secondary.map(RateLimitWindow::from),
credits: value.credits.map(CreditsSnapshot::from),
plan_type: value.plan_type,
rate_limit_reached_type: value
.rate_limit_reached_type
.map(RateLimitReachedType::from),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export_to = "v2/", rename_all = "snake_case")]
pub enum RateLimitReachedType {
RateLimitReached,
WorkspaceOwnerCreditsDepleted,
WorkspaceMemberCreditsDepleted,
WorkspaceOwnerUsageLimitReached,
WorkspaceMemberUsageLimitReached,
}
impl From<CoreRateLimitReachedType> for RateLimitReachedType {
fn from(value: CoreRateLimitReachedType) -> Self {
match value {
CoreRateLimitReachedType::RateLimitReached => Self::RateLimitReached,
CoreRateLimitReachedType::WorkspaceOwnerCreditsDepleted => {
Self::WorkspaceOwnerCreditsDepleted
}
CoreRateLimitReachedType::WorkspaceMemberCreditsDepleted => {
Self::WorkspaceMemberCreditsDepleted
}
CoreRateLimitReachedType::WorkspaceOwnerUsageLimitReached => {
Self::WorkspaceOwnerUsageLimitReached
}
CoreRateLimitReachedType::WorkspaceMemberUsageLimitReached => {
Self::WorkspaceMemberUsageLimitReached
}
}
}
}
impl From<RateLimitReachedType> for CoreRateLimitReachedType {
fn from(value: RateLimitReachedType) -> Self {
match value {
RateLimitReachedType::RateLimitReached => Self::RateLimitReached,
RateLimitReachedType::WorkspaceOwnerCreditsDepleted => {
Self::WorkspaceOwnerCreditsDepleted
}
RateLimitReachedType::WorkspaceMemberCreditsDepleted => {
Self::WorkspaceMemberCreditsDepleted
}
RateLimitReachedType::WorkspaceOwnerUsageLimitReached => {
Self::WorkspaceOwnerUsageLimitReached
}
RateLimitReachedType::WorkspaceMemberUsageLimitReached => {
Self::WorkspaceMemberUsageLimitReached
}
}
}
}

View File

@@ -4138,6 +4138,7 @@ mod tests {
balance: Some("5".to_string()),
}),
plan_type: None,
rate_limit_reached_type: None,
};
handle_token_count_event(

View File

@@ -736,6 +736,7 @@ mod tests {
secondary: None,
credits: None,
plan_type: Some(PlanType::Plus),
rate_limit_reached_type: None,
},
});
@@ -754,7 +755,8 @@ mod tests {
},
"secondary": null,
"credits": null,
"planType": "plus"
"planType": "plus",
"rateLimitReachedType": null
}
},
}),

View File

@@ -7,6 +7,7 @@ use codex_app_server_protocol::GetAccountRateLimitsResponse;
use codex_app_server_protocol::JSONRPCError;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::LoginAccountResponse;
use codex_app_server_protocol::RateLimitReachedType;
use codex_app_server_protocol::RateLimitSnapshot;
use codex_app_server_protocol::RateLimitWindow;
use codex_app_server_protocol::RequestId;
@@ -118,6 +119,9 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> {
"reset_at": secondary_reset_timestamp,
}
},
"rate_limit_reached_type": {
"type": "workspace_member_usage_limit_reached",
},
"additional_rate_limits": [
{
"limit_name": "codex_other",
@@ -173,6 +177,7 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> {
}),
credits: None,
plan_type: Some(AccountPlanType::Pro),
rate_limit_reached_type: Some(RateLimitReachedType::WorkspaceMemberUsageLimitReached),
},
rate_limits_by_limit_id: Some(
[
@@ -193,6 +198,9 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> {
}),
credits: None,
plan_type: Some(AccountPlanType::Pro),
rate_limit_reached_type: Some(
RateLimitReachedType::WorkspaceMemberUsageLimitReached,
),
},
),
(
@@ -208,6 +216,7 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> {
secondary: None,
credits: None,
plan_type: Some(AccountPlanType::Pro),
rate_limit_reached_type: None,
},
),
]

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);
}
}

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;

View File

@@ -93,6 +93,7 @@ pub fn parse_rate_limit_for_limit(
secondary,
credits,
plan_type: None,
rate_limit_reached_type: None,
})
}
@@ -156,6 +157,7 @@ pub fn parse_rate_limit_event(payload: &str) -> Option<RateLimitSnapshot> {
secondary,
credits,
plan_type: event.plan_type,
rate_limit_reached_type: None,
})
}

View File

@@ -32,6 +32,8 @@ pub use self::additional_rate_limit_details::AdditionalRateLimitDetails;
pub(crate) mod rate_limit_status_payload;
pub use self::rate_limit_status_payload::PlanType;
pub use self::rate_limit_status_payload::RateLimitReachedKind;
pub use self::rate_limit_status_payload::RateLimitReachedType;
pub use self::rate_limit_status_payload::RateLimitStatusPayload;
pub(crate) mod rate_limit_status_details;

View File

@@ -37,6 +37,13 @@ pub struct RateLimitStatusPayload {
skip_serializing_if = "Option::is_none"
)]
pub additional_rate_limits: Option<Option<Vec<models::AdditionalRateLimitDetails>>>,
#[serde(
rename = "rate_limit_reached_type",
default,
with = "::serde_with::rust::double_option",
skip_serializing_if = "Option::is_none"
)]
pub rate_limit_reached_type: Option<Option<RateLimitReachedType>>,
}
impl RateLimitStatusPayload {
@@ -46,10 +53,36 @@ impl RateLimitStatusPayload {
rate_limit: None,
credits: None,
additional_rate_limits: None,
rate_limit_reached_type: None,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RateLimitReachedType {
#[serde(rename = "type")]
pub kind: RateLimitReachedKind,
}
#[derive(
Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, Default,
)]
pub enum RateLimitReachedKind {
#[serde(rename = "rate_limit_reached")]
RateLimitReached,
#[serde(rename = "workspace_owner_credits_depleted")]
WorkspaceOwnerCreditsDepleted,
#[serde(rename = "workspace_member_credits_depleted")]
WorkspaceMemberCreditsDepleted,
#[serde(rename = "workspace_owner_usage_limit_reached")]
WorkspaceOwnerUsageLimitReached,
#[serde(rename = "workspace_member_usage_limit_reached")]
WorkspaceMemberUsageLimitReached,
#[serde(rename = "unknown", other)]
#[default]
Unknown,
}
#[derive(
Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize, Default,
)]

View File

@@ -2070,6 +2070,7 @@ async fn set_rate_limits_retains_previous_credits() {
balance: Some("10.00".to_string()),
}),
plan_type: Some(codex_protocol::account::PlanType::Plus),
rate_limit_reached_type: None,
};
state.set_rate_limits(initial.clone());
@@ -2088,6 +2089,7 @@ async fn set_rate_limits_retains_previous_credits() {
}),
credits: None,
plan_type: None,
rate_limit_reached_type: None,
};
state.set_rate_limits(update.clone());
@@ -2100,6 +2102,7 @@ async fn set_rate_limits_retains_previous_credits() {
secondary: update.secondary,
credits: initial.credits,
plan_type: initial.plan_type,
rate_limit_reached_type: None,
})
);
}
@@ -2176,6 +2179,7 @@ async fn set_rate_limits_updates_plan_type_when_present() {
balance: Some("15.00".to_string()),
}),
plan_type: Some(codex_protocol::account::PlanType::Plus),
rate_limit_reached_type: None,
};
state.set_rate_limits(initial.clone());
@@ -2190,6 +2194,7 @@ async fn set_rate_limits_updates_plan_type_when_present() {
secondary: None,
credits: None,
plan_type: Some(codex_protocol::account::PlanType::Pro),
rate_limit_reached_type: None,
};
state.set_rate_limits(update.clone());
@@ -2202,6 +2207,7 @@ async fn set_rate_limits_updates_plan_type_when_present() {
secondary: update.secondary,
credits: initial.credits,
plan_type: update.plan_type,
rate_limit_reached_type: None,
})
);
}

View File

@@ -87,6 +87,7 @@ async fn set_rate_limits_defaults_limit_id_to_codex_when_missing() {
secondary: None,
credits: None,
plan_type: None,
rate_limit_reached_type: None,
});
assert_eq!(
@@ -114,6 +115,7 @@ async fn set_rate_limits_defaults_to_codex_when_limit_id_missing_after_other_buc
secondary: None,
credits: None,
plan_type: None,
rate_limit_reached_type: None,
});
state.set_rate_limits(RateLimitSnapshot {
limit_id: None,
@@ -126,6 +128,7 @@ async fn set_rate_limits_defaults_to_codex_when_limit_id_missing_after_other_buc
secondary: None,
credits: None,
plan_type: None,
rate_limit_reached_type: None,
});
assert_eq!(
@@ -157,6 +160,7 @@ async fn set_rate_limits_carries_credits_and_plan_type_from_codex_to_codex_other
balance: Some("50".to_string()),
}),
plan_type: Some(codex_protocol::account::PlanType::Plus),
rate_limit_reached_type: None,
});
state.set_rate_limits(RateLimitSnapshot {
@@ -170,6 +174,7 @@ async fn set_rate_limits_carries_credits_and_plan_type_from_codex_to_codex_other
secondary: None,
credits: None,
plan_type: None,
rate_limit_reached_type: None,
});
assert_eq!(
@@ -189,6 +194,7 @@ async fn set_rate_limits_carries_credits_and_plan_type_from_codex_to_codex_other
balance: Some("50".to_string()),
}),
plan_type: Some(codex_protocol::account::PlanType::Plus),
rate_limit_reached_type: None,
})
);
}

View File

@@ -2375,7 +2375,8 @@ async fn token_count_includes_rate_limits_snapshot() {
"resets_at": 1704074400
},
"credits": null,
"plan_type": null
"plan_type": null,
"rate_limit_reached_type": null
}
})
);
@@ -2426,7 +2427,8 @@ async fn token_count_includes_rate_limits_snapshot() {
"resets_at": 1704074400
},
"credits": null,
"plan_type": null
"plan_type": null,
"rate_limit_reached_type": null
}
})
);
@@ -2500,7 +2502,8 @@ async fn usage_limit_error_emits_rate_limit_event() -> anyhow::Result<()> {
"resets_at": null
},
"credits": null,
"plan_type": null
"plan_type": null,
"rate_limit_reached_type": null
});
let submission_id = codex

View File

@@ -1033,7 +1033,8 @@ async fn responses_websocket_usage_limit_error_emits_rate_limit_event() {
"resets_at": null
},
"credits": null,
"plan_type": null
"plan_type": null,
"rate_limit_reached_type": null
}
})
);

View File

@@ -36,6 +36,7 @@ fn rate_limit_snapshot() -> RateLimitSnapshot {
}),
credits: None,
plan_type: None,
rate_limit_reached_type: None,
}
}

View File

@@ -2200,6 +2200,18 @@ pub struct RateLimitSnapshot {
pub secondary: Option<RateLimitWindow>,
pub credits: Option<CreditsSnapshot>,
pub plan_type: Option<crate::account::PlanType>,
pub rate_limit_reached_type: Option<RateLimitReachedType>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
pub enum RateLimitReachedType {
RateLimitReached,
WorkspaceOwnerCreditsDepleted,
WorkspaceMemberCreditsDepleted,
WorkspaceOwnerUsageLimitReached,
WorkspaceMemberUsageLimitReached,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]

View File

@@ -1188,6 +1188,7 @@ pub(crate) fn app_server_rate_limit_snapshot_to_core(
secondary: snapshot.secondary.map(app_server_rate_limit_window_to_core),
credits: snapshot.credits.map(app_server_credits_snapshot_to_core),
plan_type: snapshot.plan_type,
rate_limit_reached_type: snapshot.rate_limit_reached_type.map(Into::into),
}
}

View File

@@ -106,6 +106,7 @@ pub(super) fn snapshot(percent: f64) -> RateLimitSnapshot {
secondary: None,
credits: None,
plan_type: None,
rate_limit_reached_type: None,
}
}

View File

@@ -235,6 +235,7 @@ async fn rate_limit_snapshot_keeps_prior_credits_when_missing_from_headers() {
balance: Some("17.5".to_string()),
}),
plan_type: None,
rate_limit_reached_type: None,
}));
let initial_balance = chat
.rate_limit_snapshots_by_limit_id
@@ -254,6 +255,7 @@ async fn rate_limit_snapshot_keeps_prior_credits_when_missing_from_headers() {
secondary: None,
credits: None,
plan_type: None,
rate_limit_reached_type: None,
}));
let display = chat
@@ -292,6 +294,7 @@ async fn rate_limit_snapshot_updates_and_retains_plan_type() {
}),
credits: None,
plan_type: Some(PlanType::Plus),
rate_limit_reached_type: None,
}));
assert_eq!(chat.plan_type, Some(PlanType::Plus));
@@ -310,6 +313,7 @@ async fn rate_limit_snapshot_updates_and_retains_plan_type() {
}),
credits: None,
plan_type: Some(PlanType::Pro),
rate_limit_reached_type: None,
}));
assert_eq!(chat.plan_type, Some(PlanType::Pro));
@@ -328,6 +332,7 @@ async fn rate_limit_snapshot_updates_and_retains_plan_type() {
}),
credits: None,
plan_type: None,
rate_limit_reached_type: None,
}));
assert_eq!(chat.plan_type, Some(PlanType::Pro));
}
@@ -351,6 +356,7 @@ async fn rate_limit_snapshots_keep_separate_entries_per_limit_id() {
balance: Some("5.00".to_string()),
}),
plan_type: Some(PlanType::Pro),
rate_limit_reached_type: None,
}));
chat.on_rate_limit_snapshot(Some(RateLimitSnapshot {
@@ -364,6 +370,7 @@ async fn rate_limit_snapshots_keep_separate_entries_per_limit_id() {
secondary: None,
credits: None,
plan_type: Some(PlanType::Pro),
rate_limit_reached_type: None,
}));
let codex = chat
@@ -416,6 +423,7 @@ async fn rate_limit_switch_prompt_skips_non_codex_limit() {
secondary: None,
credits: None,
plan_type: None,
rate_limit_reached_type: None,
}));
assert!(matches!(

View File

@@ -139,6 +139,7 @@ async fn status_snapshot_includes_reasoning_details() {
}),
credits: None,
plan_type: None,
rate_limit_reached_type: None,
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
@@ -322,6 +323,7 @@ async fn status_snapshot_includes_monthly_limit() {
secondary: None,
credits: None,
plan_type: None,
rate_limit_reached_type: None,
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
@@ -373,6 +375,7 @@ async fn status_snapshot_shows_unlimited_credits() {
balance: None,
}),
plan_type: None,
rate_limit_reached_type: None,
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
@@ -422,6 +425,7 @@ async fn status_snapshot_shows_positive_credits() {
balance: Some("12.5".to_string()),
}),
plan_type: None,
rate_limit_reached_type: None,
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
@@ -471,6 +475,7 @@ async fn status_snapshot_hides_zero_credits() {
balance: Some("0".to_string()),
}),
plan_type: None,
rate_limit_reached_type: None,
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
@@ -518,6 +523,7 @@ async fn status_snapshot_hides_when_has_no_credits_flag() {
balance: None,
}),
plan_type: None,
rate_limit_reached_type: None,
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
@@ -623,6 +629,7 @@ async fn status_snapshot_truncates_in_narrow_terminal() {
secondary: None,
credits: None,
plan_type: None,
rate_limit_reached_type: None,
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
@@ -786,6 +793,7 @@ async fn status_snapshot_shows_refreshing_limits_notice() {
}),
credits: None,
plan_type: None,
rate_limit_reached_type: None,
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
@@ -856,6 +864,7 @@ async fn status_snapshot_includes_credits_and_limits() {
balance: Some("37.5".to_string()),
}),
plan_type: None,
rate_limit_reached_type: None,
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
@@ -909,6 +918,7 @@ async fn status_snapshot_shows_unavailable_limits_message() {
secondary: None,
credits: None,
plan_type: None,
rate_limit_reached_type: None,
};
let captured_at = chrono::Local
.with_ymd_and_hms(2024, 6, 7, 8, 9, 10)
@@ -965,6 +975,7 @@ async fn status_snapshot_treats_refreshing_empty_limits_as_unavailable() {
secondary: None,
credits: None,
plan_type: None,
rate_limit_reached_type: None,
};
let captured_at = chrono::Local
.with_ymd_and_hms(2024, 6, 7, 8, 9, 10)
@@ -1035,6 +1046,7 @@ async fn status_snapshot_shows_stale_limits_message() {
}),
credits: None,
plan_type: None,
rate_limit_reached_type: None,
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let now = captured_at + ChronoDuration::minutes(20);
@@ -1105,6 +1117,7 @@ async fn status_snapshot_cached_limits_hide_credits_without_flag() {
balance: Some("80".to_string()),
}),
plan_type: None,
rate_limit_reached_type: None,
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let now = captured_at + ChronoDuration::minutes(20);