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:
pakrym-oai
2025-08-07 09:46:13 -07:00
committed by GitHub
parent bc28b87c7b
commit 62ed5907f9
3 changed files with 39 additions and 7 deletions

View File

@@ -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 RetryAfter 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 RetryAfter 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));

View File

@@ -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();

View File

@@ -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),