mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat: show enterprise monthly credit limits in status (#24812)
## Summary
Enterprise users can have an effective monthly credit limit, but Codex
`/status` currently drops that metadata from the account-usage response.
This change adds the optional `spend_control.individual_limit`
projection to the existing rate-limit snapshot flow. The backend client
reads the monthly limit, app-server exposes it as `individualLimit`, and
the TUI renders a `Monthly credit limit` row through the existing
progress-bar renderer.
When the backend does not return an effective monthly limit, existing
rate-limit behavior is unchanged.
## Existing backend state
The account-usage backend already returns the effective monthly limit
and current usage together:
```json
{
"spend_control": {
"reached": false,
"individual_limit": {
"limit": "25000",
"used": "8000",
"remaining": "17000",
"used_percent": 32,
"remaining_percent": 68,
"reset_after_seconds": 86400,
"reset_at": 1778137680
}
}
}
```
Before this change, Codex projected rolling `primary` and `secondary`
windows plus `credits`. It ignored `spend_control.individual_limit`, so
app-server clients and `/status` could not render the monthly cap.
The updated flow is:
```text
account usage backend
-> backend-client reads spend_control.individual_limit
-> existing rate-limit snapshot carries optional individual_limit
-> app-server exposes optional individualLimit
-> TUI renders Monthly credit limit
```
## App-server contract
`account/rateLimits/read` and sparse `account/rateLimits/updated`
notifications now include an additive nullable
`rateLimits.individualLimit` field:
```json
{
"individualLimit": {
"limit": "25000",
"used": "8000",
"remainingPercent": 68,
"resetsAt": 1778137680
}
}
```
In an `account/rateLimits/read` response, `null` means no monthly limit
is available. `account/rateLimits/updated` remains a sparse rolling
notification: clients merge available values into their most recent
`account/rateLimits/read` snapshot or refetch. Nullable account metadata
in a rolling notification does not clear a previously observed value.
## Design decisions
- Extend the existing rate-limit snapshot instead of introducing a
separate request or wire-level update protocol.
- Keep the Codex projection narrow: `/status` needs the effective limit,
current usage, remaining percentage, and reset timestamp.
- Render the monthly row through the existing progress-bar renderer,
with one optional detail line for `8,000 of 25,000 credits used`.
- Keep the backend response optional so existing accounts and older
usage states preserve their current behavior.
- Preserve cached monthly metadata when sparse rolling notifications
omit it. Live account-usage reads remain authoritative and can clear a
removed limit.
## Visual evidence
```text
Monthly credit limit: [██████████████░░░░░░] 68% left (resets 07:08 on 7 May)
8,000 of 25,000 credits used
```
Snapshot:
`codex-rs/tui/src/status/snapshots/codex_tui__status__tests__status_snapshot_includes_enterprise_monthly_credit_limit.snap`
## Testing
Tests: generated app-server schema verification, protocol tests,
backend-client tests, app-server integration coverage, TUI snapshot
coverage, formatting, and workspace lint cleanup.
This commit is contained in:
committed by
GitHub
Unverified
parent
c955f73078
commit
c8e5db16c9
@@ -56,3 +56,9 @@ pub use self::rate_limit_window_snapshot::RateLimitWindowSnapshot;
|
||||
|
||||
pub(crate) mod credit_status_details;
|
||||
pub use self::credit_status_details::CreditStatusDetails;
|
||||
|
||||
pub(crate) mod spend_control_limit_details;
|
||||
pub use self::spend_control_limit_details::SpendControlLimitDetails;
|
||||
|
||||
pub(crate) mod spend_control_status_details;
|
||||
pub use self::spend_control_status_details::SpendControlStatusDetails;
|
||||
|
||||
@@ -30,6 +30,13 @@ pub struct RateLimitStatusPayload {
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub credits: Option<Option<Box<models::CreditStatusDetails>>>,
|
||||
#[serde(
|
||||
rename = "spend_control",
|
||||
default,
|
||||
with = "::serde_with::rust::double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub spend_control: Option<Option<Box<models::SpendControlStatusDetails>>>,
|
||||
#[serde(
|
||||
rename = "additional_rate_limits",
|
||||
default,
|
||||
@@ -52,6 +59,7 @@ impl RateLimitStatusPayload {
|
||||
plan_type,
|
||||
rate_limit: None,
|
||||
credits: None,
|
||||
spend_control: None,
|
||||
additional_rate_limits: None,
|
||||
rate_limit_reached_type: None,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* codex-backend
|
||||
*
|
||||
* codex-backend
|
||||
*
|
||||
* The version of the OpenAPI document: 0.0.1
|
||||
*
|
||||
* Generated by: https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SpendControlLimitDetails {
|
||||
#[serde(
|
||||
rename = "source",
|
||||
default,
|
||||
with = "::serde_with::rust::double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub source: Option<Option<String>>,
|
||||
#[serde(rename = "limit")]
|
||||
pub limit: String,
|
||||
#[serde(rename = "used")]
|
||||
pub used: String,
|
||||
#[serde(rename = "remaining")]
|
||||
pub remaining: String,
|
||||
#[serde(rename = "used_percent")]
|
||||
pub used_percent: i32,
|
||||
#[serde(rename = "remaining_percent")]
|
||||
pub remaining_percent: i32,
|
||||
#[serde(rename = "reset_after_seconds")]
|
||||
pub reset_after_seconds: i32,
|
||||
#[serde(rename = "reset_at")]
|
||||
pub reset_at: i32,
|
||||
}
|
||||
|
||||
impl SpendControlLimitDetails {
|
||||
pub fn new(
|
||||
limit: String,
|
||||
used: String,
|
||||
remaining: String,
|
||||
used_percent: i32,
|
||||
remaining_percent: i32,
|
||||
reset_after_seconds: i32,
|
||||
reset_at: i32,
|
||||
) -> SpendControlLimitDetails {
|
||||
SpendControlLimitDetails {
|
||||
source: None,
|
||||
limit,
|
||||
used,
|
||||
remaining,
|
||||
used_percent,
|
||||
remaining_percent,
|
||||
reset_after_seconds,
|
||||
reset_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* codex-backend
|
||||
*
|
||||
* codex-backend
|
||||
*
|
||||
* The version of the OpenAPI document: 0.0.1
|
||||
*
|
||||
* Generated by: https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
use crate::models;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SpendControlStatusDetails {
|
||||
#[serde(rename = "reached")]
|
||||
pub reached: bool,
|
||||
#[serde(
|
||||
rename = "individual_limit",
|
||||
default,
|
||||
with = "::serde_with::rust::double_option",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub individual_limit: Option<Option<Box<models::SpendControlLimitDetails>>>,
|
||||
}
|
||||
|
||||
impl SpendControlStatusDetails {
|
||||
pub fn new(reached: bool) -> SpendControlStatusDetails {
|
||||
SpendControlStatusDetails {
|
||||
reached,
|
||||
individual_limit: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user