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}
68
69impl ApiError {
70 pub(crate) fn from_response(status: u16, request_id: Option<String>, text: &str) -> Self {
71 let body: Option<Value> = serde_json::from_str(text).ok();
72 let field = |name: &str| {
75 body.as_ref()
76 .and_then(|b| b["error"][name].as_str())
77 .map(str::to_string)
78 };
79 let top_message = body
80 .as_ref()
81 .and_then(|b| b["message"].as_str())
82 .map(str::to_string);
83 let message = field("message")
84 .or(top_message)
85 .or_else(|| {
86 let text = text.trim();
87 (!text.is_empty()).then(|| text.to_string())
88 })
89 .map(|m| truncate(&m));
90 Self {
91 status,
92 kind: ApiErrorKind::from_status(status),
93 message,
94 error_type: field("type").or_else(|| field("status")),
95 code: field("code"),
96 param: field("param"),
97 request_id,
98 body,
99 }
100 }
101}
102
103impl std::fmt::Display for ApiError {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 match &self.message {
108 Some(message) => write!(f, "{} {}", self.status, message),
109 None => write!(f, "{} status code (no body)", self.status),
110 }
111 }
112}
113
114impl std::error::Error for ApiError {}
115
116#[derive(Debug, Error)]
118pub enum LlmError {
119 #[error("{0}")]
121 Api(Box<ApiError>),
122 #[error("Connection error.")]
124 Connection(#[source] reqwest::Error),
125 #[error("Request timed out.")]
127 Timeout(#[source] reqwest::Error),
128 #[error("failed to deserialize api response: {source}")]
132 Deserialize {
133 #[source]
134 source: serde_json::Error,
135 content: String,
136 },
137 #[error("unexpected api response: {0}")]
139 InvalidResponse(String),
140 #[error("invalid args: {0}")]
142 InvalidArgument(String),
143}
144
145impl LlmError {
146 pub(crate) fn from_reqwest(error: reqwest::Error) -> Self {
147 if error.is_builder() {
148 return Self::InvalidArgument(format!(
150 "could not build request: {}",
151 error.without_url()
152 ));
153 }
154 let error = error.without_url();
156 if error.is_timeout() {
157 Self::Timeout(error)
158 } else {
159 Self::Connection(error)
160 }
161 }
162
163 pub fn status(&self) -> Option<u16> {
165 match self {
166 Self::Api(e) => Some(e.status),
167 _ => None,
168 }
169 }
170
171 pub fn request_id(&self) -> Option<&str> {
173 match self {
174 Self::Api(e) => e.request_id.as_deref(),
175 _ => None,
176 }
177 }
178
179 pub fn is_retryable(&self) -> bool {
181 match self {
182 Self::Api(e) => matches!(e.status, 408 | 409 | 429) || e.status >= 500,
183 Self::Connection(_) | Self::Timeout(_) => true,
184 _ => false,
185 }
186 }
187}
188
189pub(crate) fn truncate(text: &str) -> String {
190 text.chars().take(MAX_BODY_CHARS).collect()
191}