Skip to main content

ferrin_spec/error/
mod.rs

1//! Errors that provider adapters can return.
2//!
3//! [`ProviderError`] is the single error type of every adapter method. Each
4//! variant wraps a concrete error struct so that callers can match on the
5//! failure class and read structured fields. Large payloads are boxed to keep
6//! the enum small enough to pass through `Result` cheaply.
7
8mod api_call;
9mod config;
10mod data;
11mod model;
12
13use http::StatusCode;
14
15pub use api_call::ApiCallError;
16pub use api_call::default_retryable;
17pub use config::InvalidArgumentError;
18pub use config::InvalidPromptError;
19pub use config::LoadApiKeyError;
20pub use config::LoadSettingError;
21pub use data::EmptyResponseBodyError;
22pub use data::InvalidResponseDataError;
23pub use data::JsonParseError;
24pub use data::NoContentGeneratedError;
25pub use data::TypeValidationContext;
26pub use data::TypeValidationError;
27pub use model::ModelKind;
28pub use model::NoSuchModelError;
29pub use model::NoSuchProviderReferenceError;
30pub use model::TooManyEmbeddingValuesForCallError;
31pub use model::UnsupportedFunctionalityError;
32
33/// Boxed dynamic error used for causes and for [`ProviderError::Other`].
34pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
35
36/// Error returned by provider adapters.
37#[derive(Debug, thiserror::Error)]
38#[non_exhaustive]
39pub enum ProviderError {
40    /// An HTTP call to the provider failed.
41    #[error(transparent)]
42    ApiCall(Box<ApiCallError>),
43    /// The provider returned an empty body where one was required.
44    #[error(transparent)]
45    EmptyResponseBody(#[from] EmptyResponseBodyError),
46    /// A call argument is invalid.
47    #[error(transparent)]
48    InvalidArgument(#[from] InvalidArgumentError),
49    /// The prompt cannot be converted for this provider.
50    #[error(transparent)]
51    InvalidPrompt(Box<InvalidPromptError>),
52    /// The response has an unexpected shape.
53    #[error(transparent)]
54    InvalidResponseData(Box<InvalidResponseDataError>),
55    /// Response text is not valid JSON.
56    #[error(transparent)]
57    JsonParse(Box<JsonParseError>),
58    /// The API key could not be loaded.
59    #[error(transparent)]
60    LoadApiKey(#[from] LoadApiKeyError),
61    /// A required setting could not be loaded.
62    #[error(transparent)]
63    LoadSetting(#[from] LoadSettingError),
64    /// The provider produced no content.
65    #[error(transparent)]
66    NoContentGenerated(#[from] NoContentGeneratedError),
67    /// The requested model does not exist.
68    #[error(transparent)]
69    NoSuchModel(Box<NoSuchModelError>),
70    /// A provider reference has no entry for this provider.
71    #[error(transparent)]
72    NoSuchProviderReference(Box<NoSuchProviderReferenceError>),
73    /// Too many values were passed to a single embedding call.
74    #[error(transparent)]
75    TooManyEmbeddingValues(Box<TooManyEmbeddingValuesForCallError>),
76    /// A value failed schema or type validation.
77    #[error(transparent)]
78    TypeValidation(#[from] TypeValidationError),
79    /// The provider or model does not support the requested functionality.
80    #[error(transparent)]
81    UnsupportedFunctionality(#[from] UnsupportedFunctionalityError),
82    /// The call was cancelled through its cancellation token.
83    #[error("operation cancelled")]
84    Cancelled,
85    /// Any other error.
86    #[error(transparent)]
87    Other(#[from] BoxError),
88}
89
90impl ProviderError {
91    /// Wraps an arbitrary error in [`ProviderError::Other`].
92    #[must_use]
93    pub fn other(error: impl std::error::Error + Send + Sync + 'static) -> Self {
94        Self::Other(Box::new(error))
95    }
96
97    /// Creates an [`ProviderError::Other`] from a message.
98    #[must_use]
99    pub fn message(message: impl Into<String>) -> Self {
100        Self::Other(message.into().into())
101    }
102
103    /// Creates an [`ProviderError::UnsupportedFunctionality`] error.
104    #[must_use]
105    pub fn unsupported(functionality: impl Into<String>) -> Self {
106        Self::UnsupportedFunctionality(UnsupportedFunctionalityError::new(functionality))
107    }
108
109    /// Returns `true` when retrying the call may succeed.
110    ///
111    /// Only [`ProviderError::ApiCall`] carries retry information; every other
112    /// variant is not retryable.
113    #[must_use]
114    pub fn is_retryable(&self) -> bool {
115        match self {
116            Self::ApiCall(error) => error.is_retryable,
117            _ => false,
118        }
119    }
120
121    /// Returns the HTTP status code of an API call error.
122    #[must_use]
123    pub fn status_code(&self) -> Option<StatusCode> {
124        match self {
125            Self::ApiCall(error) => error.status_code,
126            _ => None,
127        }
128    }
129
130    /// Returns the API call error, if this is [`ProviderError::ApiCall`].
131    #[must_use]
132    pub fn as_api_call(&self) -> Option<&ApiCallError> {
133        match self {
134            Self::ApiCall(error) => Some(error),
135            _ => None,
136        }
137    }
138
139    /// Returns a stable, low-cardinality name of the variant for telemetry.
140    #[must_use]
141    pub fn kind_name(&self) -> &'static str {
142        match self {
143            Self::ApiCall(_) => "api_call",
144            Self::EmptyResponseBody(_) => "empty_response_body",
145            Self::InvalidArgument(_) => "invalid_argument",
146            Self::InvalidPrompt(_) => "invalid_prompt",
147            Self::InvalidResponseData(_) => "invalid_response_data",
148            Self::JsonParse(_) => "json_parse",
149            Self::LoadApiKey(_) => "load_api_key",
150            Self::LoadSetting(_) => "load_setting",
151            Self::NoContentGenerated(_) => "no_content_generated",
152            Self::NoSuchModel(_) => "no_such_model",
153            Self::NoSuchProviderReference(_) => "no_such_provider_reference",
154            Self::TooManyEmbeddingValues(_) => "too_many_embedding_values",
155            Self::TypeValidation(_) => "type_validation",
156            Self::UnsupportedFunctionality(_) => "unsupported_functionality",
157            Self::Cancelled => "cancelled",
158            Self::Other(_) => "other",
159        }
160    }
161}
162
163macro_rules! boxed_from {
164    ($($variant:ident($error:ty)),* $(,)?) => {
165        $(
166            impl From<$error> for ProviderError {
167                fn from(error: $error) -> Self {
168                    Self::$variant(Box::new(error))
169                }
170            }
171
172            impl From<Box<$error>> for ProviderError {
173                fn from(error: Box<$error>) -> Self {
174                    Self::$variant(error)
175                }
176            }
177        )*
178    };
179}
180
181boxed_from! {
182    ApiCall(ApiCallError),
183    InvalidPrompt(InvalidPromptError),
184    InvalidResponseData(InvalidResponseDataError),
185    JsonParse(JsonParseError),
186    NoSuchModel(NoSuchModelError),
187    NoSuchProviderReference(NoSuchProviderReferenceError),
188    TooManyEmbeddingValues(TooManyEmbeddingValuesForCallError),
189}
190
191/// Truncates `text` to at most `max_bytes` bytes on a char boundary, appending
192/// a marker when truncation happened.
193pub(crate) fn truncate_for_display(text: &str, max_bytes: usize) -> std::borrow::Cow<'_, str> {
194    if text.len() <= max_bytes {
195        return std::borrow::Cow::Borrowed(text);
196    }
197    let mut end = max_bytes;
198    while !text.is_char_boundary(end) {
199        end -= 1;
200    }
201    std::borrow::Cow::Owned(format!("{}... [truncated]", &text[..end]))
202}