Skip to main content

drep/llm/
error.rs

1//! Backend-neutral failures at the LLM boundary.
2
3use thiserror::Error;
4
5/// What can go wrong at the LLM boundary.
6///
7/// Every variant means "the file went unanalyzed", but the variants preserve
8/// the cause so retry, failover, demotion, and reporting policy never has to
9/// infer semantics from a human-readable message.
10///
11/// `Clone` because the provider chain records the reason a provider went down
12/// and hands a copy to every later file that skips it.
13#[derive(Debug, Clone, Error)]
14pub enum LlmError {
15    /// Transport failure after the backend exhausted its retries.
16    ///
17    /// `status` is the HTTP code when one exists; process failures use `None`.
18    #[error("LLM transport failed{}: {message}", status.map(|c| format!(" (HTTP {c})")).unwrap_or_default())]
19    Transport {
20        status: Option<u16>,
21        message: String,
22    },
23
24    /// A response arrived but no JSON could be extracted.
25    #[error("LLM response was unparseable: {0}")]
26    Unparseable(String),
27
28    /// The model stopped before producing JSON, and the server said why.
29    ///
30    /// It is about the request rather than the provider, so it neither fails
31    /// over nor demotes the provider.
32    #[error("{message}")]
33    ModelStopped { finish: String, message: String },
34
35    /// Configuration is incomplete.
36    #[error("LLM not configured: {0}")]
37    NotConfigured(String),
38
39    /// A non-HTTP backend classified a failure from structured process state.
40    #[error("LLM backend {kind}: {message}")]
41    Backend {
42        kind: BackendErrorKind,
43        message: String,
44    },
45}
46
47/// Routing class for a structured non-HTTP backend failure.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum BackendErrorKind {
50    Contract,
51    Authentication,
52    UsageLimit,
53    Request,
54    UnknownExit,
55}
56
57impl std::fmt::Display for BackendErrorKind {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        f.write_str(match self {
60            Self::Contract => "contract failure",
61            Self::Authentication => "authentication failure",
62            Self::UsageLimit => "usage limit",
63            Self::Request => "request rejection",
64            Self::UnknownExit => "failure",
65        })
66    }
67}
68
69impl BackendErrorKind {
70    /// Stable machine tag used by JSON failure reports.
71    pub fn as_str(self) -> &'static str {
72        match self {
73            Self::Contract => "contract",
74            Self::Authentication => "authentication",
75            Self::UsageLimit => "usage_limit",
76            Self::Request => "request",
77            Self::UnknownExit => "unknown_exit",
78        }
79    }
80}
81
82impl LlmError {
83    /// The HTTP status, when the failure carried one.
84    pub fn status(&self) -> Option<u16> {
85        match self {
86            Self::Transport { status, .. } => *status,
87            Self::Unparseable(_)
88            | Self::ModelStopped { .. }
89            | Self::NotConfigured(_)
90            | Self::Backend { .. } => None,
91        }
92    }
93}