Skip to main content

just_llm_client/
error.rs

1//! Error taxonomy for the LLM client layer, partitioned by **failure nature**.
2//!
3//! Three distinct failure natures get three distinct types, so a caller can never conflate them
4//! at the type level:
5//!
6//! - [`BackendConstructError`] — *constructing* a backend failed (`LlmBackend::new`,
7//!   `BackendFactory::create`). Precondition/setup failures only; never a chat call.
8//! - [`CapabilityError`] — a backend *statically* does not offer a capability
9//!   (`CapabilityNegotiation`). Decided without IO; never a provider call.
10//! - [`BackendError`] — *operating* an already-constructed backend failed (chat completion,
11//!   streaming, prepare/send/parse, rendering, model catalog, balance). Runtime execution only.
12
13use std::{error::Error as StdError, fmt};
14
15use thiserror::Error;
16
17/// Boxed provider error source carried by [`BackendError::Provider`] and
18/// [`BackendConstructError::Provider`].
19///
20/// Kept as `BoxError` rather than a concrete `ProviderError` so that custom backends wrapping an
21/// arbitrary provider SDK (not necessarily one built on `just-common`) can carry their own error
22/// type. Callers that need the structured `ProviderError` produced by the built-in backends
23/// downcast it (as the tests do).
24pub type BoxError = Box<dyn StdError + Send + Sync>;
25
26/// Capability names used in client-level unsupported or unavailable errors.
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28#[non_exhaustive]
29pub enum Capability {
30    /// Model catalog listing.
31    ModelCatalog,
32    /// Balance or quota inspection.
33    Balance,
34}
35
36impl fmt::Display for Capability {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        let label = match self {
39            Self::ModelCatalog => "model catalog",
40            Self::Balance => "balance",
41        };
42
43        f.write_str(label)
44    }
45}
46
47/// Constructing a backend failed: a precondition or setup failure.
48///
49/// Produced only by [`LlmBackend::new`](crate::LlmBackend::new) and
50/// [`BackendFactory::create`](crate::BackendFactory::create). Distinct from [`BackendError`]
51/// (operating a backend) and [`CapabilityError`] (static capability gating): construction never
52/// performs a chat call, so its failure model is isolated.
53#[derive(Debug, Error)]
54#[non_exhaustive]
55pub enum BackendConstructError {
56    /// No constructor is registered for the requested family (factory dispatch).
57    #[error("no backend registered for family '{family}'")]
58    UnknownFamily {
59        /// The family string that had no registered constructor.
60        family: String,
61    },
62
63    /// The provider client could not be built.
64    ///
65    /// Carries the backend family for attribution and the provider build failure as a boxed
66    /// source, so callers can downcast to inspect `TransportError::InvalidConfig` /
67    /// `TransportError::BuildClient` for the built-in backends, while custom backends may box any
68    /// SDK-specific error.
69    #[error("failed to build {family} backend: {source}")]
70    Provider {
71        /// Backend family that failed to build.
72        family: &'static str,
73        /// Provider-specific source error.
74        #[source]
75        source: BoxError,
76    },
77}
78
79impl BackendConstructError {
80    /// Creates an unknown-family error (factory dispatch miss).
81    pub fn unknown_family(family: impl Into<String>) -> Self {
82        Self::UnknownFamily {
83            family: family.into(),
84        }
85    }
86
87    /// Wraps a provider build failure for the given backend family.
88    pub fn provider<E>(family: &'static str, source: E) -> Self
89    where
90        E: StdError + Send + Sync + 'static,
91    {
92        Self::Provider {
93            family,
94            source: Box::new(source),
95        }
96    }
97}
98
99/// Static capability gating failure, returned by
100/// [`CapabilityNegotiation`](crate::CapabilityNegotiation).
101///
102/// Decided without IO: it is a fact about whether a backend *offers* a capability, not whether a
103/// live call to it failed. The latter is [`BackendError`].
104#[derive(Debug, Error)]
105#[non_exhaustive]
106pub enum CapabilityError {
107    /// The backend never offers the requested capability.
108    #[error("{family} does not support {capability}")]
109    Unsupported {
110        /// Backend family.
111        family: &'static str,
112        /// Capability that was requested.
113        capability: Capability,
114    },
115
116    /// The backend is expected to offer the capability but has not implemented it yet.
117    #[error("{family} has not implemented {capability}")]
118    Unimplemented {
119        /// Backend family.
120        family: &'static str,
121        /// Capability that was expected.
122        capability: Capability,
123    },
124
125    /// The backend can offer the capability in principle, but not in the current state.
126    #[error("{family} cannot currently provide {capability}: {message}")]
127    Unavailable {
128        /// Backend family.
129        family: &'static str,
130        /// Capability that is temporarily unavailable.
131        capability: Capability,
132        /// Additional explanation from the backend adapter.
133        message: String,
134    },
135}
136
137impl CapabilityError {
138    /// Creates an unsupported-capability error for the given backend.
139    pub fn unsupported(family: &'static str, capability: Capability) -> Self {
140        Self::Unsupported { family, capability }
141    }
142
143    /// Creates an unimplemented-capability error for the given backend.
144    pub fn unimplemented(family: &'static str, capability: Capability) -> Self {
145        Self::Unimplemented { family, capability }
146    }
147
148    /// Creates an unavailable-capability error for the given backend.
149    pub fn unavailable(
150        family: &'static str,
151        capability: Capability,
152        message: impl Into<String>,
153    ) -> Self {
154        Self::Unavailable {
155            family,
156            capability,
157            message: message.into(),
158        }
159    }
160}
161
162/// Operating an already-constructed backend failed: a runtime execution failure.
163///
164/// Returned by chat completion, streaming, prepare/send/parse, rendering, model catalog, and
165/// balance calls — everything that drives a live backend. Construction failures are
166/// [`BackendConstructError`]; static capability gating is [`CapabilityError`].
167#[derive(Debug, Error)]
168#[non_exhaustive]
169pub enum BackendError {
170    /// The request was invalid before it reached the provider.
171    #[error("invalid request: {0}")]
172    InvalidRequest(String),
173
174    /// Failed to serialize a request payload or render provider-specific types.
175    #[error("serialization error: {source}")]
176    Serialization {
177        /// Underlying `serde_json` serialization failure.
178        #[source]
179        source: serde_json::Error,
180    },
181
182    /// The underlying provider SDK returned an error.
183    ///
184    /// Carries the backend family for attribution and the provider error as a boxed source (see
185    /// [`BoxError`] for why this is not a concrete `ProviderError`).
186    #[error("{family} backend error: {source}")]
187    Provider {
188        /// Backend family.
189        family: &'static str,
190        /// Provider-specific source error.
191        #[source]
192        source: BoxError,
193    },
194}
195
196impl BackendError {
197    /// Creates an invalid-request error with a stable, user-facing message.
198    pub fn invalid_request(message: impl Into<String>) -> Self {
199        Self::InvalidRequest(message.into())
200    }
201
202    /// Creates a serialization error wrapping a `serde_json` failure.
203    pub fn serialization(source: serde_json::Error) -> Self {
204        Self::Serialization { source }
205    }
206
207    /// Wraps a provider-specific source error for the given backend family.
208    pub fn provider<E>(family: &'static str, source: E) -> Self
209    where
210        E: StdError + Send + Sync + 'static,
211    {
212        Self::Provider {
213            family,
214            source: Box::new(source),
215        }
216    }
217}