Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 7 additions & 10 deletions codex-rs/core/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ struct Error {

// Optional fields available on "usage_limit_reached" and "usage_not_included" errors
plan_type: Option<PlanType>,
resets_at: Option<String>,
resets_at: Option<i64>,
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -425,9 +425,7 @@ impl ModelClient {
.or_else(|| auth.as_ref().and_then(CodexAuth::get_plan_type));
let resets_at = error
.resets_at
.as_deref()
.and_then(|value| DateTime::parse_from_rfc3339(value).ok())
.map(|dt| dt.with_timezone(&Utc));
.and_then(|seconds| DateTime::<Utc>::from_timestamp(seconds, 0));
let codex_err = CodexErr::UsageLimitReached(UsageLimitReachedError {
plan_type,
resets_at,
Expand Down Expand Up @@ -636,10 +634,7 @@ fn parse_rate_limit_window(

used_percent.and_then(|used_percent| {
let window_minutes = parse_header_i64(headers, window_minutes_header);
let resets_at = parse_header_str(headers, resets_header)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(std::string::ToString::to_string);
let resets_at = parse_header_i64(headers, resets_header);

let has_data = used_percent != 0.0
|| window_minutes.is_some_and(|minutes| minutes != 0)
Expand Down Expand Up @@ -1426,7 +1421,8 @@ mod tests {
use crate::token_data::KnownPlan;
use crate::token_data::PlanType;

let json = r#"{"error":{"type":"usage_limit_reached","plan_type":"pro","resets_at":"2024-01-01T00:00:00Z"}}"#;
let json =
r#"{"error":{"type":"usage_limit_reached","plan_type":"pro","resets_at":1704067200}}"#;
let resp: ErrorResponse = serde_json::from_str(json).expect("should deserialize schema");

assert_matches!(resp.error.plan_type, Some(PlanType::Known(KnownPlan::Pro)));
Expand All @@ -1439,7 +1435,8 @@ mod tests {
fn error_response_deserializes_schema_unknown_plan_type_and_serializes_back() {
use crate::token_data::PlanType;

let json = r#"{"error":{"type":"usage_limit_reached","plan_type":"vip","resets_at":"2024-01-01T00:01:00Z"}}"#;
let json =
r#"{"error":{"type":"usage_limit_reached","plan_type":"vip","resets_at":1704067260}}"#;
let resp: ErrorResponse = serde_json::from_str(json).expect("should deserialize schema");

assert_matches!(resp.error.plan_type, Some(PlanType::Unknown(ref s)) if s == "vip");
Expand Down
12 changes: 10 additions & 2 deletions codex-rs/core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,16 +421,24 @@ mod tests {
use pretty_assertions::assert_eq;

fn rate_limit_snapshot() -> RateLimitSnapshot {
let primary_reset_at = Utc
.with_ymd_and_hms(2024, 1, 1, 1, 0, 0)
.unwrap()
.timestamp();
let secondary_reset_at = Utc
.with_ymd_and_hms(2024, 1, 1, 2, 0, 0)
.unwrap()
.timestamp();
RateLimitSnapshot {
primary: Some(RateLimitWindow {
used_percent: 50.0,
window_minutes: Some(60),
resets_at: Some("2024-01-01T01:00:00Z".to_string()),
resets_at: Some(primary_reset_at),
}),
secondary: Some(RateLimitWindow {
used_percent: 30.0,
window_minutes: Some(120),
resets_at: Some("2024-01-01T02:00:00Z".to_string()),
resets_at: Some(secondary_reset_at),
}),
}
}
Expand Down
18 changes: 9 additions & 9 deletions codex-rs/core/tests/suite/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -768,8 +768,8 @@ async fn token_count_includes_rate_limits_snapshot() {
.insert_header("x-codex-secondary-used-percent", "40.0")
.insert_header("x-codex-primary-window-minutes", "10")
.insert_header("x-codex-secondary-window-minutes", "60")
.insert_header("x-codex-primary-reset-at", "2024-01-01T00:30:00Z")
.insert_header("x-codex-secondary-reset-at", "2024-01-01T02:00:00Z")
.insert_header("x-codex-primary-reset-at", "1704069000")
.insert_header("x-codex-secondary-reset-at", "1704074400")
.set_body_raw(sse_body, "text/event-stream");

Mock::given(method("POST"))
Expand Down Expand Up @@ -818,12 +818,12 @@ async fn token_count_includes_rate_limits_snapshot() {
"primary": {
"used_percent": 12.5,
"window_minutes": 10,
"resets_at": "2024-01-01T00:30:00Z"
"resets_at": 1704069000
},
"secondary": {
"used_percent": 40.0,
"window_minutes": 60,
"resets_at": "2024-01-01T02:00:00Z"
"resets_at": 1704074400
}
}
})
Expand Down Expand Up @@ -865,12 +865,12 @@ async fn token_count_includes_rate_limits_snapshot() {
"primary": {
"used_percent": 12.5,
"window_minutes": 10,
"resets_at": "2024-01-01T00:30:00Z"
"resets_at": 1704069000
},
"secondary": {
"used_percent": 40.0,
"window_minutes": 60,
"resets_at": "2024-01-01T02:00:00Z"
"resets_at": 1704074400
}
}
})
Expand All @@ -893,8 +893,8 @@ async fn token_count_includes_rate_limits_snapshot() {
final_snapshot
.primary
.as_ref()
.and_then(|window| window.resets_at.as_deref()),
Some("2024-01-01T00:30:00Z")
.and_then(|window| window.resets_at),
Some(1704069000)
);

wait_for_event(&codex, |msg| matches!(msg, EventMsg::TaskComplete(_))).await;
Expand All @@ -915,7 +915,7 @@ async fn usage_limit_error_emits_rate_limit_event() -> anyhow::Result<()> {
"error": {
"type": "usage_limit_reached",
"message": "limit reached",
"resets_at": "2024-01-01T00:00:42Z",
"resets_at": 1704067242,
"plan_type": "pro"
}
}));
Expand Down
6 changes: 3 additions & 3 deletions codex-rs/protocol/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -644,9 +644,9 @@ pub struct RateLimitWindow {
/// Rolling window duration, in minutes.
#[ts(type = "number | null")]
pub window_minutes: Option<i64>,
/// Timestamp (RFC3339) when the window resets.
#[ts(type = "string | null")]
pub resets_at: Option<String>,
/// Unix timestamp (seconds since epoch) when the window resets.
#[ts(type = "number | null")]
pub resets_at: Option<i64>,
}

// Includes prompts, tools and space to call compact.
Expand Down
4 changes: 2 additions & 2 deletions codex-rs/tui/src/status/rate_limits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::chatwidget::get_limits_duration;
use super::helpers::format_reset_timestamp;
use chrono::DateTime;
use chrono::Local;
use chrono::Utc;
use codex_core::protocol::RateLimitSnapshot;
use codex_core::protocol::RateLimitWindow;

Expand Down Expand Up @@ -34,8 +35,7 @@ impl RateLimitWindowDisplay {
fn from_window(window: &RateLimitWindow, captured_at: DateTime<Local>) -> Self {
let resets_at = window
.resets_at
.as_deref()
.and_then(|value| DateTime::parse_from_rfc3339(value).ok())
.and_then(|seconds| DateTime::<Utc>::from_timestamp(seconds, 0))
.map(|dt| dt.with_timezone(&Local))
.map(|dt| format_reset_timestamp(dt, captured_at));

Expand Down
4 changes: 2 additions & 2 deletions codex-rs/tui/src/status/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,10 @@ fn sanitize_directory(lines: Vec<String>) -> Vec<String> {
.collect()
}

fn reset_at_from(captured_at: &chrono::DateTime<chrono::Local>, seconds: i64) -> String {
fn reset_at_from(captured_at: &chrono::DateTime<chrono::Local>, seconds: i64) -> i64 {
(*captured_at + ChronoDuration::seconds(seconds))
.with_timezone(&Utc)
.to_rfc3339()
.timestamp()
}

#[test]
Expand Down
Loading