Skip to main content

rai_sdk/
error.rs

1//! Error types shared by every provider.
2//!
3//! [`enum@Error`] is the single error type returned across the SDK, and [`Result`]
4//! is the matching alias. Errors carry the [`ProviderKind`] they originated
5//! from where that is meaningful, and expose classification helpers
6//! ([`Error::is_retryable`], [`Error::is_auth_error`], [`Error::kind_str`]) so
7//! callers can branch on categories instead of matching every variant.
8//!
9//! # Examples
10//!
11//! ```no_run
12//! use rai_sdk::{Error, ProviderKind};
13//!
14//! fn describe(error: &Error) -> String {
15//!     if error.is_auth_error() {
16//!         return "check your API key".to_string();
17//!     }
18//!     if error.is_retryable() {
19//!         return format!("transient {} failure", error.kind_str());
20//!     }
21//!     match error.provider() {
22//!         Some(ProviderKind::OpenAI) => "OpenAI rejected the request".to_string(),
23//!         Some(provider) => format!("{provider} rejected the request"),
24//!         None => error.to_string(),
25//!     }
26//! }
27//! ```
28
29use serde::{Deserialize, Serialize};
30use thiserror::Error;
31
32/// Identifies which AI provider was used.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "lowercase")]
35pub enum ProviderKind {
36    /// OpenAI's own API (`https://api.openai.com`).
37    OpenAI,
38    /// Anthropic's Messages API (`https://api.anthropic.com`).
39    Anthropic,
40    /// OpenRouter's aggregating API (`https://openrouter.ai`).
41    OpenRouter,
42    /// A self-hosted or third-party endpoint that speaks the OpenAI Chat
43    /// Completions wire format, such as Ollama, vLLM, or LM Studio.
44    ///
45    /// Unlike the other variants this one names no fixed service: the endpoint
46    /// is chosen per client with
47    /// [`ClientBuilder::openai_compatible_base_url`](crate::ClientBuilder::openai_compatible_base_url).
48    #[serde(rename = "openai-compatible")]
49    OpenAICompatible,
50}
51
52impl ProviderKind {
53    /// The Cargo feature that compiles support for this provider in.
54    ///
55    /// [`ProviderKind::OpenAICompatible`] rides the `openai` feature, because it
56    /// reuses that provider's request builder and stream parser, so this is not
57    /// simply the lowercased variant name.
58    pub fn feature_name(&self) -> &'static str {
59        match self {
60            ProviderKind::OpenAI | ProviderKind::OpenAICompatible => "openai",
61            ProviderKind::Anthropic => "anthropic",
62            ProviderKind::OpenRouter => "openrouter",
63        }
64    }
65}
66
67impl std::fmt::Display for ProviderKind {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        match self {
70            ProviderKind::OpenAI => write!(f, "openai"),
71            ProviderKind::Anthropic => write!(f, "anthropic"),
72            ProviderKind::OpenRouter => write!(f, "openrouter"),
73            ProviderKind::OpenAICompatible => write!(f, "openai-compatible"),
74        }
75    }
76}
77
78/// An optional part of the Chat Completions API that an endpoint may not
79/// implement.
80///
81/// OpenAI-compatible servers implement the same wire format as OpenAI but not
82/// always the same feature set: a small local model may have no tool-calling
83/// support, and a runtime may not constrain output to a JSON Schema. Requests
84/// that need something the endpoint cannot do fail with
85/// [`Error::CapabilityUnsupported`] naming the capability, so a caller can fall
86/// back instead of parsing an HTTP error body.
87///
88/// Declare what an endpoint supports with
89/// [`EndpointCapabilities`](crate::EndpointCapabilities).
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
91#[serde(rename_all = "snake_case")]
92pub enum Capability {
93    /// Advertising tools and receiving tool calls back.
94    ToolCalling,
95    /// Constraining the response with `response_format` — JSON mode or a JSON
96    /// Schema.
97    StructuredOutput,
98}
99
100impl Capability {
101    /// The snake_case name this capability serializes as.
102    pub fn as_str(&self) -> &'static str {
103        match self {
104            Capability::ToolCalling => "tool_calling",
105            Capability::StructuredOutput => "structured_output",
106        }
107    }
108}
109
110impl std::fmt::Display for Capability {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        match self {
113            Capability::ToolCalling => write!(f, "tool calling"),
114            Capability::StructuredOutput => write!(f, "structured output"),
115        }
116    }
117}
118
119/// Diagnostic info about a tool argument validation failure.
120///
121/// One issue is produced per JSON Schema violation found in the arguments a
122/// model supplied for a tool call. Issues are serialized back to the model as
123/// part of the tool error message so it can correct itself and retry.
124#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
125pub struct ToolArgumentIssue {
126    /// JSON pointer to the offending value in the arguments (`$` for the root).
127    pub path: String,
128    /// JSON pointer to the schema keyword that rejected the value.
129    pub schema_path: String,
130    /// Human-readable description of the violation.
131    pub message: String,
132}
133
134/// Errors that can occur when using the AI SDK.
135///
136/// Every fallible operation in this crate returns this type through
137/// [`Result`]. Rather than matching each variant, prefer the classification
138/// helpers when you only care about the category of failure.
139///
140/// # Examples
141///
142/// ```no_run
143/// use rai_sdk::{Error, ProviderKind};
144///
145/// let error = Error::RateLimit {
146///     provider: ProviderKind::OpenAI,
147///     message: "slow down".to_string(),
148/// };
149///
150/// assert!(error.is_rate_limit());
151/// assert!(error.is_retryable());
152/// assert_eq!(error.kind_str(), "rate_limit");
153/// assert_eq!(error.provider(), Some(ProviderKind::OpenAI));
154/// ```
155#[derive(Error, Debug)]
156pub enum Error {
157    /// Authentication failed (invalid API key, expired token, etc.)
158    #[error("authentication error for {provider}: {message}")]
159    Auth {
160        /// Provider that rejected the credentials.
161        provider: ProviderKind,
162        /// Message reported by the provider.
163        message: String,
164    },
165
166    /// The API request failed.
167    ///
168    /// Used for provider errors that do not map to a more specific variant,
169    /// including malformed provider responses.
170    #[error("API request failed for {provider}: {message}")]
171    Request {
172        /// Provider that produced the failure.
173        provider: ProviderKind,
174        /// Message reported by the provider, or a description of what was wrong
175        /// with its response.
176        message: String,
177    },
178
179    /// Rate limit exceeded.
180    ///
181    /// Retryable: see [`Error::is_retryable`].
182    #[error("rate limit exceeded for {provider}: {message}")]
183    RateLimit {
184        /// Provider that throttled the request.
185        provider: ProviderKind,
186        /// Message reported by the provider.
187        message: String,
188    },
189
190    /// Invalid request (bad parameters, etc.)
191    #[error("invalid request: {0}")]
192    InvalidRequest(String),
193
194    /// The requested model is not available or not supported.
195    #[error("model not available: {model} for provider {provider}")]
196    ModelNotAvailable {
197        /// Provider the model was requested from.
198        provider: ProviderKind,
199        /// Model identifier that was rejected.
200        model: String,
201    },
202
203    /// Provider not configured (missing API key, etc.)
204    #[error("provider {0} is not configured")]
205    ProviderNotConfigured(ProviderKind),
206
207    /// Provider feature not enabled.
208    #[error(
209        "provider {0} feature is not enabled — enable the '{feature}' feature in Cargo.toml",
210        feature = .0.feature_name()
211    )]
212    ProviderNotEnabled(ProviderKind),
213
214    /// The endpoint does not implement a part of the API the request needed.
215    ///
216    /// Raised for OpenAI-compatible endpoints, which share OpenAI's wire format
217    /// without necessarily sharing its feature set. It is deliberately distinct
218    /// from [`Error::Request`] and [`Error::InvalidRequest`] so a caller can
219    /// degrade gracefully — retry without tools, or parse free-form text
220    /// instead of asking for a schema — rather than pattern-matching an HTTP
221    /// error body.
222    ///
223    /// Produced either up front, when
224    /// [`EndpointCapabilities`](crate::EndpointCapabilities) says the endpoint
225    /// lacks the capability, or from the endpoint's own rejection of a request
226    /// that used it.
227    #[error("{capability} is not supported by the {provider} endpoint at {base_url}: {message}")]
228    CapabilityUnsupported {
229        /// Provider that could not serve the request.
230        provider: ProviderKind,
231        /// Capability the request needed.
232        capability: Capability,
233        /// Base URL of the endpoint that could not serve it.
234        base_url: String,
235        /// Why the capability is unavailable: the endpoint's own message, or a
236        /// note that it was declared unsupported.
237        message: String,
238    },
239
240    /// Content was filtered/blocked by the provider.
241    #[error("content filtered by {provider}: {reason}")]
242    ContentFiltered {
243        /// Provider that filtered the content.
244        provider: ProviderKind,
245        /// Reason the provider gave for filtering.
246        reason: String,
247    },
248
249    /// Configuration error.
250    #[error("configuration error: {0}")]
251    Config(String),
252
253    /// Serialization/deserialization error.
254    #[error("serialization error: {0}")]
255    Serialization(#[from] serde_json::Error),
256
257    /// HTTP client error.
258    #[error("HTTP error: {0}")]
259    Http(#[from] reqwest::Error),
260
261    /// Stream error.
262    #[error("stream error: {0}")]
263    Stream(String),
264
265    /// Timeout.
266    ///
267    /// Retryable: see [`Error::is_retryable`].
268    #[error("request timed out for {provider}")]
269    Timeout {
270        /// Provider whose request timed out.
271        provider: ProviderKind,
272    },
273
274    /// Tool calling is not supported for the selected provider.
275    #[error("tool calling is not supported for provider {provider}")]
276    ToolProviderUnsupported {
277        /// Provider that does not support tool calling.
278        provider: ProviderKind,
279    },
280
281    /// Tool arguments failed validation.
282    ///
283    /// Surfaced to the model as a tool error message rather than aborting the
284    /// tool loop, so it can retry with corrected arguments.
285    #[error("invalid arguments for tool '{name}': {message}")]
286    ToolArguments {
287        /// Name of the tool whose arguments were rejected.
288        name: String,
289        /// Summary of the validation failures.
290        message: String,
291        /// Per-violation diagnostics, sorted and deduplicated.
292        issues: Vec<ToolArgumentIssue>,
293    },
294
295    /// A requested tool is not registered.
296    #[error("tool not found: {name}")]
297    ToolNotFound {
298        /// Tool name the model tried to call.
299        name: String,
300    },
301
302    /// Tool execution exceeded the configured loop limit.
303    ///
304    /// The limit comes from
305    /// [`GenerationConfig::with_max_tool_rounds`](crate::GenerationConfig::with_max_tool_rounds).
306    #[error("tool execution exceeded the maximum number of rounds ({max_rounds})")]
307    ToolLoopLimitExceeded {
308        /// Round limit that was hit.
309        max_rounds: usize,
310    },
311
312    /// Structured output could not be validated against the requested type.
313    #[error("structured output validation failed for {provider} model {model}: {message}")]
314    StructuredOutput {
315        /// Provider that produced the output.
316        provider: ProviderKind,
317        /// Model that produced the output.
318        model: String,
319        /// Why the output was rejected (empty, invalid JSON, schema violation,
320        /// or deserialization failure).
321        message: String,
322    },
323}
324
325impl Error {
326    /// Returns `true` if this error is likely transient and the request can be retried.
327    pub fn is_retryable(&self) -> bool {
328        matches!(
329            self,
330            Error::RateLimit { .. } | Error::Timeout { .. } | Error::Http(_)
331        )
332    }
333
334    /// Returns `true` if this is an authentication error.
335    pub fn is_auth_error(&self) -> bool {
336        matches!(self, Error::Auth { .. })
337    }
338
339    /// Returns `true` if this is a rate limit error.
340    pub fn is_rate_limit(&self) -> bool {
341        matches!(self, Error::RateLimit { .. })
342    }
343
344    /// The capability an endpoint could not provide, if this is a
345    /// [`Error::CapabilityUnsupported`].
346    ///
347    /// This is the hook for falling back to a simpler request shape.
348    ///
349    /// # Examples
350    ///
351    /// ```
352    /// use rai_sdk::{Capability, Error, ProviderKind};
353    ///
354    /// let error = Error::CapabilityUnsupported {
355    ///     provider: ProviderKind::OpenAICompatible,
356    ///     capability: Capability::ToolCalling,
357    ///     base_url: "http://localhost:11434/v1".to_string(),
358    ///     message: "the model does not support tools".to_string(),
359    /// };
360    ///
361    /// assert_eq!(error.unsupported_capability(), Some(Capability::ToolCalling));
362    /// assert!(!error.is_retryable());
363    /// ```
364    pub fn unsupported_capability(&self) -> Option<Capability> {
365        match self {
366            Error::CapabilityUnsupported { capability, .. } => Some(*capability),
367            _ => None,
368        }
369    }
370
371    /// Short error category string for use as a metrics or logging label.
372    pub fn kind_str(&self) -> &'static str {
373        match self {
374            Error::Auth { .. } => "auth",
375            Error::Request { .. } => "request",
376            Error::RateLimit { .. } => "rate_limit",
377            Error::InvalidRequest(_) => "invalid_request",
378            Error::ModelNotAvailable { .. } => "model_not_available",
379            Error::ProviderNotConfigured(_) => "provider_not_configured",
380            Error::ProviderNotEnabled(_) => "provider_not_enabled",
381            Error::CapabilityUnsupported { .. } => "capability_unsupported",
382            Error::ContentFiltered { .. } => "content_filtered",
383            Error::Config(_) => "config",
384            Error::Serialization(_) => "serialization",
385            Error::Http(_) => "http",
386            Error::Stream(_) => "stream",
387            Error::Timeout { .. } => "timeout",
388            Error::ToolProviderUnsupported { .. } => "tool_provider_unsupported",
389            Error::ToolArguments { .. } => "tool_arguments",
390            Error::ToolNotFound { .. } => "tool_not_found",
391            Error::ToolLoopLimitExceeded { .. } => "tool_loop_limit_exceeded",
392            Error::StructuredOutput { .. } => "structured_output",
393        }
394    }
395
396    /// Get the provider associated with this error, if any.
397    pub fn provider(&self) -> Option<ProviderKind> {
398        match self {
399            Error::Auth { provider, .. }
400            | Error::Request { provider, .. }
401            | Error::RateLimit { provider, .. }
402            | Error::ModelNotAvailable { provider, .. }
403            | Error::ProviderNotConfigured(provider)
404            | Error::ProviderNotEnabled(provider)
405            | Error::CapabilityUnsupported { provider, .. }
406            | Error::ContentFiltered { provider, .. }
407            | Error::Timeout { provider }
408            | Error::ToolProviderUnsupported { provider }
409            | Error::StructuredOutput { provider, .. } => Some(*provider),
410            _ => None,
411        }
412    }
413}
414
415/// Result type alias for AI SDK operations.
416pub type Result<T> = std::result::Result<T, Error>;