mirror of
https://github.com/openai/codex.git
synced 2026-11-13 06:45:07 +00:00
[app-server] Include reset-credit details in rate limits (#30395)
## Why Supported clients currently receive only a reset-credit count from `account/rateLimits/read`. The redemption UI and other app-server clients need each available credit's expiry and ID so they can explain what will expire and consume the credit a user selected. This information belongs on the existing rate-limit read surface rather than a second app-server list RPC that clients would need to coordinate. ## What changed - extend `rateLimitResetCredits` on `account/rateLimits/read` with nullable `credits` detail rows - fetch usage and reset-credit details concurrently; if the detail request fails, times out, or cannot be parsed, preserve the usage response and return `credits: null` - expose each credit's ID, reset type, status, grant time, expiry time, title, and description - add an optional nullable `creditId` to `account/rateLimitResetCredit/consume`; omitting it preserves the existing automatic-selection behavior - forward a selected credit ID to the Codex backend and update the app-server documentation and generated schemas The TUI consumer is stacked in #30488. ## Validation - `just test -p codex-app-server-protocol` (251 passed) - `just test -p codex-backend-client` (16 passed) - `just test -p codex-app-server rate_limit` (18 passed) Part of #29618.
This commit is contained in:
24 files changed
+967
-53
No files matched your search
@@ -3,6 +3,7 @@
|
||||
use super::Client;
|
||||
use super::PathStyle;
|
||||
use crate::types::ConsumeRateLimitResetCreditResponse;
|
||||
use crate::types::RateLimitResetCreditsDetails;
|
||||
use crate::types::RateLimitStatusWithResetCredits;
|
||||
use crate::types::RateLimitsWithResetCredits;
|
||||
use anyhow::Result;
|
||||
@@ -13,6 +14,8 @@ use serde::Serialize;
|
||||
#[derive(Serialize)]
|
||||
struct ConsumeRateLimitResetCreditRequest<'a> {
|
||||
redeem_request_id: &'a str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
credit_id: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
@@ -31,9 +34,34 @@ impl Client {
|
||||
self.decode_json(&url, &ct, &body)
|
||||
}
|
||||
|
||||
pub async fn list_rate_limit_reset_credits(&self) -> Result<RateLimitResetCreditsDetails> {
|
||||
let url = self.rate_limit_reset_credits_url();
|
||||
let req = self.http.get(&url).headers(self.headers());
|
||||
let (body, ct) = self.exec_request(req, "GET", &url).await?;
|
||||
self.decode_json(&url, &ct, &body)
|
||||
}
|
||||
|
||||
pub async fn consume_rate_limit_reset_credit(
|
||||
&self,
|
||||
redeem_request_id: &str,
|
||||
) -> Result<ConsumeRateLimitResetCreditResponse> {
|
||||
self.consume_rate_limit_reset_credit_request(redeem_request_id, /*credit_id*/ None)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn consume_rate_limit_reset_credit_by_id(
|
||||
&self,
|
||||
redeem_request_id: &str,
|
||||
credit_id: &str,
|
||||
) -> Result<ConsumeRateLimitResetCreditResponse> {
|
||||
self.consume_rate_limit_reset_credit_request(redeem_request_id, Some(credit_id))
|
||||
.await
|
||||
}
|
||||
|
||||
async fn consume_rate_limit_reset_credit_request(
|
||||
&self,
|
||||
redeem_request_id: &str,
|
||||
credit_id: Option<&str>,
|
||||
) -> Result<ConsumeRateLimitResetCreditResponse> {
|
||||
let url = self.consume_rate_limit_reset_credit_url();
|
||||
let req = self
|
||||
@@ -41,7 +69,10 @@ impl Client {
|
||||
.post(&url)
|
||||
.headers(self.headers())
|
||||
.header(CONTENT_TYPE, HeaderValue::from_static("application/json"))
|
||||
.json(&ConsumeRateLimitResetCreditRequest { redeem_request_id });
|
||||
.json(&ConsumeRateLimitResetCreditRequest {
|
||||
redeem_request_id,
|
||||
credit_id,
|
||||
});
|
||||
let (body, ct) = self.exec_request(req, "POST", &url).await?;
|
||||
self.decode_json(&url, &ct, &body)
|
||||
}
|
||||
@@ -53,6 +84,17 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
fn rate_limit_reset_credits_url(&self) -> String {
|
||||
match self.path_style {
|
||||
PathStyle::CodexApi => {
|
||||
format!("{}/api/codex/rate-limit-reset-credits", self.base_url)
|
||||
}
|
||||
PathStyle::ChatGptApi => {
|
||||
format!("{}/wham/rate-limit-reset-credits", self.base_url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn consume_rate_limit_reset_credit_url(&self) -> String {
|
||||
match self.path_style {
|
||||
PathStyle::CodexApi => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use super::*;
|
||||
use crate::types::ConsumeRateLimitResetCreditCode;
|
||||
use crate::types::RateLimitResetCreditDetails;
|
||||
use crate::types::RateLimitResetCreditsDetails;
|
||||
use crate::types::RateLimitResetCreditsSummary;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -9,6 +11,10 @@ fn rate_limit_reset_contract_uses_expected_paths_and_payloads() {
|
||||
test_client("https://example.test", PathStyle::CodexApi).rate_limit_status_url(),
|
||||
"https://example.test/api/codex/usage"
|
||||
);
|
||||
assert_eq!(
|
||||
test_client("https://example.test", PathStyle::CodexApi).rate_limit_reset_credits_url(),
|
||||
"https://example.test/api/codex/rate-limit-reset-credits"
|
||||
);
|
||||
assert_eq!(
|
||||
test_client("https://example.test", PathStyle::CodexApi)
|
||||
.consume_rate_limit_reset_credit_url(),
|
||||
@@ -19,6 +25,11 @@ fn rate_limit_reset_contract_uses_expected_paths_and_payloads() {
|
||||
.rate_limit_status_url(),
|
||||
"https://chatgpt.com/backend-api/wham/usage"
|
||||
);
|
||||
assert_eq!(
|
||||
test_client("https://chatgpt.com/backend-api", PathStyle::ChatGptApi)
|
||||
.rate_limit_reset_credits_url(),
|
||||
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits"
|
||||
);
|
||||
assert_eq!(
|
||||
test_client("https://chatgpt.com/backend-api", PathStyle::ChatGptApi)
|
||||
.consume_rate_limit_reset_credit_url(),
|
||||
@@ -28,10 +39,22 @@ fn rate_limit_reset_contract_uses_expected_paths_and_payloads() {
|
||||
assert_eq!(
|
||||
serde_json::to_value(ConsumeRateLimitResetCreditRequest {
|
||||
redeem_request_id: "redeem-123",
|
||||
credit_id: None,
|
||||
})
|
||||
.unwrap(),
|
||||
serde_json::json!({ "redeem_request_id": "redeem-123" })
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(ConsumeRateLimitResetCreditRequest {
|
||||
redeem_request_id: "redeem-456",
|
||||
credit_id: Some("credit-123"),
|
||||
})
|
||||
.unwrap(),
|
||||
serde_json::json!({
|
||||
"redeem_request_id": "redeem-456",
|
||||
"credit_id": "credit-123",
|
||||
})
|
||||
);
|
||||
|
||||
let status: RateLimitStatusWithResetCredits = serde_json::from_value(serde_json::json!({
|
||||
"plan_type": "plus",
|
||||
@@ -43,6 +66,60 @@ fn rate_limit_reset_contract_uses_expected_paths_and_payloads() {
|
||||
Some(RateLimitResetCreditsSummary { available_count: 3 })
|
||||
);
|
||||
|
||||
let details: RateLimitResetCreditsDetails = serde_json::from_value(serde_json::json!({
|
||||
"credits": [
|
||||
{
|
||||
"id": "credit-1",
|
||||
"reset_type": "codex_rate_limits",
|
||||
"status": "available",
|
||||
"granted_at": "2026-06-17T00:00:00Z",
|
||||
"expires_at": "2026-07-17T00:00:00Z",
|
||||
"redeem_started_at": null,
|
||||
"redeemed_at": null,
|
||||
"profile_image_url": "https://example.test/avatar.png",
|
||||
"profile_user_id": "@friend",
|
||||
"title": "Full reset (Weekly + 5 hr)",
|
||||
"description": "Ready to redeem"
|
||||
},
|
||||
{
|
||||
"id": "credit-2",
|
||||
"reset_type": "codex_rate_limits",
|
||||
"status": "available",
|
||||
"granted_at": "2026-06-18T00:00:00Z",
|
||||
"expires_at": null
|
||||
}
|
||||
],
|
||||
"available_count": 2,
|
||||
"total_earned_count": 4
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
details,
|
||||
RateLimitResetCreditsDetails {
|
||||
credits: vec![
|
||||
RateLimitResetCreditDetails {
|
||||
id: "credit-1".to_string(),
|
||||
reset_type: "codex_rate_limits".to_string(),
|
||||
status: "available".to_string(),
|
||||
granted_at: "2026-06-17T00:00:00Z".to_string(),
|
||||
expires_at: Some("2026-07-17T00:00:00Z".to_string()),
|
||||
title: Some("Full reset (Weekly + 5 hr)".to_string()),
|
||||
description: Some("Ready to redeem".to_string()),
|
||||
},
|
||||
RateLimitResetCreditDetails {
|
||||
id: "credit-2".to_string(),
|
||||
reset_type: "codex_rate_limits".to_string(),
|
||||
status: "available".to_string(),
|
||||
granted_at: "2026-06-18T00:00:00Z".to_string(),
|
||||
expires_at: None,
|
||||
title: None,
|
||||
description: None,
|
||||
},
|
||||
],
|
||||
available_count: 2,
|
||||
}
|
||||
);
|
||||
|
||||
let response: ConsumeRateLimitResetCreditResponse = serde_json::from_value(serde_json::json!({
|
||||
"code": "reset",
|
||||
"credit": { "id": "ignored-by-cli" },
|
||||
|
||||
Reference in new issue
Block a user