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}