Skip to main content

llmkit_core/
error.rs

1//! Unified error type. Every fallible llmkit operation returns [`LlmResult<T>`].
2
3use std::fmt;
4
5/// `Result` alias used throughout llmkit.
6pub type LlmResult<T> = Result<T, LlmError>;
7
8/// The single error type returned by every llmkit operation.
9#[derive(Debug, thiserror::Error)]
10#[non_exhaustive]
11pub enum LlmError {
12    /// Request rejected before sending (missing model, empty messages, …).
13    #[error("invalid request: {0}")]
14    InvalidRequest(String),
15
16    /// Authentication failed (missing or invalid API key).
17    #[error("authentication failed: {0}")]
18    Auth(String),
19
20    /// Provider returned HTTP 429.
21    #[error("rate limited{}", .retry_after.map(|d| format!(" (retry after {}s)", d.as_secs())).unwrap_or_default())]
22    RateLimited {
23        /// Suggested delay before retrying, if provided.
24        retry_after: Option<std::time::Duration>,
25        /// Provider message.
26        message: String,
27    },
28
29    /// Provider returned a non-success status with a body.
30    #[error("provider error (status {status}): {message}")]
31    Provider {
32        /// HTTP status code.
33        status: u16,
34        /// Extracted message or body.
35        message: String,
36    },
37
38    /// Network / transport failure.
39    #[error("transport error: {0}")]
40    Transport(String),
41
42    /// Request exceeded the configured timeout.
43    #[error("request timed out")]
44    Timeout,
45
46    /// Failed to (de)serialize a body.
47    #[error("serialization error: {0}")]
48    Serialization(String),
49
50    /// Error while parsing or driving a streaming response.
51    #[error("stream error: {0}")]
52    Stream(String),
53
54    /// A configured budget (e.g. session cost cap) was exceeded.
55    #[error("budget exceeded: {0}")]
56    BudgetExceeded(String),
57
58    /// A tool invocation failed during execution.
59    #[error("tool `{tool}` failed: {message}")]
60    Tool {
61        /// Tool name.
62        tool: String,
63        /// Handler error message.
64        message: String,
65    },
66
67    /// Capability not supported by this provider.
68    #[error("unsupported: {0}")]
69    Unsupported(String),
70
71    /// Every provider in a fallback chain failed.
72    #[error("all providers failed ({} attempted)", .0.len())]
73    AllProvidersFailed(Vec<LlmError>),
74
75    /// Catch-all.
76    #[error("{0}")]
77    Other(String),
78}
79
80impl LlmError {
81    /// Whether retrying might succeed. Used by the retry layer.
82    pub fn is_retryable(&self) -> bool {
83        match self {
84            LlmError::Timeout
85            | LlmError::Transport(_)
86            | LlmError::Stream(_)
87            | LlmError::RateLimited { .. } => true,
88            LlmError::Provider { status, .. } => *status >= 500 || *status == 408,
89            _ => false,
90        }
91    }
92
93    /// Construct an [`LlmError::InvalidRequest`].
94    pub fn invalid(msg: impl fmt::Display) -> Self {
95        LlmError::InvalidRequest(msg.to_string())
96    }
97
98    /// Construct an [`LlmError::Serialization`].
99    pub fn serde(msg: impl fmt::Display) -> Self {
100        LlmError::Serialization(msg.to_string())
101    }
102}
103
104impl From<serde_json::Error> for LlmError {
105    fn from(e: serde_json::Error) -> Self {
106        LlmError::Serialization(e.to_string())
107    }
108}