plan type reconciliation

This commit is contained in:
kevin zhao
2025-11-17 15:04:52 -08:00
parent 6259f91e5d
commit c7d8c38d2f
6 changed files with 88 additions and 9 deletions
@@ -967,6 +967,7 @@ impl CodexMessageProcessor {
client
.get_rate_limits()
.await
.map(|status| status.snapshot)
.map_err(|err| JSONRPCErrorError {
code: INTERNAL_ERROR_CODE,
message: format!("failed to fetch codex rate limits: {err}"),
+27 -2
View File
@@ -1,5 +1,6 @@
use crate::types::CodeTaskDetailsResponse;
use crate::types::PaginatedListTaskListItem;
use crate::types::PlanType;
use crate::types::RateLimitStatusPayload;
use crate::types::RateLimitWindowSnapshot;
use crate::types::TurnAttemptsSiblingTurnsResponse;
@@ -44,6 +45,12 @@ pub struct Client {
path_style: PathStyle,
}
#[derive(Clone, Debug)]
pub struct RateLimitStatus {
pub plan_type: String,
pub snapshot: RateLimitSnapshot,
}
impl Client {
pub fn new(base_url: impl Into<String>) -> Result<Self> {
let mut base_url = base_url.into();
@@ -155,7 +162,7 @@ impl Client {
}
}
pub async fn get_rate_limits(&self) -> Result<RateLimitSnapshot> {
pub async fn get_rate_limits(&self) -> Result<RateLimitStatus> {
let url = match self.path_style {
PathStyle::CodexApi => format!("{}/api/codex/usage", self.base_url),
PathStyle::ChatGptApi => format!("{}/wham/usage", self.base_url),
@@ -163,7 +170,9 @@ impl Client {
let req = self.http.get(&url).headers(self.headers());
let (body, ct) = self.exec_request(req, "GET", &url).await?;
let payload: RateLimitStatusPayload = self.decode_json(&url, &ct, &body)?;
Ok(Self::rate_limit_snapshot_from_payload(payload))
let plan_type = Self::plan_type_to_string(payload.plan_type);
let snapshot = Self::rate_limit_snapshot_from_payload(payload);
Ok(RateLimitStatus { plan_type, snapshot })
}
pub async fn list_tasks(
@@ -288,6 +297,22 @@ impl Client {
}
}
fn plan_type_to_string(plan_type: PlanType) -> String {
match plan_type {
PlanType::Free => "free",
PlanType::Go => "go",
PlanType::Plus => "plus",
PlanType::Pro => "pro",
PlanType::Team => "team",
PlanType::Business => "business",
PlanType::Education => "education",
PlanType::Quorum => "quorum",
PlanType::Enterprise => "enterprise",
PlanType::Edu => "edu",
}
.to_string()
}
fn map_rate_limit_window(
window: Option<Option<Box<RateLimitWindowSnapshot>>>,
) -> Option<RateLimitWindow> {
+1
View File
@@ -2,6 +2,7 @@ mod client;
pub mod types;
pub use client::Client;
pub use client::RateLimitStatus;
pub use types::CodeTaskDetailsResponse;
pub use types::CodeTaskDetailsResponseExt;
pub use types::PaginatedListTaskListItem;
+5 -2
View File
@@ -496,8 +496,11 @@ impl App {
AppEvent::FileSearchResult { query, matches } => {
self.chat_widget.apply_file_search_result(query, matches);
}
AppEvent::RateLimitSnapshotFetched(snapshot) => {
self.chat_widget.on_rate_limit_snapshot(Some(snapshot));
AppEvent::RateLimitSnapshotFetched(status) => {
self.chat_widget
.on_rate_limit_snapshot(Some(status.snapshot.clone()));
self.chat_widget
.reconcile_plan_type_from_usage(status.plan_type);
}
AppEvent::UpdateReasoningEffort(effort) => {
self.on_update_reasoning_effort(effort);
+2 -2
View File
@@ -1,10 +1,10 @@
use std::path::PathBuf;
use codex_backend_client::RateLimitStatus;
use codex_common::approval_presets::ApprovalPreset;
use codex_common::model_presets::ModelPreset;
use codex_core::protocol::ConversationPathResponseEvent;
use codex_core::protocol::Event;
use codex_core::protocol::RateLimitSnapshot;
use codex_file_search::FileMatch;
use crate::bottom_pane::ApprovalRequest;
@@ -43,7 +43,7 @@ pub(crate) enum AppEvent {
},
/// Result of refreshing rate limits
RateLimitSnapshotFetched(RateLimitSnapshot),
RateLimitSnapshotFetched(RateLimitStatus),
/// Result of computing a `/diff` command.
DiffResult(String),
+52 -3
View File
@@ -6,6 +6,7 @@ use std::time::Duration;
use codex_app_server_protocol::AuthMode;
use codex_backend_client::Client as BackendClient;
use codex_backend_client::RateLimitStatus;
use codex_core::config::Config;
use codex_core::config::types::Notifications;
use codex_core::git_info::current_branch_name;
@@ -261,6 +262,7 @@ pub(crate) struct ChatWidget {
rate_limit_warnings: RateLimitWarningState,
rate_limit_switch_prompt: RateLimitSwitchPromptState,
rate_limit_poller: Option<JoinHandle<()>>,
last_plan_refresh_attempt: Option<String>,
// Stream lifecycle controller
stream_controller: Option<StreamController>,
running_commands: HashMap<String, RunningCommand>,
@@ -553,6 +555,51 @@ impl ChatWidget {
self.rate_limit_snapshot = None;
}
}
pub(crate) fn reconcile_plan_type_from_usage(&mut self, backend_plan: String) {
let trimmed = backend_plan.trim();
if trimmed.is_empty() {
return;
}
let plan_lower = trimmed.to_ascii_lowercase();
let Some(auth) = self.auth_manager.auth() else {
self.last_plan_refresh_attempt = None;
return;
};
if auth.mode != AuthMode::ChatGPT {
self.last_plan_refresh_attempt = None;
return;
}
let current_plan = auth
.raw_plan_type()
.map(|plan| plan.to_ascii_lowercase());
if current_plan
.as_deref()
.is_some_and(|plan| plan == plan_lower)
{
self.last_plan_refresh_attempt = None;
return;
}
if self
.last_plan_refresh_attempt
.as_deref()
.is_some_and(|attempt| attempt == plan_lower)
{
return;
}
self.last_plan_refresh_attempt = Some(plan_lower);
let auth_manager = self.auth_manager.clone();
tokio::spawn(async move {
if let Err(err) = auth_manager.refresh_token().await {
debug!(?err, "failed to refresh auth after plan mismatch");
}
});
}
/// Finalize any active exec as failed and stop/clear running UI state.
fn finalize_turn(&mut self) {
// Ensure any spinner is replaced by a red ✗ and flushed into history.
@@ -1065,6 +1112,7 @@ impl ChatWidget {
rate_limit_warnings: RateLimitWarningState::default(),
rate_limit_switch_prompt: RateLimitSwitchPromptState::default(),
rate_limit_poller: None,
last_plan_refresh_attempt: None,
stream_controller: None,
running_commands: HashMap::new(),
task_complete_pending: false,
@@ -1137,6 +1185,7 @@ impl ChatWidget {
rate_limit_warnings: RateLimitWarningState::default(),
rate_limit_switch_prompt: RateLimitSwitchPromptState::default(),
rate_limit_poller: None,
last_plan_refresh_attempt: None,
stream_controller: None,
running_commands: HashMap::new(),
task_complete_pending: false,
@@ -1776,8 +1825,8 @@ impl ChatWidget {
let mut interval = tokio::time::interval(Duration::from_secs(60));
loop {
if let Some(snapshot) = fetch_rate_limits(base_url.clone(), auth.clone()).await {
app_event_tx.send(AppEvent::RateLimitSnapshotFetched(snapshot));
if let Some(status) = fetch_rate_limits(base_url.clone(), auth.clone()).await {
app_event_tx.send(AppEvent::RateLimitSnapshotFetched(status));
}
interval.tick().await;
}
@@ -2946,7 +2995,7 @@ fn extract_first_bold(s: &str) -> Option<String> {
None
}
async fn fetch_rate_limits(base_url: String, auth: CodexAuth) -> Option<RateLimitSnapshot> {
async fn fetch_rate_limits(base_url: String, auth: CodexAuth) -> Option<RateLimitStatus> {
match BackendClient::from_auth(base_url, &auth).await {
Ok(client) => match client.get_rate_limits().await {
Ok(snapshot) => Some(snapshot),