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 crate::util::backoff;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct ErrorResponse {
|
||||||
|
error: Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct Error {
|
||||||
|
code: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ModelClient {
|
pub struct ModelClient {
|
||||||
config: Arc<Config>,
|
config: Arc<Config>,
|
||||||
@@ -225,6 +235,14 @@ impl ModelClient {
|
|||||||
}
|
}
|
||||||
Ok(res) => {
|
Ok(res) => {
|
||||||
let status = res.status();
|
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
|
// 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
|
// 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.
|
// "unexpected status 400 Bad Request" which makes debugging nearly impossible.
|
||||||
@@ -238,17 +256,24 @@ impl ModelClient {
|
|||||||
return Err(CodexErr::UnexpectedStatus(status, body));
|
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 {
|
if attempt > max_retries {
|
||||||
return Err(CodexErr::RetryLimit(status));
|
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
|
let delay = retry_after_secs
|
||||||
.map(|s| Duration::from_millis(s * 1_000))
|
.map(|s| Duration::from_millis(s * 1_000))
|
||||||
.unwrap_or_else(|| backoff(attempt));
|
.unwrap_or_else(|| backoff(attempt));
|
||||||
|
|||||||
@@ -1290,6 +1290,7 @@ async fn run_turn(
|
|||||||
Ok(output) => return Ok(output),
|
Ok(output) => return Ok(output),
|
||||||
Err(CodexErr::Interrupted) => return Err(CodexErr::Interrupted),
|
Err(CodexErr::Interrupted) => return Err(CodexErr::Interrupted),
|
||||||
Err(CodexErr::EnvVar(var)) => return Err(CodexErr::EnvVar(var)),
|
Err(CodexErr::EnvVar(var)) => return Err(CodexErr::EnvVar(var)),
|
||||||
|
Err(e @ (CodexErr::UsageLimitReached | CodexErr::UsageNotIncluded)) => return Err(e),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Use the configured provider-specific stream retry budget.
|
// Use the configured provider-specific stream retry budget.
|
||||||
let max_retries = sess.client.get_provider().stream_max_retries();
|
let max_retries = sess.client.get_provider().stream_max_retries();
|
||||||
|
|||||||
@@ -62,6 +62,12 @@ pub enum CodexErr {
|
|||||||
#[error("unexpected status {0}: {1}")]
|
#[error("unexpected status {0}: {1}")]
|
||||||
UnexpectedStatus(StatusCode, String),
|
UnexpectedStatus(StatusCode, String),
|
||||||
|
|
||||||
|
#[error("Usage limit has been reached")]
|
||||||
|
UsageLimitReached,
|
||||||
|
|
||||||
|
#[error("Usage not included with the plan")]
|
||||||
|
UsageNotIncluded,
|
||||||
|
|
||||||
/// Retry limit exceeded.
|
/// Retry limit exceeded.
|
||||||
#[error("exceeded retry limit, last status: {0}")]
|
#[error("exceeded retry limit, last status: {0}")]
|
||||||
RetryLimit(StatusCode),
|
RetryLimit(StatusCode),
|
||||||
|
|||||||
Reference in New Issue
Block a user