use std::error::Error;
use std::fmt;
#[derive(Debug, Clone)]
pub struct LlmCallError {
pub user_message: String,
pub http_status: Option<u16>,
pub retryable: bool,
}
impl fmt::Display for LlmCallError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.user_message)
}
}
impl Error for LlmCallError {}
pub fn http_status_retryable_for_backoff(status: u16) -> bool {
matches!(status, 408 | 429 | 500..=599)
}
impl LlmCallError {
pub fn from_http_api(status: u16, user_message: String) -> Self {
Self {
retryable: http_status_retryable_for_backoff(status),
http_status: Some(status),
user_message,
}
}
pub fn boxed_from_reqwest(e: reqwest::Error) -> Box<dyn Error + Send + Sync> {
let retryable = e.is_timeout() || e.is_connect();
let msg = crate::cm_llm::http_client::format_reqwest_transport_err(&e);
Box::new(Self {
user_message: msg,
http_status: None,
retryable,
})
}
}
pub fn llm_call_error_retryable(e: &(dyn Error + Send + Sync + 'static)) -> bool {
e.downcast_ref::<LlmCallError>()
.is_some_and(|x| x.retryable)
}
pub fn llm_call_error_http_status(e: &(dyn Error + Send + Sync + 'static)) -> Option<u16> {
e.downcast_ref::<LlmCallError>().and_then(|x| x.http_status)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn http_status_retryable_matches_table() {
assert!(!http_status_retryable_for_backoff(400));
assert!(!http_status_retryable_for_backoff(401));
assert!(!http_status_retryable_for_backoff(403));
assert!(!http_status_retryable_for_backoff(404));
assert!(http_status_retryable_for_backoff(408));
assert!(http_status_retryable_for_backoff(429));
assert!(http_status_retryable_for_backoff(500));
assert!(http_status_retryable_for_backoff(503));
assert!(http_status_retryable_for_backoff(599));
assert!(!http_status_retryable_for_backoff(600));
}
#[test]
fn display_is_user_message() {
let e = LlmCallError::from_http_api(401, "模型接口返回错误(HTTP 401):x".to_string());
assert_eq!(e.to_string(), "模型接口返回错误(HTTP 401):x");
assert!(!e.retryable);
}
}