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
2 changes: 2 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1518,6 +1518,7 @@ pub struct RateLimitSnapshot {
pub primary: Option<RateLimitWindow>,
pub secondary: Option<RateLimitWindow>,
pub credits: Option<CreditsSnapshot>,
pub plan_type: Option<PlanType>,
}

impl From<CoreRateLimitSnapshot> for RateLimitSnapshot {
Expand All @@ -1526,6 +1527,7 @@ impl From<CoreRateLimitSnapshot> for RateLimitSnapshot {
primary: value.primary.map(RateLimitWindow::from),
secondary: value.secondary.map(RateLimitWindow::from),
credits: value.credits.map(CreditsSnapshot::from),
plan_type: value.plan_type,
}
}
}
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/src/bespoke_event_handling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1494,6 +1494,7 @@ mod tests {
unlimited: false,
balance: Some("5".to_string()),
}),
plan_type: None,
};

handle_token_count_event(
Expand Down
7 changes: 6 additions & 1 deletion codex-rs/app-server/src/outgoing_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ use tracing::warn;

use crate::error_code::INTERNAL_ERROR_CODE;

#[cfg(test)]
use codex_protocol::account::PlanType;

/// Sends messages to the client and manages request callbacks.
pub(crate) struct OutgoingMessageSender {
next_request_id: AtomicI64,
Expand Down Expand Up @@ -230,6 +233,7 @@ mod tests {
}),
secondary: None,
credits: None,
plan_type: Some(PlanType::Plus),
},
});

Expand All @@ -245,7 +249,8 @@ mod tests {
"resetsAt": 123
},
"secondary": null,
"credits": null
"credits": null,
"planType": "plus"
}
},
}),
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/app-server/tests/suite/v2/rate_limits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use codex_app_server_protocol::RateLimitSnapshot;
use codex_app_server_protocol::RateLimitWindow;
use codex_app_server_protocol::RequestId;
use codex_core::auth::AuthCredentialsStoreMode;
use codex_protocol::account::PlanType as AccountPlanType;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::path::Path;
Expand Down Expand Up @@ -153,6 +154,7 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> {
resets_at: Some(secondary_reset_timestamp),
}),
credits: None,
plan_type: Some(AccountPlanType::Pro),
},
};
assert_eq!(received, expected);
Expand Down
19 changes: 19 additions & 0 deletions codex-rs/backend-client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::types::TurnAttemptsSiblingTurnsResponse;
use anyhow::Result;
use codex_core::auth::CodexAuth;
use codex_core::default_client::get_codex_user_agent;
use codex_protocol::account::PlanType as AccountPlanType;
use codex_protocol::protocol::CreditsSnapshot;
use codex_protocol::protocol::RateLimitSnapshot;
use codex_protocol::protocol::RateLimitWindow;
Expand Down Expand Up @@ -291,6 +292,7 @@ impl Client {
primary,
secondary,
credits: Self::map_credits(payload.credits),
plan_type: Some(Self::map_plan_type(payload.plan_type)),
}
}

Expand Down Expand Up @@ -325,6 +327,23 @@ impl Client {
})
}

fn map_plan_type(plan_type: crate::types::PlanType) -> AccountPlanType {
match plan_type {
crate::types::PlanType::Free => AccountPlanType::Free,
crate::types::PlanType::Plus => AccountPlanType::Plus,
crate::types::PlanType::Pro => AccountPlanType::Pro,
crate::types::PlanType::Team => AccountPlanType::Team,
crate::types::PlanType::Business => AccountPlanType::Business,
crate::types::PlanType::Enterprise => AccountPlanType::Enterprise,
crate::types::PlanType::Edu | crate::types::PlanType::Education => AccountPlanType::Edu,
crate::types::PlanType::Guest
| crate::types::PlanType::Go
| crate::types::PlanType::FreeWorkspace
| crate::types::PlanType::Quorum
| crate::types::PlanType::K12 => AccountPlanType::Unknown,
}
}

fn window_minutes_from_seconds(seconds: i32) -> Option<i64> {
if seconds <= 0 {
return None;
Expand Down
1 change: 1 addition & 0 deletions codex-rs/codex-api/src/rate_limits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub fn parse_rate_limit(headers: &HeaderMap) -> Option<RateLimitSnapshot> {
primary,
secondary,
credits,
plan_type: None,
})
}

Expand Down
25 changes: 0 additions & 25 deletions codex-rs/core/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,23 +227,6 @@ impl CodexAuth {
})
}

/// Raw plan string from the ID token (including unknown/new plan types).
pub fn raw_plan_type(&self) -> Option<String> {
self.get_plan_type().map(|plan| match plan {
InternalPlanType::Known(k) => format!("{k:?}"),
InternalPlanType::Unknown(raw) => raw,
})
}

/// Raw internal plan value from the ID token.
/// Exposes the underlying `token_data::PlanType` without mapping it to the
/// public `AccountPlanType`. Use this when downstream code needs to inspect
/// internal/unknown plan strings exactly as issued in the token.
pub(crate) fn get_plan_type(&self) -> Option<InternalPlanType> {
self.get_current_token_data()
.and_then(|t| t.id_token.chatgpt_plan_type)
}

fn get_current_auth_json(&self) -> Option<AuthDotJson> {
#[expect(clippy::unwrap_used)]
self.auth_dot_json.lock().unwrap().clone()
Expand Down Expand Up @@ -1041,10 +1024,6 @@ mod tests {
.expect("auth available");

pretty_assertions::assert_eq!(auth.account_plan_type(), Some(AccountPlanType::Pro));
pretty_assertions::assert_eq!(
auth.get_plan_type(),
Some(InternalPlanType::Known(InternalKnownPlan::Pro))
);
}

#[test]
Expand All @@ -1065,10 +1044,6 @@ mod tests {
.expect("auth available");

pretty_assertions::assert_eq!(auth.account_plan_type(), Some(AccountPlanType::Unknown));
pretty_assertions::assert_eq!(
auth.get_plan_type(),
Some(InternalPlanType::Unknown("mystery-tier".to_string()))
);
}
}

Expand Down
74 changes: 74 additions & 0 deletions codex-rs/core/src/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2650,6 +2650,7 @@ mod tests {
unlimited: false,
balance: Some("10.00".to_string()),
}),
plan_type: Some(codex_protocol::account::PlanType::Plus),
};
state.set_rate_limits(initial.clone());

Expand All @@ -2665,6 +2666,7 @@ mod tests {
resets_at: Some(1_900),
}),
credits: None,
plan_type: None,
};
state.set_rate_limits(update.clone());

Expand All @@ -2674,6 +2676,78 @@ mod tests {
primary: update.primary.clone(),
secondary: update.secondary,
credits: initial.credits,
plan_type: initial.plan_type,
})
);
}

#[test]
fn set_rate_limits_updates_plan_type_when_present() {
let codex_home = tempfile::tempdir().expect("create temp dir");
let config = Config::load_from_base_config_with_overrides(
ConfigToml::default(),
ConfigOverrides::default(),
codex_home.path().to_path_buf(),
)
.expect("load default test config");
let config = Arc::new(config);
let session_configuration = SessionConfiguration {
provider: config.model_provider.clone(),
model: config.model.clone(),
model_reasoning_effort: config.model_reasoning_effort,
model_reasoning_summary: config.model_reasoning_summary,
developer_instructions: config.developer_instructions.clone(),
user_instructions: config.user_instructions.clone(),
base_instructions: config.base_instructions.clone(),
compact_prompt: config.compact_prompt.clone(),
approval_policy: config.approval_policy,
sandbox_policy: config.sandbox_policy.clone(),
cwd: config.cwd.clone(),
original_config_do_not_use: Arc::clone(&config),
exec_policy: Arc::new(RwLock::new(ExecPolicy::empty())),
session_source: SessionSource::Exec,
};

let mut state = SessionState::new(session_configuration);
let initial = RateLimitSnapshot {
primary: Some(RateLimitWindow {
used_percent: 15.0,
window_minutes: Some(20),
resets_at: Some(1_600),
}),
secondary: Some(RateLimitWindow {
used_percent: 5.0,
window_minutes: Some(45),
resets_at: Some(1_650),
}),
credits: Some(CreditsSnapshot {
has_credits: true,
unlimited: false,
balance: Some("15.00".to_string()),
}),
plan_type: Some(codex_protocol::account::PlanType::Plus),
};
state.set_rate_limits(initial.clone());

let update = RateLimitSnapshot {
primary: Some(RateLimitWindow {
used_percent: 35.0,
window_minutes: Some(25),
resets_at: Some(1_700),
}),
secondary: None,
credits: None,
plan_type: Some(codex_protocol::account::PlanType::Pro),
};
state.set_rate_limits(update.clone());

assert_eq!(
state.latest_rate_limits,
Some(RateLimitSnapshot {
primary: update.primary,
secondary: update.secondary,
credits: initial.credits,
plan_type: update.plan_type,
})
);
}
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@ mod tests {
resets_at: Some(secondary_reset_at),
}),
credits: None,
plan_type: None,
}
}

Expand Down
9 changes: 6 additions & 3 deletions codex-rs/core/src/state/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ impl SessionState {
}

pub(crate) fn set_rate_limits(&mut self, snapshot: RateLimitSnapshot) {
self.latest_rate_limits = Some(merge_rate_limit_credits(
self.latest_rate_limits = Some(merge_rate_limit_fields(
self.latest_rate_limits.as_ref(),
snapshot,
));
Expand All @@ -83,13 +83,16 @@ impl SessionState {
}
}

// Sometimes new snapshots don't include credits
fn merge_rate_limit_credits(
// Sometimes new snapshots don't include credits or plan information.
fn merge_rate_limit_fields(
previous: Option<&RateLimitSnapshot>,
mut snapshot: RateLimitSnapshot,
) -> RateLimitSnapshot {
if snapshot.credits.is_none() {
snapshot.credits = previous.and_then(|prior| prior.credits.clone());
}
if snapshot.plan_type.is_none() {
snapshot.plan_type = previous.and_then(|prior| prior.plan_type);
}
snapshot
}
9 changes: 6 additions & 3 deletions codex-rs/core/tests/suite/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1192,7 +1192,8 @@ async fn token_count_includes_rate_limits_snapshot() {
"window_minutes": 60,
"resets_at": 1704074400
},
"credits": null
"credits": null,
"plan_type": null
}
})
);
Expand Down Expand Up @@ -1240,7 +1241,8 @@ async fn token_count_includes_rate_limits_snapshot() {
"window_minutes": 60,
"resets_at": 1704074400
},
"credits": null
"credits": null,
"plan_type": null
}
})
);
Expand Down Expand Up @@ -1311,7 +1313,8 @@ async fn usage_limit_error_emits_rate_limit_event() -> anyhow::Result<()> {
"window_minutes": 60,
"resets_at": null
},
"credits": null
"credits": null,
"plan_type": null
});

let submission_id = codex
Expand Down
1 change: 1 addition & 0 deletions codex-rs/protocol/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,7 @@ pub struct RateLimitSnapshot {
pub primary: Option<RateLimitWindow>,
pub secondary: Option<RateLimitWindow>,
pub credits: Option<CreditsSnapshot>,
pub plan_type: Option<crate::account::PlanType>,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, TS)]
Expand Down
7 changes: 7 additions & 0 deletions codex-rs/tui/src/chatwidget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ use codex_core::protocol::WebSearchBeginEvent;
use codex_core::protocol::WebSearchEndEvent;
use codex_core::skills::model::SkillMetadata;
use codex_protocol::ConversationId;
use codex_protocol::account::PlanType;
use codex_protocol::approvals::ElicitationRequestEvent;
use codex_protocol::parse_command::ParsedCommand;
use codex_protocol::user_input::UserInput;
Expand Down Expand Up @@ -282,6 +283,7 @@ pub(crate) struct ChatWidget {
initial_user_message: Option<UserMessage>,
token_info: Option<TokenUsageInfo>,
rate_limit_snapshot: Option<RateLimitSnapshotDisplay>,
plan_type: Option<PlanType>,
rate_limit_warnings: RateLimitWarningState,
rate_limit_switch_prompt: RateLimitSwitchPromptState,
rate_limit_poller: Option<JoinHandle<()>>,
Expand Down Expand Up @@ -577,6 +579,8 @@ impl ChatWidget {
});
}

self.plan_type = snapshot.plan_type.or(self.plan_type);

let warnings = self.rate_limit_warnings.take_warnings(
snapshot
.secondary
Expand Down Expand Up @@ -1272,6 +1276,7 @@ impl ChatWidget {
),
token_info: None,
rate_limit_snapshot: None,
plan_type: None,
rate_limit_warnings: RateLimitWarningState::default(),
rate_limit_switch_prompt: RateLimitSwitchPromptState::default(),
rate_limit_poller: None,
Expand Down Expand Up @@ -1354,6 +1359,7 @@ impl ChatWidget {
),
token_info: None,
rate_limit_snapshot: None,
plan_type: None,
rate_limit_warnings: RateLimitWarningState::default(),
rate_limit_switch_prompt: RateLimitSwitchPromptState::default(),
rate_limit_poller: None,
Expand Down Expand Up @@ -1998,6 +2004,7 @@ impl ChatWidget {
context_usage,
&self.conversation_id,
self.rate_limit_snapshot.as_ref(),
self.plan_type,
Local::now(),
));
}
Expand Down
Loading
Loading