1use serde_json::Value;
6use thiserror::Error;
7
8const MAX_BODY_CHARS: usize = 4096;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum ApiErrorKind {
14 BadRequest,
16 Authentication,
18 PermissionDenied,
20 NotFound,
22 Conflict,
24 UnprocessableEntity,
26 RateLimit,
28 InternalServer,
30 Other,
32}
33
34impl ApiErrorKind {
35 pub fn from_status(status: u16) -> Self {
36 match status {
37 400 => Self::BadRequest,
38 401 => Self::Authentication,
39 403 => Self::PermissionDenied,
40 404 => Self::NotFound,
41 409 => Self::Conflict,
42 422 => Self::UnprocessableEntity,
43 429 => Self::RateLimit,
44 s if s >= 500 => Self::InternalServer,
45 _ => Self::Other,
46 }
47 }
48}
49
50#[derive(Debug, Clone, PartialEq)]
52pub struct ApiError {
53 pub status: u16,
54 pub kind: ApiErrorKind,
55 pub message: Option<String>,
57 pub error_type: Option<String>,
59 pub code: Option<String>,
61 pub param: Option<String>,
63 pub request_id: Option<String>,
65 pub body: Option<Value>,
67 pub retry_after: Option<std::time::Duration>,
69}
70
71impl ApiError {
72 pub(crate) fn from_response(status: u16, request_id: Option<String>, text: &str) -> Self {
73 let body: Option<Value> = serde_json::from_str(text).ok();
74 let field = |name: &str| {
77 body.as_ref()
78 .and_then(|b| b["error"][name].as_str())
79 .map(str::to_string)
80 };
81 let top_message = body
82 .as_ref()
83 .and_then(|b| b["message"].as_str())
84 .map(str::to_string);
85 let message = field("message")
86 .or(top_message)
87 .or_else(|| {
88 let text = text.trim();
89 (!text.is_empty()).then(|| text.to_string())
90 })
91 .map(|m| truncate(&m));
92 Self {
93 status,
94 kind: ApiErrorKind::from_status(status),
95 message,
96 error_type: field("type").or_else(|| field("status")),
97 code: field("code"),
98 param: field("param"),
99 request_id,
100 body,
101 retry_after: None,
102 }
103 }
104
105 pub(crate) fn from_stream_error(error: &Value) -> Self {
108 let error_type = error["type"].as_str().or_else(|| error["status"].as_str());
109 let status = error["code"]
110 .as_u64()
111 .and_then(|c| u16::try_from(c).ok())
112 .filter(|c| (400..600).contains(c))
113 .unwrap_or(match error_type {
114 Some("overloaded_error") => 529,
115 Some("rate_limit_error" | "RESOURCE_EXHAUSTED") => 429,
116 Some("invalid_request_error" | "INVALID_ARGUMENT") => 400,
117 Some("authentication_error" | "UNAUTHENTICATED") => 401,
118 Some("permission_error" | "PERMISSION_DENIED") => 403,
119 Some("not_found_error" | "NOT_FOUND") => 404,
120 _ => 500,
121 });
122 Self {
123 status,
124 kind: ApiErrorKind::from_status(status),
125 message: error["message"].as_str().map(truncate),
126 error_type: error_type.map(str::to_string),
127 code: error["code"].as_str().map(str::to_string),
128 param: error["param"].as_str().map(str::to_string),
129 request_id: None,
130 body: Some(error.clone()),
131 retry_after: None,
132 }
133 }
134}
135
136impl std::fmt::Display for ApiError {
139 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140 match &self.message {
141 Some(message) => write!(f, "{} {}", self.status, message),
142 None => write!(f, "{} status code (no body)", self.status),
143 }
144 }
145}
146
147impl std::error::Error for ApiError {}
148
149#[derive(Debug, Error)]
151pub enum LlmError {
152 #[error("{0}")]
154 Api(Box<ApiError>),
155 #[error("Connection error.")]
157 Connection(#[source] reqwest::Error),
158 #[error("Request timed out.")]
160 Timeout(#[source] reqwest::Error),
161 #[error("failed to deserialize api response: {source}")]
165 Deserialize {
166 #[source]
167 source: serde_json::Error,
168 content: String,
169 },
170 #[error("unexpected api response: {0}")]
172 InvalidResponse(String),
173 #[error("invalid args: {0}")]
175 InvalidArgument(String),
176}
177
178impl LlmError {
179 pub(crate) fn from_reqwest(error: reqwest::Error) -> Self {
180 if error.is_builder() {
181 return Self::InvalidArgument(format!(
183 "could not build request: {}",
184 error.without_url()
185 ));
186 }
187 let error = error.without_url();
189 if error.is_timeout() {
190 Self::Timeout(error)
191 } else {
192 Self::Connection(error)
193 }
194 }
195
196 pub fn status(&self) -> Option<u16> {
198 match self {
199 Self::Api(e) => Some(e.status),
200 _ => None,
201 }
202 }
203
204 pub fn request_id(&self) -> Option<&str> {
206 match self {
207 Self::Api(e) => e.request_id.as_deref(),
208 _ => None,
209 }
210 }
211
212 pub fn is_retryable(&self) -> bool {
214 match self {
215 Self::Api(e) => matches!(e.status, 408 | 409 | 429) || e.status >= 500,
216 Self::Connection(_) | Self::Timeout(_) => true,
217 _ => false,
218 }
219 }
220}
221
222pub(crate) fn truncate(text: &str) -> String {
223 text.chars().take(MAX_BODY_CHARS).collect()
224}