caliban_provider/
error.rs1use std::time::Duration;
4
5#[derive(thiserror::Error, Debug)]
7pub enum Error {
8 #[error("authentication failed: {0}")]
10 Auth(String),
11
12 #[error("rate limit exceeded (retry after {retry_after:?})")]
14 RateLimit {
15 retry_after: Option<Duration>,
17 },
18
19 #[error("invalid request: {0}")]
21 InvalidRequest(String),
22
23 #[error("context too long: requested {requested_tokens} but max is {max_tokens}")]
25 ContextTooLong {
26 max_tokens: u32,
28 requested_tokens: u32,
30 },
31
32 #[error("model unavailable: {0}")]
34 ModelUnavailable(String),
35
36 #[error("server error (HTTP {status}): {body}")]
38 ServerError {
39 status: u16,
41 body: String,
43 },
44
45 #[error("upstream server fault: {0}")]
52 UpstreamServerFault(String),
53
54 #[error("content filter triggered: {0}")]
56 ContentFilter(String),
57
58 #[error("network error: {0}")]
60 Network(Box<dyn std::error::Error + Send + Sync>),
61
62 #[error("stream interrupted mid-response: {0}")]
71 StreamInterrupted(String),
72
73 #[error("operation cancelled")]
75 Cancelled,
76
77 #[error("adapter error: {0}")]
79 Adapter(#[source] Box<dyn std::error::Error + Send + Sync>),
80
81 #[error("stream idle for {0:?}")]
83 StreamIdle(std::time::Duration),
84}
85
86pub type Result<T> = std::result::Result<T, Error>;
88
89impl Error {
90 pub fn network(e: impl std::error::Error + Send + Sync + 'static) -> Self {
92 Self::Network(Box::new(e))
93 }
94
95 pub fn adapter(e: impl std::error::Error + Send + Sync + 'static) -> Self {
97 Self::Adapter(Box::new(e))
98 }
99
100 pub fn stream_interrupted(inner: impl std::fmt::Display) -> Self {
104 Self::StreamInterrupted(inner.to_string())
105 }
106}
107
108#[must_use]
115pub fn is_auth_error(err: &Error) -> bool {
116 match err {
117 Error::Auth(_) => true,
118 Error::ServerError { status, .. } => *status == 401 || *status == 403,
119 _ => false,
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn stream_interrupted_display_uses_clear_prefix() {
129 let e = Error::stream_interrupted("hyper: connection reset by peer");
133 assert_eq!(
134 e.to_string(),
135 "stream interrupted mid-response: hyper: connection reset by peer"
136 );
137 }
138
139 #[test]
140 fn stream_interrupted_constructor_accepts_display() {
141 let io = std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "eof");
144 let e = Error::stream_interrupted(io);
145 assert!(matches!(e, Error::StreamInterrupted(_)));
146 assert!(e.to_string().contains("eof"));
147 }
148
149 #[test]
150 fn is_auth_error_matches_explicit_auth_variant() {
151 assert!(is_auth_error(&Error::Auth("bad key".into())));
152 }
153
154 #[test]
155 fn is_auth_error_matches_server_error_401() {
156 assert!(is_auth_error(&Error::ServerError {
157 status: 401,
158 body: "unauthorized".into(),
159 }));
160 }
161
162 #[test]
163 fn is_auth_error_matches_server_error_403() {
164 assert!(is_auth_error(&Error::ServerError {
165 status: 403,
166 body: "forbidden".into(),
167 }));
168 }
169
170 #[test]
171 fn is_auth_error_rejects_other_server_errors() {
172 assert!(!is_auth_error(&Error::ServerError {
173 status: 500,
174 body: "boom".into(),
175 }));
176 assert!(!is_auth_error(&Error::ServerError {
177 status: 429,
178 body: "slow down".into(),
179 }));
180 }
181
182 #[test]
183 fn is_auth_error_rejects_unrelated_variants() {
184 assert!(!is_auth_error(&Error::RateLimit { retry_after: None }));
185 assert!(!is_auth_error(&Error::InvalidRequest("nope".into())));
186 assert!(!is_auth_error(&Error::Cancelled));
187 assert!(!is_auth_error(&Error::Network(Box::new(
188 std::io::Error::new(std::io::ErrorKind::ConnectionReset, "x")
189 ))));
190 }
191}