Skip to content

Commit b8eab7c

Browse files
authored
fix: taking plan type from usage endpoint instead of thru auth token (#7610)
pull plan type from the usage endpoint, persist it in session state / tui state, and propagate through rate limit snapshots
1 parent b1c918d commit b8eab7c

File tree

17 files changed

+224
-35
lines changed

17 files changed

+224
-35
lines changed

codex-rs/app-server-protocol/src/protocol/v2.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1524,6 +1524,7 @@ pub struct RateLimitSnapshot {
15241524
pub primary: Option<RateLimitWindow>,
15251525
pub secondary: Option<RateLimitWindow>,
15261526
pub credits: Option<CreditsSnapshot>,
1527+
pub plan_type: Option<PlanType>,
15271528
}
15281529

15291530
impl From<CoreRateLimitSnapshot> for RateLimitSnapshot {
@@ -1532,6 +1533,7 @@ impl From<CoreRateLimitSnapshot> for RateLimitSnapshot {
15321533
primary: value.primary.map(RateLimitWindow::from),
15331534
secondary: value.secondary.map(RateLimitWindow::from),
15341535
credits: value.credits.map(CreditsSnapshot::from),
1536+
plan_type: value.plan_type,
15351537
}
15361538
}
15371539
}

codex-rs/app-server/src/bespoke_event_handling.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1499,6 +1499,7 @@ mod tests {
14991499
unlimited: false,
15001500
balance: Some("5".to_string()),
15011501
}),
1502+
plan_type: None,
15021503
};
15031504

15041505
handle_token_count_event(

codex-rs/app-server/src/outgoing_message.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ use tracing::warn;
1616

1717
use crate::error_code::INTERNAL_ERROR_CODE;
1818

19+
#[cfg(test)]
20+
use codex_protocol::account::PlanType;
21+
1922
/// Sends messages to the client and manages request callbacks.
2023
pub(crate) struct OutgoingMessageSender {
2124
next_request_id: AtomicI64,
@@ -230,6 +233,7 @@ mod tests {
230233
}),
231234
secondary: None,
232235
credits: None,
236+
plan_type: Some(PlanType::Plus),
233237
},
234238
});
235239

@@ -245,7 +249,8 @@ mod tests {
245249
"resetsAt": 123
246250
},
247251
"secondary": null,
248-
"credits": null
252+
"credits": null,
253+
"planType": "plus"
249254
}
250255
},
251256
}),

codex-rs/app-server/tests/suite/v2/rate_limits.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use codex_app_server_protocol::RateLimitSnapshot;
1111
use codex_app_server_protocol::RateLimitWindow;
1212
use codex_app_server_protocol::RequestId;
1313
use codex_core::auth::AuthCredentialsStoreMode;
14+
use codex_protocol::account::PlanType as AccountPlanType;
1415
use pretty_assertions::assert_eq;
1516
use serde_json::json;
1617
use std::path::Path;
@@ -153,6 +154,7 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> {
153154
resets_at: Some(secondary_reset_timestamp),
154155
}),
155156
credits: None,
157+
plan_type: Some(AccountPlanType::Pro),
156158
},
157159
};
158160
assert_eq!(received, expected);

codex-rs/backend-client/src/client.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::types::TurnAttemptsSiblingTurnsResponse;
77
use anyhow::Result;
88
use codex_core::auth::CodexAuth;
99
use codex_core::default_client::get_codex_user_agent;
10+
use codex_protocol::account::PlanType as AccountPlanType;
1011
use codex_protocol::protocol::CreditsSnapshot;
1112
use codex_protocol::protocol::RateLimitSnapshot;
1213
use codex_protocol::protocol::RateLimitWindow;
@@ -291,6 +292,7 @@ impl Client {
291292
primary,
292293
secondary,
293294
credits: Self::map_credits(payload.credits),
295+
plan_type: Some(Self::map_plan_type(payload.plan_type)),
294296
}
295297
}
296298

@@ -325,6 +327,23 @@ impl Client {
325327
})
326328
}
327329

330+
fn map_plan_type(plan_type: crate::types::PlanType) -> AccountPlanType {
331+
match plan_type {
332+
crate::types::PlanType::Free => AccountPlanType::Free,
333+
crate::types::PlanType::Plus => AccountPlanType::Plus,
334+
crate::types::PlanType::Pro => AccountPlanType::Pro,
335+
crate::types::PlanType::Team => AccountPlanType::Team,
336+
crate::types::PlanType::Business => AccountPlanType::Business,
337+
crate::types::PlanType::Enterprise => AccountPlanType::Enterprise,
338+
crate::types::PlanType::Edu | crate::types::PlanType::Education => AccountPlanType::Edu,
339+
crate::types::PlanType::Guest
340+
| crate::types::PlanType::Go
341+
| crate::types::PlanType::FreeWorkspace
342+
| crate::types::PlanType::Quorum
343+
| crate::types::PlanType::K12 => AccountPlanType::Unknown,
344+
}
345+
}
346+
328347
fn window_minutes_from_seconds(seconds: i32) -> Option<i64> {
329348
if seconds <= 0 {
330349
return None;

codex-rs/codex-api/src/rate_limits.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ pub fn parse_rate_limit(headers: &HeaderMap) -> Option<RateLimitSnapshot> {
3737
primary,
3838
secondary,
3939
credits,
40+
plan_type: None,
4041
})
4142
}
4243

codex-rs/core/src/auth.rs

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -227,23 +227,6 @@ impl CodexAuth {
227227
})
228228
}
229229

230-
/// Raw plan string from the ID token (including unknown/new plan types).
231-
pub fn raw_plan_type(&self) -> Option<String> {
232-
self.get_plan_type().map(|plan| match plan {
233-
InternalPlanType::Known(k) => format!("{k:?}"),
234-
InternalPlanType::Unknown(raw) => raw,
235-
})
236-
}
237-
238-
/// Raw internal plan value from the ID token.
239-
/// Exposes the underlying `token_data::PlanType` without mapping it to the
240-
/// public `AccountPlanType`. Use this when downstream code needs to inspect
241-
/// internal/unknown plan strings exactly as issued in the token.
242-
pub(crate) fn get_plan_type(&self) -> Option<InternalPlanType> {
243-
self.get_current_token_data()
244-
.and_then(|t| t.id_token.chatgpt_plan_type)
245-
}
246-
247230
fn get_current_auth_json(&self) -> Option<AuthDotJson> {
248231
#[expect(clippy::unwrap_used)]
249232
self.auth_dot_json.lock().unwrap().clone()
@@ -1041,10 +1024,6 @@ mod tests {
10411024
.expect("auth available");
10421025

10431026
pretty_assertions::assert_eq!(auth.account_plan_type(), Some(AccountPlanType::Pro));
1044-
pretty_assertions::assert_eq!(
1045-
auth.get_plan_type(),
1046-
Some(InternalPlanType::Known(InternalKnownPlan::Pro))
1047-
);
10481027
}
10491028

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

10671046
pretty_assertions::assert_eq!(auth.account_plan_type(), Some(AccountPlanType::Unknown));
1068-
pretty_assertions::assert_eq!(
1069-
auth.get_plan_type(),
1070-
Some(InternalPlanType::Unknown("mystery-tier".to_string()))
1071-
);
10721047
}
10731048
}
10741049

codex-rs/core/src/codex.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2598,6 +2598,7 @@ mod tests {
25982598
unlimited: false,
25992599
balance: Some("10.00".to_string()),
26002600
}),
2601+
plan_type: Some(codex_protocol::account::PlanType::Plus),
26012602
};
26022603
state.set_rate_limits(initial.clone());
26032604

@@ -2613,6 +2614,7 @@ mod tests {
26132614
resets_at: Some(1_900),
26142615
}),
26152616
credits: None,
2617+
plan_type: None,
26162618
};
26172619
state.set_rate_limits(update.clone());
26182620

@@ -2622,6 +2624,78 @@ mod tests {
26222624
primary: update.primary.clone(),
26232625
secondary: update.secondary,
26242626
credits: initial.credits,
2627+
plan_type: initial.plan_type,
2628+
})
2629+
);
2630+
}
2631+
2632+
#[test]
2633+
fn set_rate_limits_updates_plan_type_when_present() {
2634+
let codex_home = tempfile::tempdir().expect("create temp dir");
2635+
let config = Config::load_from_base_config_with_overrides(
2636+
ConfigToml::default(),
2637+
ConfigOverrides::default(),
2638+
codex_home.path().to_path_buf(),
2639+
)
2640+
.expect("load default test config");
2641+
let config = Arc::new(config);
2642+
let session_configuration = SessionConfiguration {
2643+
provider: config.model_provider.clone(),
2644+
model: config.model.clone(),
2645+
model_reasoning_effort: config.model_reasoning_effort,
2646+
model_reasoning_summary: config.model_reasoning_summary,
2647+
developer_instructions: config.developer_instructions.clone(),
2648+
user_instructions: config.user_instructions.clone(),
2649+
base_instructions: config.base_instructions.clone(),
2650+
compact_prompt: config.compact_prompt.clone(),
2651+
approval_policy: config.approval_policy,
2652+
sandbox_policy: config.sandbox_policy.clone(),
2653+
cwd: config.cwd.clone(),
2654+
original_config_do_not_use: Arc::clone(&config),
2655+
exec_policy: Arc::new(RwLock::new(ExecPolicy::empty())),
2656+
session_source: SessionSource::Exec,
2657+
};
2658+
2659+
let mut state = SessionState::new(session_configuration);
2660+
let initial = RateLimitSnapshot {
2661+
primary: Some(RateLimitWindow {
2662+
used_percent: 15.0,
2663+
window_minutes: Some(20),
2664+
resets_at: Some(1_600),
2665+
}),
2666+
secondary: Some(RateLimitWindow {
2667+
used_percent: 5.0,
2668+
window_minutes: Some(45),
2669+
resets_at: Some(1_650),
2670+
}),
2671+
credits: Some(CreditsSnapshot {
2672+
has_credits: true,
2673+
unlimited: false,
2674+
balance: Some("15.00".to_string()),
2675+
}),
2676+
plan_type: Some(codex_protocol::account::PlanType::Plus),
2677+
};
2678+
state.set_rate_limits(initial.clone());
2679+
2680+
let update = RateLimitSnapshot {
2681+
primary: Some(RateLimitWindow {
2682+
used_percent: 35.0,
2683+
window_minutes: Some(25),
2684+
resets_at: Some(1_700),
2685+
}),
2686+
secondary: None,
2687+
credits: None,
2688+
plan_type: Some(codex_protocol::account::PlanType::Pro),
2689+
};
2690+
state.set_rate_limits(update.clone());
2691+
2692+
assert_eq!(
2693+
state.latest_rate_limits,
2694+
Some(RateLimitSnapshot {
2695+
primary: update.primary,
2696+
secondary: update.secondary,
2697+
credits: initial.credits,
2698+
plan_type: update.plan_type,
26252699
})
26262700
);
26272701
}

codex-rs/core/src/error.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,7 @@ mod tests {
560560
resets_at: Some(secondary_reset_at),
561561
}),
562562
credits: None,
563+
plan_type: None,
563564
}
564565
}
565566

codex-rs/core/src/state/session.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ impl SessionState {
6262
}
6363

6464
pub(crate) fn set_rate_limits(&mut self, snapshot: RateLimitSnapshot) {
65-
self.latest_rate_limits = Some(merge_rate_limit_credits(
65+
self.latest_rate_limits = Some(merge_rate_limit_fields(
6666
self.latest_rate_limits.as_ref(),
6767
snapshot,
6868
));
@@ -83,13 +83,16 @@ impl SessionState {
8383
}
8484
}
8585

86-
// Sometimes new snapshots don't include credits
87-
fn merge_rate_limit_credits(
86+
// Sometimes new snapshots don't include credits or plan information.
87+
fn merge_rate_limit_fields(
8888
previous: Option<&RateLimitSnapshot>,
8989
mut snapshot: RateLimitSnapshot,
9090
) -> RateLimitSnapshot {
9191
if snapshot.credits.is_none() {
9292
snapshot.credits = previous.and_then(|prior| prior.credits.clone());
9393
}
94+
if snapshot.plan_type.is_none() {
95+
snapshot.plan_type = previous.and_then(|prior| prior.plan_type);
96+
}
9497
snapshot
9598
}

0 commit comments

Comments
 (0)