Better usage errors (#1941)
<img width="771" height="279" alt="image" src="https://github.com/user-attachments/assets/e56f967f-bcd7-49f7-8a94-3d88df68b65a" />
This commit is contained in:
@@ -40,6 +40,16 @@ use crate::protocol::TokenUsage;
|
||||
use crate::util::backoff;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ErrorResponse {
|
||||
error: Error,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Error {
|
||||
code: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ModelClient {
|
||||
config: Arc<Config>,
|
||||
@@ -225,6 +235,14 @@ impl ModelClient {
|
||||
}
|
||||
Ok(res) => {
|
||||
let status = res.status();
|
||||
|
||||
// Pull out Retry‑After header if present.
|
||||
let retry_after_secs = res
|
||||
.headers()
|
||||
.get(reqwest::header::RETRY_AFTER)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|s| s.parse::<u64>().ok());
|
||||
|
||||
// The OpenAI Responses endpoint returns structured JSON bodies even for 4xx/5xx
|
||||
// errors. When we bubble early with only the HTTP status the caller sees an opaque
|
||||
// "unexpected status 400 Bad Request" which makes debugging nearly impossible.
|
||||
@@ -238,17 +256,24 @@ impl ModelClient {
|
||||
return Err(CodexErr::UnexpectedStatus(status, body));
|
||||
}
|
||||
|
||||
if status == StatusCode::TOO_MANY_REQUESTS {
|
||||
let body = res.json::<ErrorResponse>().await.ok();
|
||||
if let Some(ErrorResponse {
|
||||
error: Error { code, .. },
|
||||
}) = body
|
||||
{
|
||||
if code == "usage_limit_reached" {
|
||||
return Err(CodexErr::UsageLimitReached);
|
||||
} else if code == "usage_not_included" {
|
||||
return Err(CodexErr::UsageNotIncluded);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if attempt > max_retries {
|
||||
return Err(CodexErr::RetryLimit(status));
|
||||
}
|
||||
|
||||
// Pull out Retry‑After header if present.
|
||||
let retry_after_secs = res
|
||||
.headers()
|
||||
.get(reqwest::header::RETRY_AFTER)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|s| s.parse::<u64>().ok());
|
||||
|
||||
let delay = retry_after_secs
|
||||
.map(|s| Duration::from_millis(s * 1_000))
|
||||
.unwrap_or_else(|| backoff(attempt));
|
||||
|
||||
@@ -1290,6 +1290,7 @@ async fn run_turn(
|
||||
Ok(output) => return Ok(output),
|
||||
Err(CodexErr::Interrupted) => return Err(CodexErr::Interrupted),
|
||||
Err(CodexErr::EnvVar(var)) => return Err(CodexErr::EnvVar(var)),
|
||||
Err(e @ (CodexErr::UsageLimitReached | CodexErr::UsageNotIncluded)) => return Err(e),
|
||||
Err(e) => {
|
||||
// Use the configured provider-specific stream retry budget.
|
||||
let max_retries = sess.client.get_provider().stream_max_retries();
|
||||
|
||||
@@ -62,6 +62,12 @@ pub enum CodexErr {
|
||||
#[error("unexpected status {0}: {1}")]
|
||||
UnexpectedStatus(StatusCode, String),
|
||||
|
||||
#[error("Usage limit has been reached")]
|
||||
UsageLimitReached,
|
||||
|
||||
#[error("Usage not included with the plan")]
|
||||
UsageNotIncluded,
|
||||
|
||||
/// Retry limit exceeded.
|
||||
#[error("exceeded retry limit, last status: {0}")]
|
||||
RetryLimit(StatusCode),
|
||||
|
||||
Reference in New Issue
Block a user