Skip to main content

crabllm_core/
error.rs

1use serde::{Deserialize, Serialize};
2use std::{fmt, time::Duration};
3
4/// Shared error type for the crabllm workspace.
5#[derive(Debug)]
6pub enum Error {
7    /// TOML config parse error or missing env var.
8    Config(String),
9    /// Upstream provider returned an error status.
10    Provider {
11        status: u16,
12        body: String,
13        retry_after: Option<Duration>,
14    },
15    /// JSON serialization/deserialization error.
16    Json(serde_json::Error),
17    /// Catch-all for internal errors.
18    Internal(String),
19    /// Request to upstream provider timed out.
20    Timeout,
21}
22
23impl fmt::Display for Error {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        match self {
26            Error::Config(msg) => write!(f, "config error: {msg}"),
27            Error::Provider { status, body, .. } => {
28                write!(f, "provider error (HTTP {status}): {body}")
29            }
30            Error::Json(e) => write!(f, "json error: {e}"),
31            Error::Internal(msg) => write!(f, "internal error: {msg}"),
32            Error::Timeout => write!(f, "request timed out"),
33        }
34    }
35}
36
37impl Error {
38    /// Whether this error is transient and the request should be retried.
39    /// Transient: 429 (rate limit), 500, 502, 503, 504 (server errors),
40    /// and connection/internal errors.
41    pub fn is_transient(&self) -> bool {
42        match self {
43            Error::Provider { status, .. } => matches!(status, 429 | 500 | 502 | 503 | 504),
44            Error::Internal(_) | Error::Timeout => true,
45            _ => false,
46        }
47    }
48
49    /// Extract the retry-after duration from a `Provider` error, if present.
50    pub fn retry_after(&self) -> Option<Duration> {
51        match self {
52            Error::Provider { retry_after, .. } => *retry_after,
53            _ => None,
54        }
55    }
56
57    /// Build an "operation not supported" error for a provider trait method
58    /// that has no implementation. Used by `Provider` trait default impls.
59    /// Distinct from per-provider rejection messages so log lines can be
60    /// disambiguated by grep.
61    pub fn not_implemented(method: &str) -> Self {
62        Error::Internal(format!("provider method '{method}' not implemented"))
63    }
64}
65
66impl std::error::Error for Error {
67    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
68        match self {
69            Error::Json(e) => Some(e),
70            _ => None,
71        }
72    }
73}
74
75#[cfg(feature = "gateway")]
76impl From<toml::de::Error> for Error {
77    fn from(e: toml::de::Error) -> Self {
78        Error::Config(e.to_string())
79    }
80}
81
82impl From<serde_json::Error> for Error {
83    fn from(e: serde_json::Error) -> Self {
84        Error::Json(e)
85    }
86}
87
88/// OpenAI-compatible error response returned to clients.
89#[derive(Debug, Clone, Serialize, Deserialize)]
90#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
91pub struct ApiError {
92    pub error: ApiErrorBody,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
96#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
97pub struct ApiErrorBody {
98    pub message: String,
99    #[serde(rename = "type")]
100    pub kind: String,
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub param: Option<String>,
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub code: Option<String>,
105}
106
107impl ApiError {
108    pub fn new(message: impl Into<String>, kind: impl Into<String>) -> Self {
109        ApiError {
110            error: ApiErrorBody {
111                message: message.into(),
112                kind: kind.into(),
113                param: None,
114                code: None,
115            },
116        }
117    }
118}