diff --git a/codex-rs/codex-api/src/api_bridge.rs b/codex-rs/codex-api/src/api_bridge.rs index e8d7c4743c..6b79b4754a 100644 --- a/codex-rs/codex-api/src/api_bridge.rs +++ b/codex-rs/codex-api/src/api_bridge.rs @@ -159,6 +159,19 @@ pub fn map_api_error(err: ApiError) -> CodexErr { }); } else if err.error.error_type.as_deref() == Some("usage_not_included") { return CodexErr::UsageNotIncluded; + } else if err.error.error_type.as_deref() == Some("insufficient_quota") + || matches!( + err.error.code.as_deref(), + Some( + "insufficient_quota" + | "credit_balance_exhausted" + | "organization_spend_limit_exceeded" + | "project_spend_limit_exceeded" + | "organization_usage_limit_exceeded" + ) + ) + { + return CodexErr::QuotaExceeded; } } @@ -263,6 +276,7 @@ struct UsageErrorResponse { #[derive(Debug, Deserialize)] struct UsageErrorBody { + code: Option, #[serde(rename = "type")] error_type: Option, plan_type: Option, diff --git a/codex-rs/codex-api/src/api_bridge_tests.rs b/codex-rs/codex-api/src/api_bridge_tests.rs index 666dea8864..51fb5a28b2 100644 --- a/codex-rs/codex-api/src/api_bridge_tests.rs +++ b/codex-rs/codex-api/src/api_bridge_tests.rs @@ -322,6 +322,36 @@ fn map_api_error_keeps_unknown_400_errors_generic() { assert_eq!(message, &body); } +#[test] +fn map_api_error_distinguishes_http_quota_errors_from_rate_limits() { + for error in [ + serde_json::json!({"type": "insufficient_quota"}), + serde_json::json!({"code": "insufficient_quota"}), + serde_json::json!({"code": "credit_balance_exhausted"}), + serde_json::json!({"code": "organization_spend_limit_exceeded"}), + serde_json::json!({"code": "project_spend_limit_exceeded"}), + serde_json::json!({"code": "organization_usage_limit_exceeded"}), + serde_json::json!({"type": "rate_limit_error", "code": "rate_limit_exceeded"}), + serde_json::json!({"type": "rate_limit_error", "code": "slow_down"}), + ] { + let expected = if error["type"] == "rate_limit_error" { + CodexErrorInfo::ResponseTooManyFailedAttempts { + http_status_code: Some(429), + } + } else { + CodexErrorInfo::UsageLimitExceeded + }; + let err = map_api_error(ApiError::Transport(TransportError::Http { + status: http::StatusCode::TOO_MANY_REQUESTS, + url: None, + headers: None, + body: Some(serde_json::json!({"error": error}).to_string()), + })); + + assert_eq!(err.to_codex_protocol_error(), expected, "{error}"); + } +} + #[test] fn map_api_error_maps_usage_limit_limit_name_header() { let mut headers = HeaderMap::new();