An AI agent built to do Ralph loops - plan mode for planning and ralph mode for implementing.
at main 134 lines 3.7 kB view raw
1use reqwest::StatusCode; 2use std::{borrow::Cow, fmt, time::Duration}; 3 4#[derive(Debug, Clone, Copy, PartialEq, Eq)] 5pub enum ErrorKind { 6 RateLimited, 7 Transient, 8 Auth, 9 BadRequest, 10 Unknown, 11} 12 13#[derive(Debug)] 14pub struct LlmError { 15 pub provider: &'static str, 16 pub kind: ErrorKind, 17 pub status: Option<StatusCode>, 18 pub retry_after: Option<Duration>, 19 pub user_message: Cow<'static, str>, 20 pub raw: Option<String>, 21} 22 23impl fmt::Display for LlmError { 24 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 25 write!(f, "{}", self.user_message) 26 } 27} 28 29impl std::error::Error for LlmError {} 30 31impl LlmError { 32 pub fn rate_limited( 33 provider: &'static str, 34 retry_after: Option<Duration>, 35 raw: Option<String>, 36 ) -> Self { 37 Self { 38 provider, 39 kind: ErrorKind::RateLimited, 40 status: Some(StatusCode::TOO_MANY_REQUESTS), 41 retry_after, 42 user_message: "Rate limited by provider; retrying shortly.".into(), 43 raw, 44 } 45 } 46 47 pub fn transient( 48 provider: &'static str, 49 status: Option<StatusCode>, 50 raw: Option<String>, 51 ) -> Self { 52 Self { 53 provider, 54 kind: ErrorKind::Transient, 55 status, 56 retry_after: None, 57 user_message: "Temporary network/provider error.".into(), 58 raw, 59 } 60 } 61 62 pub fn auth(provider: &'static str, raw: Option<String>) -> Self { 63 Self { 64 provider, 65 kind: ErrorKind::Auth, 66 status: Some(StatusCode::UNAUTHORIZED), 67 retry_after: None, 68 user_message: "Authentication failed. Check API key.".into(), 69 raw, 70 } 71 } 72 73 pub fn bad_request(provider: &'static str, raw: Option<String>) -> Self { 74 Self { 75 provider, 76 kind: ErrorKind::BadRequest, 77 status: Some(StatusCode::BAD_REQUEST), 78 retry_after: None, 79 user_message: "Request rejected by provider.".into(), 80 raw, 81 } 82 } 83 84 pub fn network(provider: &'static str, raw: Option<String>) -> Self { 85 Self { 86 provider, 87 kind: ErrorKind::Transient, 88 status: None, 89 retry_after: None, 90 user_message: "Network error contacting provider.".into(), 91 raw, 92 } 93 } 94 95 pub fn from_status( 96 provider: &'static str, 97 status: StatusCode, 98 body: String, 99 retry_after: Option<Duration>, 100 ) -> Self { 101 let kind = classify_status(status); 102 let user_message: Cow<'static, str> = match kind { 103 ErrorKind::RateLimited => "Rate limited by provider; retrying shortly.".into(), 104 ErrorKind::Transient => "Temporary provider error.".into(), 105 ErrorKind::Auth => "Authentication failed. Check API key.".into(), 106 ErrorKind::BadRequest => "Request rejected by provider.".into(), 107 ErrorKind::Unknown => "Unexpected provider error.".into(), 108 }; 109 110 Self { 111 provider, 112 kind, 113 status: Some(status), 114 retry_after, 115 user_message, 116 raw: Some(body), 117 } 118 } 119 120 pub fn is_retryable(&self) -> bool { 121 matches!(self.kind, ErrorKind::RateLimited | ErrorKind::Transient) 122 } 123} 124 125pub fn classify_status(status: StatusCode) -> ErrorKind { 126 match status.as_u16() { 127 429 => ErrorKind::RateLimited, 128 401 | 403 => ErrorKind::Auth, 129 408 | 425 => ErrorKind::Transient, 130 400..=499 => ErrorKind::BadRequest, 131 500..=599 => ErrorKind::Transient, 132 _ => ErrorKind::Unknown, 133 } 134}