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>;