An AI agent built to do Ralph loops - plan mode for planning and ralph mode for implementing.
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}