Skip to main content

aither_core/llm/failure/
mod.rs

1//! Classification of language-model failures.
2//!
3//! A provider error says what went wrong in provider terms. [`Classify`] lets a
4//! caller decide what to do about it without parsing a message: wait for the
5//! network, retry after the upstream's requested delay, give up on a quota, or
6//! surface a permanent fault.
7
8use alloc::boxed::Box;
9use core::{convert::Infallible, time::Duration};
10
11#[cfg(feature = "zenwave")]
12pub mod zenwave;
13
14/// What kind of failure a provider error is.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum FailureKind {
17    /// No response came back from the network: DNS failure, refused or reset
18    /// connection, TLS failure, stream EOF or reset mid-response, request
19    /// timeout.
20    Network,
21    /// The upstream answered with a temporary fault: 5xx, 529/overloaded, 408,
22    /// or a 429 that is not a quota exhaustion.
23    Transient,
24    /// A quota or usage limit is exhausted; retrying will not help until it
25    /// resets.
26    Quota,
27    /// Everything else: other 4xx, authentication, malformed requests, parse
28    /// errors and local-model errors.
29    Permanent,
30}
31
32/// A classified failure and, when the upstream asked for one, its retry delay.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub struct Failure {
35    /// What kind of failure this is.
36    pub kind: FailureKind,
37    /// The delay the upstream requested before the next attempt, from its
38    /// `Retry-After` header or an equivalent field.
39    pub retry_after: Option<Duration>,
40}
41
42impl Failure {
43    /// A failure of `kind` with no requested retry delay.
44    #[must_use]
45    pub const fn new(kind: FailureKind) -> Self {
46        Self {
47            kind,
48            retry_after: None,
49        }
50    }
51
52    /// A [`FailureKind::Network`] failure.
53    #[must_use]
54    pub const fn network() -> Self {
55        Self::new(FailureKind::Network)
56    }
57
58    /// A [`FailureKind::Transient`] failure.
59    #[must_use]
60    pub const fn transient() -> Self {
61        Self::new(FailureKind::Transient)
62    }
63
64    /// A [`FailureKind::Quota`] failure.
65    #[must_use]
66    pub const fn quota() -> Self {
67        Self::new(FailureKind::Quota)
68    }
69
70    /// A [`FailureKind::Permanent`] failure.
71    #[must_use]
72    pub const fn permanent() -> Self {
73        Self::new(FailureKind::Permanent)
74    }
75
76    /// Sets the retry delay the upstream requested.
77    #[must_use]
78    pub const fn with_retry_after(mut self, retry_after: Option<Duration>) -> Self {
79        self.retry_after = retry_after;
80        self
81    }
82
83    /// Whether the same request may succeed if sent again later
84    /// ([`FailureKind::Network`] or [`FailureKind::Transient`]).
85    #[must_use]
86    pub const fn is_retryable(&self) -> bool {
87        matches!(self.kind, FailureKind::Network | FailureKind::Transient)
88    }
89}
90
91/// Errors that can say what kind of failure they are.
92///
93/// Implementations read the structured data behind the error — HTTP status,
94/// the error type in the response body, the transport error's kind — and never
95/// match on message text when a typed source exists.
96pub trait Classify {
97    /// Classifies this error.
98    fn failure(&self) -> Failure;
99}
100
101impl<T: Classify + ?Sized> Classify for &T {
102    fn failure(&self) -> Failure {
103        T::failure(self)
104    }
105}
106
107impl<T: Classify + ?Sized> Classify for Box<T> {
108    fn failure(&self) -> Failure {
109        T::failure(self)
110    }
111}
112
113impl Classify for Infallible {
114    fn failure(&self) -> Failure {
115        // `Infallible` has no values, so this can never be called.
116        Failure::permanent()
117    }
118}
119
120impl<E: Classify> Classify for super::GenerateError<E> {
121    fn failure(&self) -> Failure {
122        match self {
123            Self::Provider(error) => error.failure(),
124            Self::Parse { .. } => Failure::permanent(),
125        }
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn retryable_kinds() {
135        assert!(Failure::network().is_retryable());
136        assert!(Failure::transient().is_retryable());
137        assert!(!Failure::quota().is_retryable());
138        assert!(!Failure::permanent().is_retryable());
139    }
140
141    #[test]
142    fn retry_after_is_carried() {
143        let failure = Failure::transient().with_retry_after(Some(Duration::from_secs(3)));
144        assert_eq!(failure.retry_after, Some(Duration::from_secs(3)));
145    }
146}