feat(app-server): expose rate-limit reset credits (#28143)

## Why

Codex users can earn personal rate-limit reset credits, but app-server
clients do not currently have an API for reading or redeeming them. This
adds the backend and protocol foundation used by the `/usage` TUI flow
in #28154.

## What changed

- Extend `account/rateLimits/read` with a nullable
`rateLimitResetCredits` summary sourced from the existing usage
response.
- Add backend-client and app-server support for consuming a reset with a
caller-generated idempotency key. A UUID is recommended, and clients
reuse the same key when retrying the same logical reset.
- Return only the consume `outcome`; clients refetch
`account/rateLimits/read` for updated window state.
- Document the response field and each consume outcome, and regenerate
the JSON and TypeScript schema fixtures.
- Clarify in `AGENTS.md` that new app-server string enum values use
camelCase on the wire.
- Update the existing TUI response fixture for the expanded protocol
shape.
- Add coverage for authentication, response mapping, backend failures,
consume outcomes, and request timeout behavior.

## Validation

- `just test -p codex-app-server-protocol` — 231 passed.
- `just test -p codex-backend-client` — 14 passed.
- Focused `codex-app-server` reset-credit tests — 5 passed.
- Focused `codex-tui` protocol response fixture test — passed.
- `just fix -p codex-backend-client -p codex-app-server-protocol -p
codex-app-server` — passed.
- `just fmt` — passed.
This commit is contained in:
jay
2026-06-15 14:54:01 -07:00
committed by GitHub
Unverified
parent af99f6a72f
commit bef99f861b
31 changed files with 1060 additions and 84 deletions
@@ -1004,6 +1004,12 @@ client_request_definitions! {
response: v2::GetAccountRateLimitsResponse,
},
ConsumeAccountRateLimitResetCredit => "account/rateLimitResetCredit/consume" {
params: v2::ConsumeAccountRateLimitResetCreditParams,
serialization: global("account-auth"),
response: v2::ConsumeAccountRateLimitResetCreditResponse,
},
GetAccountTokenUsage => "account/usage/read" {
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
serialization: None,
@@ -256,6 +256,44 @@ pub struct GetAccountRateLimitsResponse {
pub rate_limits: RateLimitSnapshot,
/// Multi-bucket view keyed by metered `limit_id` (for example, `codex`).
pub rate_limits_by_limit_id: Option<HashMap<String, RateLimitSnapshot>>,
pub rate_limit_reset_credits: Option<RateLimitResetCreditsSummary>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct RateLimitResetCreditsSummary {
pub available_count: i64,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ConsumeAccountRateLimitResetCreditParams {
/// Identifies one logical reset attempt. A UUID is recommended; reuse the same value when
/// retrying that attempt.
pub idempotency_key: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ConsumeAccountRateLimitResetCreditResponse {
pub outcome: ConsumeAccountRateLimitResetCreditOutcome,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/", rename_all = "camelCase")]
pub enum ConsumeAccountRateLimitResetCreditOutcome {
/// A reset credit was consumed and the eligible rate-limit windows were reset.
Reset,
/// No current rate-limit window is eligible for a reset.
NothingToReset,
/// The account has no earned reset credits available.
NoCredit,
/// The same idempotency key already completed a reset successfully.
AlreadyRedeemed,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]