Skip to main content

rig_core/
error.rs

1//! The provider error type, serializable error reports, and shared retry
2//! classifications for the effect protocol. Reports preserve classifications,
3//! available provider metadata, and textual source chains.
4//!
5//! ```
6//! use rig_core::error::{ErrorKind, ProviderError};
7//!
8//! let error = ProviderError::from_http_response(http::StatusCode::TOO_MANY_REQUESTS, "slow down");
9//! assert!(error.is_retryable());
10//! assert_eq!(error.report().kind, ErrorKind::ProviderResponse);
11//! ```
12
13use std::fmt;
14use std::sync::Arc;
15
16use serde::{Deserialize, Serialize};
17
18use crate::{
19    completion::UnsupportedOption,
20    http_client,
21    observe::AdapterErrorBoundary,
22    provider_response::ProviderResponseError,
23    tool::{ToolErrorKind, ToolExecutionError},
24    vector_store::VectorStoreError,
25};
26
27mod retry;
28
29/// Normalized classification of an [`ErrorReport`].
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum ErrorKind {
33    /// A transport failure that produced no provider reply: a reset
34    /// connection, a timeout, a protocol error, an unreadable response. It
35    /// never carries a status; a reply the server made, whatever its status,
36    /// is [`Self::ProviderResponse`].
37    Http,
38    /// JSON serialization or deserialization failed.
39    Json,
40    /// A URL could not be parsed.
41    Url,
42    /// The request could not be built.
43    Request,
44    /// The response could not be parsed.
45    Response,
46    /// The provider reported a failure without a preserved raw response.
47    Provider,
48    /// The provider replied and its raw response was preserved: a non-2xx
49    /// status with a body, a 2xx error envelope, or a non-HTTP transport's
50    /// error payload. Status, body, request id and headers are on the report.
51    ProviderResponse,
52    /// A tool failed; the inner kind is the tool's own classification.
53    Tool(ToolErrorKind),
54    /// An internal invariant was violated.
55    Internal,
56    /// The operation was cancelled.
57    Cancelled,
58    /// The operation exceeded its deadline.
59    Timeout,
60    /// No handler serves the requested key and effect family.
61    /// The message identifies the key.
62    HandlerUnavailable,
63    /// Replay requested a different effect or exhausted the log.
64    /// The run fails without retry rather than inventing a recorded answer.
65    Divergence,
66    /// Policy denied dispatch before handler execution. Not retryable under
67    /// the same policy; distinct from program cancellation.
68    Denied,
69    /// Anything else.
70    Other,
71}
72
73impl ErrorKind {
74    /// Stable snake-case classification code, excluding variant payloads.
75    pub fn code(&self) -> &'static str {
76        match self {
77            Self::Http => "http",
78            Self::Json => "json",
79            Self::Url => "url",
80            Self::Request => "request",
81            Self::Response => "response",
82            Self::Provider => "provider",
83            Self::ProviderResponse => "provider_response",
84            Self::Tool(_) => "tool",
85            Self::Internal => "internal",
86            Self::Cancelled => "cancelled",
87            Self::Timeout => "timeout",
88            Self::HandlerUnavailable => "handler_unavailable",
89            Self::Divergence => "divergence",
90            Self::Denied => "denied",
91            Self::Other => "other",
92        }
93    }
94}
95
96/// A serde-able error crossing a wire boundary.
97///
98/// Field semantics:
99/// - `retryable` is the one policy signal; it is decided at conversion time
100///   from the source's own classification (see [`retryable_status`]).
101/// - `message` is the source's `Display`; `source_chain` is the `Display` of
102///   each `source()` link, outermost first, excluding `message` itself.
103/// - `code`, `http_status`, `refusal` are copied when the source had them.
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105pub struct ErrorReport {
106    /// Normalized classification.
107    pub kind: ErrorKind,
108    /// Whether the same operation may reasonably be retried.
109    pub retryable: bool,
110    /// Human-readable description (the source's `Display`).
111    pub message: String,
112    /// A provider- or tool-specific machine code, when one was reported:
113    /// for a provider's reply, the transport's own code when it gave one
114    /// apart from the body, else the code the body names
115    /// ([`ProviderResponseError::machine_code`]).
116    pub code: Option<String>,
117    /// The HTTP status, when the failure had one.
118    pub http_status: Option<u16>,
119    /// The failure was an intentional refusal rather than a fault.
120    pub refusal: bool,
121    /// `Display` of each `source()` link, outermost first.
122    pub source_chain: Vec<String>,
123    /// The provider's request id, when the failure had a response that
124    /// carried one.
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    pub request_id: Option<String>,
127    /// Preserved provider failure response, including available status, body,
128    /// headers, and request ID.
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub provider_response: Option<crate::provider_response::ProviderResponseError>,
131}
132
133impl ErrorReport {
134    /// Build a report of `kind` with `message` and no other metadata.
135    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
136        Self {
137            kind,
138            retryable: false,
139            message: message.into(),
140            code: None,
141            http_status: None,
142            refusal: false,
143            source_chain: Vec::new(),
144            request_id: None,
145            provider_response: None,
146        }
147    }
148
149    /// Set `retryable`.
150    pub fn with_retryable(mut self, retryable: bool) -> Self {
151        self.retryable = retryable;
152        self
153    }
154
155    /// Set the machine code.
156    pub fn with_code(mut self, code: impl Into<String>) -> Self {
157        self.code = Some(code.into());
158        self
159    }
160
161    /// Set the HTTP status.
162    pub fn with_http_status(mut self, status: u16) -> Self {
163        self.http_status = Some(status);
164        self
165    }
166
167    /// Mark the report as an intentional refusal.
168    pub fn refused(mut self) -> Self {
169        self.refusal = true;
170        self
171    }
172
173    /// Attach the provider's request id.
174    pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
175        self.request_id = Some(request_id.into());
176        self
177    }
178
179    /// Whether the same operation may reasonably be retried.
180    pub const fn is_retryable(&self) -> bool {
181        self.retryable
182    }
183}
184
185impl ErrorReport {
186    /// The provider response body preserved on this report, if any.
187    pub fn provider_response_body(&self) -> Option<&str> {
188        self.provider_response
189            .as_ref()
190            .map(|response| response.body.as_str())
191    }
192
193    /// The preserved provider response body parsed as JSON, when present.
194    pub fn provider_response_json(&self) -> Result<Option<serde_json::Value>, serde_json::Error> {
195        crate::provider_response::json(self.provider_response_body())
196    }
197
198    /// The preserved provider response headers, if any.
199    pub fn provider_response_headers(&self) -> Option<&http::HeaderMap> {
200        self.provider_response
201            .as_ref()
202            .and_then(|response| response.headers.as_ref())
203    }
204
205    /// The HTTP status this report carries, as a status code.
206    pub fn provider_response_status(&self) -> Option<http::StatusCode> {
207        self.http_status
208            .and_then(|status| http::StatusCode::from_u16(status).ok())
209    }
210
211    /// The provider's transport request id, if the failure carried one.
212    pub fn provider_request_id(&self) -> Option<&str> {
213        self.request_id.as_deref()
214    }
215}
216
217impl fmt::Display for ErrorReport {
218    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219        f.write_str(&self.message)
220    }
221}
222
223impl std::error::Error for ErrorReport {}
224
225/// The one status → retryable table.
226///
227/// Request Timeout (408), Too Early (425), Too Many Requests (429) and every
228/// server error (5xx) are retryable; every other status is not. A missing
229/// status decides nothing here: a failure with no status is classified by
230/// what it is ([`transient_transport`]), not by what it lacks.
231pub const fn retryable_status(status: Option<u16>) -> bool {
232    match status {
233        None => false,
234        Some(408 | 425 | 429) => true,
235        Some(s) => s >= 500 && s <= 599,
236    }
237}
238
239/// Classifies `StreamEnded` and backend `Instance` errors as retryable.
240/// Protocol, header, and content-type errors, and a client that could not be
241/// built (`Unbuilt`), are not retryable. Errors carrying
242/// an HTTP status use [`retryable_status`]. This does not guarantee the server
243/// has not processed the request.
244pub fn transient_transport(error: &http_client::Error) -> bool {
245    match error {
246        http_client::Error::StreamEnded | http_client::Error::Instance(_) => true,
247        http_client::Error::InvalidStatusCodeWithDetails { status, .. } => {
248            retryable_status(Some(status.as_u16()))
249        }
250        _ => false,
251    }
252}
253
254/// Collect the `Display` of each `source()` link below `error`, outermost
255/// first.
256pub(crate) fn source_chain(error: &(dyn std::error::Error + 'static)) -> Vec<String> {
257    let mut chain = Vec::new();
258    let mut current = error.source();
259    while let Some(source) = current {
260        chain.push(source.to_string());
261        current = source.source();
262    }
263    chain
264}
265
266/// A boxed request-building failure.
267#[cfg(not(target_family = "wasm"))]
268pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
269
270/// A boxed request-building failure.
271#[cfg(target_family = "wasm")]
272pub type BoxError = Box<dyn std::error::Error + 'static>;
273
274/// A request-building failure shared by every clone of the error that holds
275/// it.
276#[cfg(not(target_family = "wasm"))]
277pub type SharedError = Arc<dyn std::error::Error + Send + Sync + 'static>;
278
279/// A request-building failure shared by every clone of the error that holds
280/// it.
281#[cfg(target_family = "wasm")]
282pub type SharedError = Arc<dyn std::error::Error + 'static>;
283
284/// A failed provider operation: completion, embedding, reranking,
285/// transcription, image or audio generation, verification, model listing, or
286/// context caching.
287///
288/// The variant is the classification, and each variant maps to one
289/// [`ErrorKind`]. A provider's reply is preserved as a
290/// [`ProviderResponseError`] with its status, body, headers, and request ID;
291/// read it through [`Self::provider_response`] or the accessors below.
292///
293/// ```
294/// use rig_core::error::ProviderError;
295///
296/// fn log(error: &ProviderError) {
297///     if let Some(status) = error.provider_response_status() {
298///         // Error envelopes can arrive with successful HTTP statuses.
299///         eprintln!("provider returned HTTP {status}");
300///     }
301///     match error.provider_response_json() {
302///         Ok(Some(json)) => eprintln!("provider error payload: {json}"),
303///         Ok(None) => eprintln!("no provider response body: {error}"),
304///         Err(_) => eprintln!("non-JSON body: {:?}", error.provider_response_body()),
305///     }
306/// }
307/// ```
308///
309/// Errors are `Clone`: sources are shared, so a stream that already yielded
310/// an error can hand the same error to a later caller.
311#[non_exhaustive]
312#[derive(Debug, Clone)]
313pub enum ProviderError {
314    /// A transport failure that produced no provider reply: a reset
315    /// connection, a timeout, an unreadable response. A reply the server
316    /// made is [`Self::ProviderResponse`].
317    Http(Arc<http_client::Error>),
318    /// JSON serialization or deserialization failed.
319    Json(Arc<serde_json::Error>),
320    /// A URL could not be parsed.
321    Url(url::ParseError),
322    /// The request could not be built.
323    Request(SharedError),
324    /// The reply decoded but does not answer the request.
325    Response(String),
326    /// The provider reported a failure without a preserved reply: a Rig
327    /// diagnostic, never classified as retryable. A provider's own error
328    /// payload belongs in [`Self::ProviderResponse`], whose envelope decides
329    /// its code and retry verdict; never put provider JSON here.
330    Provider(String),
331    /// The provider's reply, preserved: a non-success status with its body, a
332    /// 2xx error envelope, or a non-HTTP transport's error payload.
333    ProviderResponse(ProviderResponseError),
334    /// The provider rejected the configured credentials with 401 or 403.
335    InvalidAuthentication(ProviderResponseError),
336    /// A request for an existing context-cache handle answered 403 or 404.
337    /// The reply's body is the provider's explanation, which can name a
338    /// cause other than expiry, such as a credential or quota failure.
339    CacheExpired {
340        /// The cache handle the request named.
341        name: String,
342        /// The provider's reply.
343        response: ProviderResponseError,
344    },
345    /// The provider returned vectors of a width other than the one the caller
346    /// declared through an embedding wire's `ndims` argument. Raised only
347    /// when the width was set explicitly.
348    MismatchedDimensions {
349        /// Provider whose response disagreed with the declared width.
350        provider: String,
351        /// Width the caller declared.
352        requested: usize,
353        /// Width the provider actually returned.
354        returned: usize,
355    },
356    /// A failure a relay delivered as its report, such as a relayed
357    /// stream. It reports as the relayed report, unchanged.
358    Relayed(Box<ErrorReport>),
359    /// The reply stopped before the provider ended it: its frames ran out,
360    /// or the runtime stopped, without the provider's end.
361    Truncated,
362    /// A [`GenerationOptions`](crate::completion::GenerationOptions) field
363    /// the wire or model cannot honour, under
364    /// [`OnUnsupported::Error`](crate::completion::OnUnsupported::Error).
365    UnsupportedOption(UnsupportedOption),
366}
367
368impl fmt::Display for ProviderError {
369    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
370        match self {
371            Self::Http(error) => write!(f, "HttpError: {error}"),
372            Self::Json(error) => write!(f, "JsonError: {error}"),
373            Self::Url(error) => write!(f, "UrlError: {error}"),
374            Self::Request(error) => write!(f, "RequestError: {error}"),
375            Self::Response(message) => write!(f, "ResponseError: {message}"),
376            Self::Provider(message) => write!(f, "ProviderError: {message}"),
377            Self::ProviderResponse(response) => write!(f, "ProviderResponseError: {response}"),
378            Self::InvalidAuthentication(response) => {
379                write!(f, "invalid authentication: {response}")
380            }
381            Self::CacheExpired { name, response } => write!(
382                f,
383                "cached content `{name}` is expired or was deleted: {}",
384                response.body
385            ),
386            Self::MismatchedDimensions {
387                provider,
388                requested,
389                returned,
390            } => write!(
391                f,
392                "{provider} embedding response returned {returned}-dimension vectors, but the \
393                 model was created with {requested} dimensions; this provider does not resize \
394                 embeddings"
395            ),
396            Self::Relayed(report) => f.write_str(&report.message),
397            Self::Truncated => {
398                f.write_str("ResponseError: the reply ended before the provider ended it")
399            }
400            Self::UnsupportedOption(option) => write!(f, "RequestError: {option}"),
401        }
402    }
403}
404
405/// The source is the shared error itself, not its `Arc`, so callers can
406/// downcast it.
407impl std::error::Error for ProviderError {
408    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
409        match self {
410            Self::Json(error) => Some(&**error),
411            Self::Url(error) => Some(error),
412            Self::Request(error) => Some(&**error),
413            _ => None,
414        }
415    }
416}
417
418impl From<url::ParseError> for ProviderError {
419    fn from(error: url::ParseError) -> Self {
420        Self::Url(error)
421    }
422}
423
424impl ProviderError {
425    /// A request that could not be built, for the given reason.
426    pub fn request(reason: impl Into<BoxError>) -> Self {
427        Self::Request(Arc::from(reason.into()))
428    }
429
430    /// Preserves the status and verbatim body as [`Self::ProviderResponse`],
431    /// including error envelopes returned with 2xx statuses.
432    pub fn from_http_response(status: http::StatusCode, body: impl Into<String>) -> Self {
433        Self::ProviderResponse(ProviderResponseError::new(status, body))
434    }
435
436    /// Preserves a verbatim provider error body with no HTTP status as
437    /// [`Self::ProviderResponse`] (see [`ProviderResponseError::from_body`]).
438    pub fn from_provider_body(body: impl Into<String>) -> Self {
439        Self::ProviderResponse(ProviderResponseError::from_body(body))
440    }
441
442    /// Converts a non-success reply the transport reported as an error to
443    /// [`Self::ProviderResponse`], keeping its status, body, and headers.
444    /// Other transport errors become [`Self::Http`].
445    pub fn from_transport_error(error: http_client::Error) -> Self {
446        match error {
447            http_client::Error::InvalidStatusCodeWithDetails {
448                status,
449                body,
450                headers,
451            } => Self::from_http_response(status, body).with_response_headers(Some(headers)),
452            other => Self::Http(Arc::new(other)),
453        }
454    }
455
456    /// The classification this error reports as.
457    pub fn kind(&self) -> ErrorKind {
458        match self {
459            Self::Http(_) => ErrorKind::Http,
460            Self::Json(_) => ErrorKind::Json,
461            Self::Url(_) => ErrorKind::Url,
462            Self::Request(_) | Self::UnsupportedOption(_) => ErrorKind::Request,
463            Self::Response(_) | Self::MismatchedDimensions { .. } | Self::Truncated => {
464                ErrorKind::Response
465            }
466            Self::Provider(_) => ErrorKind::Provider,
467            Self::ProviderResponse(_)
468            | Self::InvalidAuthentication(_)
469            | Self::CacheExpired { .. } => ErrorKind::ProviderResponse,
470            Self::Relayed(report) => report.kind,
471        }
472    }
473
474    /// Classifies transport failures with [`transient_transport`] and
475    /// preserved replies with [`ProviderResponseError::is_retryable`]; a
476    /// truncated reply is retryable. Every other failure, rejected
477    /// credentials and expired caches included, is not retryable.
478    pub fn is_retryable(&self) -> bool {
479        match self {
480            Self::Http(error) => transient_transport(error),
481            Self::ProviderResponse(response) => response.is_retryable(),
482            Self::Relayed(report) => report.retryable,
483            // A reply cut short, by its transport or its runtime, may end on
484            // the next attempt.
485            Self::Truncated => true,
486            _ => false,
487        }
488    }
489
490    /// The provider's preserved reply, when this error carries one.
491    pub fn provider_response(&self) -> Option<&ProviderResponseError> {
492        match self {
493            Self::ProviderResponse(response)
494            | Self::InvalidAuthentication(response)
495            | Self::CacheExpired { response, .. } => Some(response),
496            Self::Relayed(report) => report.provider_response.as_ref(),
497            _ => None,
498        }
499    }
500
501    /// The preserved reply's body. An empty body returns `Some("")`, while
502    /// [`Self::provider_response_json`] maps it to `Ok(None)`.
503    pub fn provider_response_body(&self) -> Option<&str> {
504        self.provider_response()
505            .map(|response| response.body.as_str())
506    }
507
508    /// Parses the preserved reply's body as JSON: `Ok(None)` when there is no
509    /// body or it is empty, `Err` when it is not valid JSON.
510    pub fn provider_response_json(&self) -> Result<Option<serde_json::Value>, serde_json::Error> {
511        crate::provider_response::json(self.provider_response_body())
512    }
513
514    /// The preserved reply's HTTP status. It may be 2xx for an error envelope.
515    pub fn provider_response_status(&self) -> Option<http::StatusCode> {
516        self.provider_response()
517            .and_then(|response| response.status)
518    }
519
520    /// The provider's transport request ID, when the reply carried one.
521    pub fn provider_request_id(&self) -> Option<&str> {
522        self.provider_response()
523            .and_then(|response| response.provider_request_id.as_deref())
524    }
525
526    /// The preserved reply's headers. `None` means not captured, as for
527    /// non-HTTP transports and replies built from only a status and body.
528    /// This example reads the seconds form of `Retry-After`:
529    ///
530    /// ```no_run
531    /// # use rig_core::error::ProviderError;
532    /// # use std::time::Duration;
533    /// fn backoff(error: &ProviderError) -> Option<Duration> {
534    ///     let seconds = error
535    ///         .provider_response_headers()?
536    ///         .get(http::header::RETRY_AFTER)?
537    ///         .to_str()
538    ///         .ok()?
539    ///         .parse()
540    ///         .ok()?;
541    ///     Some(Duration::from_secs(seconds))
542    /// }
543    /// ```
544    pub fn provider_response_headers(&self) -> Option<&http::HeaderMap> {
545        self.provider_response()
546            .and_then(|response| response.headers.as_ref())
547    }
548
549    /// Fills an absent request ID on the preserved reply, ignoring empty
550    /// strings.
551    pub fn with_provider_request_id(self, request_id: Option<String>) -> Self {
552        self.map_response(|response| match response.provider_request_id {
553            Some(_) => response,
554            None => response.with_provider_request_id(request_id),
555        })
556    }
557
558    /// Fills absent headers on the preserved reply.
559    pub fn with_response_headers(self, headers: Option<http::HeaderMap>) -> Self {
560        self.map_response(|response| match (&response.headers, headers) {
561            (None, Some(headers)) => response.with_headers(Some(headers)),
562            _ => response,
563        })
564    }
565
566    /// Attaches the HTTP status a transport reported beside a reply preserved
567    /// without one, so it classifies by status. A captured status is kept.
568    pub fn with_provider_status(self, status: Option<http::StatusCode>) -> Self {
569        self.map_response(|response| response.with_status(status))
570    }
571
572    /// Attaches the provider's machine-readable code for the failure, such as
573    /// a gRPC status name or an AWS exception type.
574    pub fn with_provider_code(self, code: Option<String>) -> Self {
575        self.map_response(|response| response.with_code(code))
576    }
577
578    /// Replaces the preserved reply's transport retry verdict, used when its
579    /// status is absent or successful.
580    pub fn with_transient(self, transient: Option<bool>) -> Self {
581        self.map_response(|response| response.with_transient(transient))
582    }
583
584    /// The wire form of this error.
585    pub fn report(&self) -> ErrorReport {
586        ErrorReport::from(self)
587    }
588
589    /// Which boundary produced this error, by its classification.
590    pub(crate) fn boundary(&self) -> AdapterErrorBoundary {
591        match (self, self.kind()) {
592            (Self::Http(error), _) => AdapterErrorBoundary::from_http(error),
593            (_, ErrorKind::Json | ErrorKind::Response) => AdapterErrorBoundary::Decode,
594            (_, ErrorKind::Provider | ErrorKind::ProviderResponse) => {
595                AdapterErrorBoundary::ProviderResponse
596            }
597            _ => AdapterErrorBoundary::Request,
598        }
599    }
600
601    fn map_response(
602        self,
603        map: impl FnOnce(ProviderResponseError) -> ProviderResponseError,
604    ) -> Self {
605        match self {
606            Self::ProviderResponse(response) => Self::ProviderResponse(map(response)),
607            Self::InvalidAuthentication(response) => Self::InvalidAuthentication(map(response)),
608            Self::CacheExpired { name, response } => Self::CacheExpired {
609                name,
610                response: map(response),
611            },
612            // The report restates what it read from the reply it preserves,
613            // so what the map fills reaches both. What it held is kept.
614            Self::Relayed(mut report) => {
615                if let Some(response) = report.provider_response.take() {
616                    let response = map(response);
617                    report.request_id = report
618                        .request_id
619                        .or_else(|| response.provider_request_id.clone());
620                    report.http_status = report
621                        .http_status
622                        .or_else(|| response.status.map(|status| status.as_u16()));
623                    report.code = report.code.or_else(|| response.machine_code());
624                    report.refusal |= response.refusal;
625                    report.provider_response = Some(response);
626                }
627                Self::Relayed(report)
628            }
629            other => other,
630        }
631    }
632}
633
634impl From<serde_json::Error> for ProviderError {
635    fn from(error: serde_json::Error) -> Self {
636        Self::Json(Arc::new(error))
637    }
638}
639
640impl From<BoxError> for ProviderError {
641    fn from(error: BoxError) -> Self {
642        Self::Request(Arc::from(error))
643    }
644}
645
646impl From<http_client::Error> for ProviderError {
647    fn from(error: http_client::Error) -> Self {
648        Self::from_transport_error(error)
649    }
650}
651
652/// A failure to build a provider request, returned by
653/// [`Wire::encode`](crate::wire::Wire::encode). It converts only into
654/// [`ProviderError::Request`] or [`ProviderError::UnsupportedOption`], both
655/// of kind [`ErrorKind::Request`], so every provider classifies its encode
656/// failures alike.
657#[non_exhaustive]
658#[derive(Debug, thiserror::Error)]
659#[error(transparent)]
660pub struct EncodeError(ProviderError);
661
662impl EncodeError {
663    /// A request that could not be built, for the given reason.
664    pub fn request(reason: impl Into<BoxError>) -> Self {
665        Self(ProviderError::request(reason))
666    }
667
668    /// A request that sets an option the wire or model cannot honour.
669    pub fn unsupported(option: UnsupportedOption) -> Self {
670        Self(ProviderError::UnsupportedOption(option))
671    }
672
673    /// The refused option, when this error is a refusal.
674    pub fn unsupported_option(&self) -> Option<&UnsupportedOption> {
675        match &self.0 {
676            ProviderError::UnsupportedOption(option) => Some(option),
677            _ => None,
678        }
679    }
680}
681
682impl From<EncodeError> for ProviderError {
683    fn from(error: EncodeError) -> Self {
684        debug_assert_eq!(error.0.kind(), ErrorKind::Request);
685        error.0
686    }
687}
688
689impl From<http::Error> for EncodeError {
690    fn from(error: http::Error) -> Self {
691        Self::request(error)
692    }
693}
694
695impl From<serde_json::Error> for EncodeError {
696    fn from(error: serde_json::Error) -> Self {
697        Self::request(error)
698    }
699}
700
701impl From<crate::message::MessageError> for EncodeError {
702    fn from(error: crate::message::MessageError) -> Self {
703        Self::request(error)
704    }
705}
706
707impl From<BoxError> for EncodeError {
708    fn from(error: BoxError) -> Self {
709        Self(ProviderError::from(error))
710    }
711}
712
713impl From<http::Error> for ProviderError {
714    fn from(error: http::Error) -> Self {
715        Self::request(error)
716    }
717}
718
719impl From<&ProviderError> for ErrorReport {
720    fn from(error: &ProviderError) -> Self {
721        if let ProviderError::Relayed(report) = error {
722            return (**report).clone();
723        }
724        let response = error.provider_response();
725        ErrorReport {
726            kind: error.kind(),
727            retryable: error.is_retryable(),
728            message: error.to_string(),
729            code: response.and_then(ProviderResponseError::machine_code),
730            http_status: response
731                .and_then(|response| response.status)
732                .map(|status| status.as_u16()),
733            refusal: response.is_some_and(|response| response.refusal),
734            source_chain: source_chain(error),
735            request_id: response.and_then(|response| response.provider_request_id.clone()),
736            provider_response: response.cloned(),
737        }
738    }
739}
740
741impl From<ProviderError> for ErrorReport {
742    fn from(error: ProviderError) -> Self {
743        Self::from(&error)
744    }
745}
746
747impl ToolExecutionError {
748    /// The wire form of this error.
749    pub fn report(&self) -> ErrorReport {
750        ErrorReport::from(self)
751    }
752}
753
754impl ToolExecutionError {
755    /// Whether the tool may reasonably be re-run: the explicit override when
756    /// one was set, else the kind's own default
757    /// ([`ToolErrorKind::default_retryable`]); a kind that leaves it to the
758    /// tool (`None`) is not retryable on the wire.
759    pub fn is_retryable(&self) -> bool {
760        self.retryable()
761            .or_else(|| self.kind().default_retryable())
762            .unwrap_or(false)
763    }
764}
765
766impl From<&ToolExecutionError> for ErrorReport {
767    fn from(error: &ToolExecutionError) -> Self {
768        ErrorReport {
769            kind: ErrorKind::Tool(error.kind()),
770            retryable: error.is_retryable(),
771            message: error.message().to_string(),
772            code: error.code().map(str::to_string),
773            http_status: error.http_status(),
774            refusal: error.is_refusal(),
775            source_chain: source_chain(error),
776            request_id: None,
777            provider_response: None,
778        }
779    }
780}
781
782impl From<ToolExecutionError> for ErrorReport {
783    fn from(error: ToolExecutionError) -> Self {
784        Self::from(&error)
785    }
786}
787
788/// Rebuilds a tool failure from a dispatch report.
789///
790/// An [`ErrorKind::Tool`] report keeps its tool kind; other kinds map to the
791/// closest [`ToolErrorKind`]. Retryability, code, HTTP status, and refusal are
792/// copied, the message is model-visible, and the report is the `source()`.
793impl From<ErrorReport> for ToolExecutionError {
794    fn from(report: ErrorReport) -> Self {
795        let kind = match report.kind {
796            ErrorKind::Tool(kind) => kind,
797            ErrorKind::Timeout => ToolErrorKind::Timeout,
798            ErrorKind::Cancelled => ToolErrorKind::Cancelled,
799            ErrorKind::Denied => ToolErrorKind::PermissionDenied,
800            ErrorKind::HandlerUnavailable => ToolErrorKind::NotFound,
801            ErrorKind::Http => ToolErrorKind::Network,
802            ErrorKind::Provider | ErrorKind::ProviderResponse => ToolErrorKind::Provider,
803            ErrorKind::Json
804            | ErrorKind::Url
805            | ErrorKind::Request
806            | ErrorKind::Response
807            | ErrorKind::Internal
808            | ErrorKind::Divergence
809            | ErrorKind::Other => ToolErrorKind::Other,
810        };
811        let mut error = if report.refusal {
812            ToolExecutionError::refused(report.message.clone())
813        } else {
814            ToolExecutionError::new(kind, report.message.clone())
815        }
816        .with_retryable(report.retryable);
817        if let Some(code) = &report.code {
818            error = error.with_code(code.clone());
819        }
820        if let Some(status) = report.http_status {
821            error = error.with_http_status(status);
822        }
823        error.with_source(report)
824    }
825}
826
827impl From<&VectorStoreError> for ErrorReport {
828    fn from(error: &VectorStoreError) -> Self {
829        let (kind, provider_response) = match error {
830            VectorStoreError::EmbeddingError(inner) => {
831                return Self {
832                    message: error.to_string(),
833                    source_chain: source_chain(error),
834                    ..Self::from(inner)
835                };
836            }
837            VectorStoreError::JsonError(_) => (ErrorKind::Json, None),
838            VectorStoreError::DatastoreError(_) => (ErrorKind::Provider, None),
839            VectorStoreError::FilterError(_) | VectorStoreError::SamplesOutOfRange { .. } => {
840                (ErrorKind::Request, None)
841            }
842            VectorStoreError::MissingIdError(_) => (ErrorKind::Response, None),
843            VectorStoreError::Http(crate::http_client::Error::InvalidStatusCodeWithDetails {
844                status,
845                body,
846                headers,
847            }) => (
848                ErrorKind::ProviderResponse,
849                Some(
850                    crate::provider_response::ProviderResponseError::new(*status, body.clone())
851                        .with_headers(Some(headers.clone())),
852                ),
853            ),
854            VectorStoreError::Http(_) => (ErrorKind::Http, None),
855            // A store's own non-2xx reply: the server answered, so the status
856            // classifies it and its retryability, like any provider reply.
857            VectorStoreError::ExternalAPIError(status, body) => (
858                ErrorKind::ProviderResponse,
859                Some(crate::provider_response::ProviderResponseError::new(
860                    *status,
861                    body.clone(),
862                )),
863            ),
864        };
865        let http_status = provider_response
866            .as_ref()
867            .and_then(|response| response.status.map(|status| status.as_u16()));
868        let retryable = match error {
869            VectorStoreError::Http(inner) => transient_transport(inner),
870            VectorStoreError::ExternalAPIError(..) => retryable_status(http_status),
871            _ => false,
872        };
873        let code = provider_response
874            .as_ref()
875            .and_then(|response| response.machine_code());
876        ErrorReport {
877            kind,
878            retryable,
879            message: error.to_string(),
880            code,
881            http_status,
882            refusal: provider_response.as_ref().is_some_and(|r| r.refusal),
883            source_chain: source_chain(error),
884            request_id: None,
885            provider_response,
886        }
887    }
888}
889
890impl From<VectorStoreError> for ErrorReport {
891    fn from(error: VectorStoreError) -> Self {
892        Self::from(&error)
893    }
894}
895
896// The report is the wire error of the effect protocol: it must cross threads
897// and serialize on every target, browser wasm included.
898const _: fn() = || {
899    fn assert_wire<T: Send + Sync + 'static + Serialize + serde::de::DeserializeOwned>() {}
900    assert_wire::<ErrorReport>();
901    assert_wire::<ErrorKind>();
902};
903
904#[cfg(test)]
905mod encode_tests;
906#[cfg(test)]
907mod tests;