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 /// Caller cancelled the provider attempt.
13 #[error("llm request cancelled")]
14 Cancelled,
15
16 /// Caller deadline elapsed before the provider attempt settled.
17 #[error("llm request deadline exceeded")]
18 DeadlineExceeded,
19 /// Circuit breaker is open — we fail fast without hitting the provider.
20 #[error("llm circuit breaker is open; failing fast")]
21 CircuitOpen,
22
23 /// Transport-level failure (connection, TLS, timeout).
24 #[error("llm transport error: {0}")]
25 Transport(#[from] reqwest::Error),
26
27 /// Provider returned a non-2xx status.
28 #[error("llm api error: status {status}, body: {body}")]
29 Api {
30 /// HTTP status returned.
31 status: u16,
32 /// Response body, truncated for logs.
33 body: String,
34 },
35
36 /// Response body could not be decoded into the expected shape.
37 #[error("llm response decode error: {0}")]
38 Decode(#[from] serde_json::Error),
39
40 /// A streaming response violated the SSE completion contract.
41 #[error("llm stream protocol error: {0}")]
42 StreamProtocol(String),
43
44 /// A streaming response contained bytes that were not valid UTF-8.
45 #[error("llm stream utf-8 error: {0}")]
46 InvalidUtf8(#[from] std::string::FromUtf8Error),
47}
48
49impl LlmError {
50 /// Whether retrying this error class could plausibly succeed.
51 ///
52 /// A 4xx (except 408/429) is the caller's fault and will fail identically on
53 /// retry; transport errors and 5xx/408/429 are worth another attempt.
54 pub fn is_retryable(&self) -> bool {
55 match self {
56 LlmError::Transport(_) | LlmError::StreamProtocol(_) | LlmError::InvalidUtf8(_) => true,
57 LlmError::Api { status, .. } => {
58 *status == 408 || *status == 429 || (500..600).contains(status)
59 }
60 // Circuit-open and decode failures are not improved by retry.
61 LlmError::CircuitOpen
62 | LlmError::Decode(_)
63 | LlmError::Cancelled
64 | LlmError::DeadlineExceeded => false,
65 }
66 }
67}
68
69/// Result alias for this crate's error.
70pub type Result<T> = std::result::Result<T, LlmError>;