mirror of
https://github.com/openai/codex.git
synced 2026-11-06 16:32:29 +00:00
[Codex][CLI] Show model-capacity guidance on 429 (#10118)
###### Problem
Users get generic 429s with no guidance when a model is at capacity.
###### Solution
Detect model-cap headers, surface a clear “try a different model”
message, and keep behavior non‑intrusive (no auto‑switch).
###### Scope
CLI/TUI only; protocol + error mapping updated to carry model‑cap info.
###### Tests
- just fmt
- cargo test -p codex-tui
- cargo test -p codex-core --lib
shell_snapshot::tests::try_new_creates_and_deletes_snapshot_file --
--nocapture (ran in isolated env)
- validate local build with backend
<img width="719" height="845" alt="image"
src="https://github.com/user-attachments/assets/1470b33d-0974-4b1f-b8e6-d11f892f4b54"
/>
This commit is contained in:
1 parent
ca9d417633
commit
b79bf69af6
6 files changed
+268
-2
No files matched your search
@@ -49,6 +49,7 @@ use codex_core::protocol::AgentReasoningRawContentDeltaEvent;
|
||||
use codex_core::protocol::AgentReasoningRawContentEvent;
|
||||
use codex_core::protocol::ApplyPatchApprovalRequestEvent;
|
||||
use codex_core::protocol::BackgroundEventEvent;
|
||||
use codex_core::protocol::CodexErrorInfo;
|
||||
use codex_core::protocol::CreditsSnapshot;
|
||||
use codex_core::protocol::DeprecationNoticeEvent;
|
||||
use codex_core::protocol::ErrorEvent;
|
||||
@@ -399,6 +400,33 @@ enum ConnectorsCacheState {
|
||||
Failed(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum RateLimitErrorKind {
|
||||
ModelCap {
|
||||
model: String,
|
||||
reset_after_seconds: Option<u64>,
|
||||
},
|
||||
UsageLimit,
|
||||
Generic,
|
||||
}
|
||||
|
||||
fn rate_limit_error_kind(info: &CodexErrorInfo) -> Option<RateLimitErrorKind> {
|
||||
match info {
|
||||
CodexErrorInfo::ModelCap {
|
||||
model,
|
||||
reset_after_seconds,
|
||||
} => Some(RateLimitErrorKind::ModelCap {
|
||||
model: model.clone(),
|
||||
reset_after_seconds: *reset_after_seconds,
|
||||
}),
|
||||
CodexErrorInfo::UsageLimitExceeded => Some(RateLimitErrorKind::UsageLimit),
|
||||
CodexErrorInfo::ResponseTooManyFailedAttempts {
|
||||
http_status_code: Some(429),
|
||||
} => Some(RateLimitErrorKind::Generic),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub(crate) enum ExternalEditorState {
|
||||
#[default]
|
||||
@@ -1156,6 +1184,24 @@ impl ChatWidget {
|
||||
self.maybe_show_pending_rate_limit_prompt();
|
||||
}
|
||||
|
||||
fn on_model_cap_error(&mut self, model: String, reset_after_seconds: Option<u64>) {
|
||||
self.finalize_turn();
|
||||
|
||||
let mut message = format!("Model {model} is at capacity. Please try a different model.");
|
||||
if let Some(seconds) = reset_after_seconds {
|
||||
message.push_str(&format!(
|
||||
" Try again in {}.",
|
||||
format_duration_short(seconds)
|
||||
));
|
||||
} else {
|
||||
message.push_str(" Try again later.");
|
||||
}
|
||||
|
||||
self.add_to_history(history_cell::new_warning_event(message));
|
||||
self.request_redraw();
|
||||
self.maybe_send_next_queued_input();
|
||||
}
|
||||
|
||||
fn on_error(&mut self, message: String) {
|
||||
self.finalize_turn();
|
||||
self.add_to_history(history_cell::new_error_event(message));
|
||||
@@ -3085,7 +3131,26 @@ impl ChatWidget {
|
||||
self.on_rate_limit_snapshot(ev.rate_limits);
|
||||
}
|
||||
EventMsg::Warning(WarningEvent { message }) => self.on_warning(message),
|
||||
EventMsg::Error(ErrorEvent { message, .. }) => self.on_error(message),
|
||||
EventMsg::Error(ErrorEvent {
|
||||
message,
|
||||
codex_error_info,
|
||||
}) => {
|
||||
if let Some(info) = codex_error_info
|
||||
&& let Some(kind) = rate_limit_error_kind(&info)
|
||||
{
|
||||
match kind {
|
||||
RateLimitErrorKind::ModelCap {
|
||||
model,
|
||||
reset_after_seconds,
|
||||
} => self.on_model_cap_error(model, reset_after_seconds),
|
||||
RateLimitErrorKind::UsageLimit | RateLimitErrorKind::Generic => {
|
||||
self.on_error(message)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.on_error(message);
|
||||
}
|
||||
}
|
||||
EventMsg::McpStartupUpdate(ev) => self.on_mcp_startup_update(ev),
|
||||
EventMsg::McpStartupComplete(ev) => self.on_mcp_startup_complete(ev),
|
||||
EventMsg::TurnAborted(ev) => match ev.reason {
|
||||
@@ -5911,5 +5976,17 @@ pub(crate) fn show_review_commit_picker_with_entries(
|
||||
});
|
||||
}
|
||||
|
||||
fn format_duration_short(seconds: u64) -> String {
|
||||
if seconds < 60 {
|
||||
"less than a minute".to_string()
|
||||
} else if seconds < 3600 {
|
||||
format!("{}m", seconds / 60)
|
||||
} else if seconds < 86_400 {
|
||||
format!("{}h", seconds / 3600)
|
||||
} else {
|
||||
format!("{}d", seconds / 86_400)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests;
|
||||
@@ -3021,6 +3021,43 @@ async fn model_picker_hides_show_in_picker_false_models_from_cache() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn model_cap_error_does_not_switch_models() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(Some("boomslang")).await;
|
||||
chat.set_model("boomslang");
|
||||
while rx.try_recv().is_ok() {}
|
||||
while op_rx.try_recv().is_ok() {}
|
||||
|
||||
chat.handle_codex_event(Event {
|
||||
id: "err-1".to_string(),
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
message: "model cap".to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::ModelCap {
|
||||
model: "boomslang".to_string(),
|
||||
reset_after_seconds: Some(120),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
while let Ok(event) = rx.try_recv() {
|
||||
if let AppEvent::UpdateModel(model) = event {
|
||||
assert_eq!(
|
||||
model, "boomslang",
|
||||
"did not expect model switch on model-cap error"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
while let Ok(event) = op_rx.try_recv() {
|
||||
if let Op::OverrideTurnContext { model, .. } = event {
|
||||
assert!(
|
||||
model.is_none(),
|
||||
"did not expect OverrideTurnContext model update on model-cap error"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn approvals_selection_popup_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
|
||||
Reference in new issue
Block a user