af_llm/error.rs
1//! Error type for the LLM client.
2
3use thiserror::Error;
4
5/// Everything that can go wrong on the way to a completion.
6///
7/// Mirrors the failure modes of the Python `llm_completion` wrapper, but where
8/// that returned `None` and swallowed the cause, we keep a typed error so the
9/// caller decides how to degrade.
10#[derive(Debug, Error)]
11pub enum LlmError {
12 /// Circuit breaker is open — we fail fast without hitting the provider.
13 #[error("llm circuit breaker is open; failing fast")]
14 CircuitOpen,
15
16 /// Transport-level failure (connection, TLS, timeout).
17 #[error("llm transport error: {0}")]
18 Transport(#[from] reqwest::Error),
19
20 /// Provider returned a non-2xx status.
21 #[error("llm api error: status {status}, body: {body}")]
22 Api { status: u16, body: String },
23
24 /// Response body could not be decoded into the expected shape.
25 #[error("llm response decode error: {0}")]
26 Decode(#[from] serde_json::Error),
27
28 /// A streaming response violated the SSE completion contract.
29 #[error("llm stream protocol error: {0}")]
30 StreamProtocol(String),
31
32 /// A streaming response contained bytes that were not valid UTF-8.
33 #[error("llm stream utf-8 error: {0}")]
34 InvalidUtf8(#[from] std::string::FromUtf8Error),
35
36 /// All retry attempts were exhausted. Carries the last error seen.
37 #[error("llm failed after {attempts} attempts; last error: {source}")]
38 AllRetriesFailed {
39 attempts: u32,
40 #[source]
41 source: Box<LlmError>,
42 },
43}
44
45impl LlmError {
46 /// Whether retrying this error class could plausibly succeed.
47 ///
48 /// A 4xx (except 408/429) is the caller's fault and will fail identically on
49 /// retry; transport errors and 5xx/408/429 are worth another attempt.
50 pub fn is_retryable(&self) -> bool {
51 match self {
52 LlmError::Transport(_) | LlmError::StreamProtocol(_) | LlmError::InvalidUtf8(_) => true,
53 LlmError::Api { status, .. } => {
54 *status == 408 || *status == 429 || (500..600).contains(status)
55 }
56 // Circuit-open / decode / already-exhausted are not improved by retry.
57 LlmError::CircuitOpen | LlmError::Decode(_) | LlmError::AllRetriesFailed { .. } => {
58 false
59 }
60 }
61 }
62}
63
64pub type Result<T> = std::result::Result<T, LlmError>;