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 streamed reply sent nothing for this long, before its first frame
363    /// or between two, and was dropped. Retryable.
364    Stalled(std::time::Duration),
365    /// A [`GenerationOptions`](crate::completion::GenerationOptions) field
366    /// the wire or model cannot honour, under
367    /// [`OnUnsupported::Error`](crate::completion::OnUnsupported::Error).
368    UnsupportedOption(UnsupportedOption),
369}
370
371impl fmt::Display for ProviderError {
372    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
373        match self {
374            Self::Http(error) => write!(f, "HttpError: {error}"),
375            Self::Json(error) => write!(f, "JsonError: {error}"),
376            Self::Url(error) => write!(f, "UrlError: {error}"),
377            Self::Request(error) => write!(f, "RequestError: {error}"),
378            Self::Response(message) => write!(f, "ResponseError: {message}"),
379            Self::Provider(message) => write!(f, "ProviderError: {message}"),
380            Self::ProviderResponse(response) => write!(f, "ProviderResponseError: {response}"),
381            Self::InvalidAuthentication(response) => {
382                write!(f, "invalid authentication: {response}")
383            }
384            Self::CacheExpired { name, response } => write!(
385                f,
386                "cached content `{name}` is expired or was deleted: {}",
387                response.body
388            ),
389            Self::MismatchedDimensions {
390                provider,
391                requested,
392                returned,
393            } => write!(
394                f,
395                "{provider} embedding response returned {returned}-dimension vectors, but the \
396                 model was created with {requested} dimensions; this provider does not resize \
397                 embeddings"
398            ),
399            Self::Relayed(report) => f.write_str(&report.message),
400            Self::Truncated => {
401                f.write_str("ResponseError: the reply ended before the provider ended it")
402            }
403            Self::Stalled(idle) => write!(
404                f,
405                "TimeoutError: the streamed reply sent nothing for {}s",
406                idle.as_secs_f64()
407            ),
408            Self::UnsupportedOption(option) => write!(f, "RequestError: {option}"),
409        }
410    }
411}
412
413/// The source is the shared error itself, not its `Arc`, so callers can
414/// downcast it.
415impl std::error::Error for ProviderError {
416    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
417        match self {
418            Self::Json(error) => Some(&**error),
419            Self::Url(error) => Some(error),
420            Self::Request(error) => Some(&**error),
421            _ => None,
422        }
423    }
424}
425
426impl From<url::ParseError> for ProviderError {
427    fn from(error: url::ParseError) -> Self {
428        Self::Url(error)
429    }
430}
431
432impl ProviderError {
433    /// A request that could not be built, for the given reason.
434    pub fn request(reason: impl Into<BoxError>) -> Self {
435        Self::Request(Arc::from(reason.into()))
436    }
437
438    /// Preserves the status and verbatim body as [`Self::ProviderResponse`],
439    /// including error envelopes returned with 2xx statuses.
440    pub fn from_http_response(status: http::StatusCode, body: impl Into<String>) -> Self {
441        Self::ProviderResponse(ProviderResponseError::new(status, body))
442    }
443
444    /// Preserves a verbatim provider error body with no HTTP status as
445    /// [`Self::ProviderResponse`] (see [`ProviderResponseError::from_body`]).
446    pub fn from_provider_body(body: impl Into<String>) -> Self {
447        Self::ProviderResponse(ProviderResponseError::from_body(body))
448    }
449
450    /// Converts a non-success reply the transport reported as an error to
451    /// [`Self::ProviderResponse`], keeping its status, body, and headers.
452    /// Other transport errors become [`Self::Http`].
453    pub fn from_transport_error(error: http_client::Error) -> Self {
454        match error {
455            http_client::Error::InvalidStatusCodeWithDetails {
456                status,
457                body,
458                headers,
459            } => Self::from_http_response(status, body).with_response_headers(Some(headers)),
460            other => Self::Http(Arc::new(other)),
461        }
462    }
463
464    /// The classification this error reports as.
465    pub fn kind(&self) -> ErrorKind {
466        match self {
467            Self::Http(_) => ErrorKind::Http,
468            Self::Json(_) => ErrorKind::Json,
469            Self::Url(_) => ErrorKind::Url,
470            Self::Request(_) | Self::UnsupportedOption(_) => ErrorKind::Request,
471            Self::Response(_) | Self::MismatchedDimensions { .. } | Self::Truncated => {
472                ErrorKind::Response
473            }
474            Self::Provider(_) => ErrorKind::Provider,
475            Self::Stalled(_) => ErrorKind::Timeout,
476            Self::ProviderResponse(_)
477            | Self::InvalidAuthentication(_)
478            | Self::CacheExpired { .. } => ErrorKind::ProviderResponse,
479            Self::Relayed(report) => report.kind,
480        }
481    }
482
483    /// Classifies transport failures with [`transient_transport`] and
484    /// preserved replies with [`ProviderResponseError::is_retryable`]; a
485    /// truncated or stalled reply is retryable. Every other failure, rejected
486    /// credentials and expired caches included, is not retryable.
487    pub fn is_retryable(&self) -> bool {
488        match self {
489            Self::Http(error) => transient_transport(error),
490            Self::ProviderResponse(response) => response.is_retryable(),
491            Self::Relayed(report) => report.retryable,
492            // A reply cut short, by its transport or its runtime, may end on
493            // the next attempt.
494            Self::Truncated => true,
495            // A stalled stream is dropped, so the next attempt starts afresh.
496            Self::Stalled(_) => true,
497            _ => false,
498        }
499    }
500
501    /// The provider's preserved reply, when this error carries one.
502    pub fn provider_response(&self) -> Option<&ProviderResponseError> {
503        match self {
504            Self::ProviderResponse(response)
505            | Self::InvalidAuthentication(response)
506            | Self::CacheExpired { response, .. } => Some(response),
507            Self::Relayed(report) => report.provider_response.as_ref(),
508            _ => None,
509        }
510    }
511
512    /// The preserved reply's body. An empty body returns `Some("")`, while
513    /// [`Self::provider_response_json`] maps it to `Ok(None)`.
514    pub fn provider_response_body(&self) -> Option<&str> {
515        self.provider_response()
516            .map(|response| response.body.as_str())
517    }
518
519    /// Parses the preserved reply's body as JSON: `Ok(None)` when there is no
520    /// body or it is empty, `Err` when it is not valid JSON.
521    pub fn provider_response_json(&self) -> Result<Option<serde_json::Value>, serde_json::Error> {
522        crate::provider_response::json(self.provider_response_body())
523    }
524
525    /// The preserved reply's HTTP status. It may be 2xx for an error envelope.
526    pub fn provider_response_status(&self) -> Option<http::StatusCode> {
527        self.provider_response()
528            .and_then(|response| response.status)
529    }
530
531    /// The provider's transport request ID, when the reply carried one.
532    pub fn provider_request_id(&self) -> Option<&str> {
533        self.provider_response()
534            .and_then(|response| response.provider_request_id.as_deref())
535    }
536
537    /// The preserved reply's headers. `None` means not captured, as for
538    /// non-HTTP transports and replies built from only a status and body.
539    /// This example reads the seconds form of `Retry-After`:
540    ///
541    /// ```no_run
542    /// # use rig_core::error::ProviderError;
543    /// # use std::time::Duration;
544    /// fn backoff(error: &ProviderError) -> Option<Duration> {
545    ///     let seconds = error
546    ///         .provider_response_headers()?
547    ///         .get(http::header::RETRY_AFTER)?
548    ///         .to_str()
549    ///         .ok()?
550    ///         .parse()
551    ///         .ok()?;
552    ///     Some(Duration::from_secs(seconds))
553    /// }
554    /// ```
555    pub fn provider_response_headers(&self) -> Option<&http::HeaderMap> {
556        self.provider_response()
557            .and_then(|response| response.headers.as_ref())
558    }
559
560    /// Fills an absent request ID on the preserved reply, ignoring empty
561    /// strings.
562    pub fn with_provider_request_id(self, request_id: Option<String>) -> Self {
563        self.map_response(|response| match response.provider_request_id {
564            Some(_) => response,
565            None => response.with_provider_request_id(request_id),
566        })
567    }
568
569    /// Fills absent headers on the preserved reply.
570    pub fn with_response_headers(self, headers: Option<http::HeaderMap>) -> Self {
571        self.map_response(|response| match (&response.headers, headers) {
572            (None, Some(headers)) => response.with_headers(Some(headers)),
573            _ => response,
574        })
575    }
576
577    /// Attaches the HTTP status a transport reported beside a reply preserved
578    /// without one, so it classifies by status. A captured status is kept.
579    pub fn with_provider_status(self, status: Option<http::StatusCode>) -> Self {
580        self.map_response(|response| response.with_status(status))
581    }
582
583    /// Attaches the provider's machine-readable code for the failure, such as
584    /// a gRPC status name or an AWS exception type.
585    pub fn with_provider_code(self, code: Option<String>) -> Self {
586        self.map_response(|response| response.with_code(code))
587    }
588
589    /// Replaces the preserved reply's transport retry verdict, used when its
590    /// status is absent or successful.
591    pub fn with_transient(self, transient: Option<bool>) -> Self {
592        self.map_response(|response| response.with_transient(transient))
593    }
594
595    /// The wire form of this error.
596    pub fn report(&self) -> ErrorReport {
597        ErrorReport::from(self)
598    }
599
600    /// Which boundary produced this error, by its classification.
601    pub(crate) fn boundary(&self) -> AdapterErrorBoundary {
602        match (self, self.kind()) {
603            (Self::Http(error), _) => AdapterErrorBoundary::from_http(error),
604            (Self::Stalled(_), _) => AdapterErrorBoundary::Transport,
605            (_, ErrorKind::Json | ErrorKind::Response) => AdapterErrorBoundary::Decode,
606            (_, ErrorKind::Provider | ErrorKind::ProviderResponse) => {
607                AdapterErrorBoundary::ProviderResponse
608            }
609            _ => AdapterErrorBoundary::Request,
610        }
611    }
612
613    fn map_response(
614        self,
615        map: impl FnOnce(ProviderResponseError) -> ProviderResponseError,
616    ) -> Self {
617        match self {
618            Self::ProviderResponse(response) => Self::ProviderResponse(map(response)),
619            Self::InvalidAuthentication(response) => Self::InvalidAuthentication(map(response)),
620            Self::CacheExpired { name, response } => Self::CacheExpired {
621                name,
622                response: map(response),
623            },
624            // The report restates what it read from the reply it preserves,
625            // so what the map fills reaches both. What it held is kept.
626            Self::Relayed(mut report) => {
627                if let Some(response) = report.provider_response.take() {
628                    let response = map(response);
629                    report.request_id = report
630                        .request_id
631                        .or_else(|| response.provider_request_id.clone());
632                    report.http_status = report
633                        .http_status
634                        .or_else(|| response.status.map(|status| status.as_u16()));
635                    report.code = report.code.or_else(|| response.machine_code());
636                    report.refusal |= response.refusal;
637                    report.provider_response = Some(response);
638                }
639                Self::Relayed(report)
640            }
641            other => other,
642        }
643    }
644}
645
646impl From<serde_json::Error> for ProviderError {
647    fn from(error: serde_json::Error) -> Self {
648        Self::Json(Arc::new(error))
649    }
650}
651
652impl From<BoxError> for ProviderError {
653    fn from(error: BoxError) -> Self {
654        Self::Request(Arc::from(error))
655    }
656}
657
658impl From<http_client::Error> for ProviderError {
659    fn from(error: http_client::Error) -> Self {
660        Self::from_transport_error(error)
661    }
662}
663
664/// A failure to build a provider request, returned by
665/// [`Wire::encode`](crate::wire::Wire::encode). It converts only into
666/// [`ProviderError::Request`] or [`ProviderError::UnsupportedOption`], both
667/// of kind [`ErrorKind::Request`], so every provider classifies its encode
668/// failures alike.
669#[non_exhaustive]
670#[derive(Debug, thiserror::Error)]
671#[error(transparent)]
672pub struct EncodeError(ProviderError);
673
674impl EncodeError {
675    /// A request that could not be built, for the given reason.
676    pub fn request(reason: impl Into<BoxError>) -> Self {
677        Self(ProviderError::request(reason))
678    }
679
680    /// A request that sets an option the wire or model cannot honour.
681    pub fn unsupported(option: UnsupportedOption) -> Self {
682        Self(ProviderError::UnsupportedOption(option))
683    }
684
685    /// The refused option, when this error is a refusal.
686    pub fn unsupported_option(&self) -> Option<&UnsupportedOption> {
687        match &self.0 {
688            ProviderError::UnsupportedOption(option) => Some(option),
689            _ => None,
690        }
691    }
692}
693
694impl From<EncodeError> for ProviderError {
695    fn from(error: EncodeError) -> Self {
696        debug_assert_eq!(error.0.kind(), ErrorKind::Request);
697        error.0
698    }
699}
700
701impl From<http::Error> for EncodeError {
702    fn from(error: http::Error) -> Self {
703        Self::request(error)
704    }
705}
706
707impl From<serde_json::Error> for EncodeError {
708    fn from(error: serde_json::Error) -> Self {
709        Self::request(error)
710    }
711}
712
713impl From<crate::message::MessageError> for EncodeError {
714    fn from(error: crate::message::MessageError) -> Self {
715        Self::request(error)
716    }
717}
718
719impl From<BoxError> for EncodeError {
720    fn from(error: BoxError) -> Self {
721        Self(ProviderError::from(error))
722    }
723}
724
725impl From<http::Error> for ProviderError {
726    fn from(error: http::Error) -> Self {
727        Self::request(error)
728    }
729}
730
731impl From<&ProviderError> for ErrorReport {
732    fn from(error: &ProviderError) -> Self {
733        if let ProviderError::Relayed(report) = error {
734            return (**report).clone();
735        }
736        let response = error.provider_response();
737        ErrorReport {
738            kind: error.kind(),
739            retryable: error.is_retryable(),
740            message: error.to_string(),
741            code: response.and_then(ProviderResponseError::machine_code),
742            http_status: response
743                .and_then(|response| response.status)
744                .map(|status| status.as_u16()),
745            refusal: response.is_some_and(|response| response.refusal),
746            source_chain: source_chain(error),
747            request_id: response.and_then(|response| response.provider_request_id.clone()),
748            provider_response: response.cloned(),
749        }
750    }
751}
752
753impl From<ProviderError> for ErrorReport {
754    fn from(error: ProviderError) -> Self {
755        Self::from(&error)
756    }
757}
758
759impl ToolExecutionError {
760    /// The wire form of this error.
761    pub fn report(&self) -> ErrorReport {
762        ErrorReport::from(self)
763    }
764}
765
766impl ToolExecutionError {
767    /// Whether the tool may reasonably be re-run: the explicit override when
768    /// one was set, else the kind's own default
769    /// ([`ToolErrorKind::default_retryable`]); a kind that leaves it to the
770    /// tool (`None`) is not retryable on the wire.
771    pub fn is_retryable(&self) -> bool {
772        self.retryable()
773            .or_else(|| self.kind().default_retryable())
774            .unwrap_or(false)
775    }
776}
777
778impl From<&ToolExecutionError> for ErrorReport {
779    fn from(error: &ToolExecutionError) -> Self {
780        ErrorReport {
781            kind: ErrorKind::Tool(error.kind()),
782            retryable: error.is_retryable(),
783            message: error.message().to_string(),
784            code: error.code().map(str::to_string),
785            http_status: error.http_status(),
786            refusal: error.is_refusal(),
787            source_chain: source_chain(error),
788            request_id: None,
789            provider_response: None,
790        }
791    }
792}
793
794impl From<ToolExecutionError> for ErrorReport {
795    fn from(error: ToolExecutionError) -> Self {
796        Self::from(&error)
797    }
798}
799
800/// Rebuilds a tool failure from a dispatch report.
801///
802/// An [`ErrorKind::Tool`] report keeps its tool kind; other kinds map to the
803/// closest [`ToolErrorKind`]. Retryability, code, HTTP status, and refusal are
804/// copied, the message is model-visible, and the report is the `source()`.
805impl From<ErrorReport> for ToolExecutionError {
806    fn from(report: ErrorReport) -> Self {
807        let kind = match report.kind {
808            ErrorKind::Tool(kind) => kind,
809            ErrorKind::Timeout => ToolErrorKind::Timeout,
810            ErrorKind::Cancelled => ToolErrorKind::Cancelled,
811            ErrorKind::Denied => ToolErrorKind::PermissionDenied,
812            ErrorKind::HandlerUnavailable => ToolErrorKind::NotFound,
813            ErrorKind::Http => ToolErrorKind::Network,
814            ErrorKind::Provider | ErrorKind::ProviderResponse => ToolErrorKind::Provider,
815            ErrorKind::Json
816            | ErrorKind::Url
817            | ErrorKind::Request
818            | ErrorKind::Response
819            | ErrorKind::Internal
820            | ErrorKind::Divergence
821            | ErrorKind::Other => ToolErrorKind::Other,
822        };
823        let mut error = if report.refusal {
824            ToolExecutionError::refused(report.message.clone())
825        } else {
826            ToolExecutionError::new(kind, report.message.clone())
827        }
828        .with_retryable(report.retryable);
829        if let Some(code) = &report.code {
830            error = error.with_code(code.clone());
831        }
832        if let Some(status) = report.http_status {
833            error = error.with_http_status(status);
834        }
835        error.with_source(report)
836    }
837}
838
839impl From<&VectorStoreError> for ErrorReport {
840    fn from(error: &VectorStoreError) -> Self {
841        let (kind, provider_response) = match error {
842            VectorStoreError::EmbeddingError(inner) => {
843                return Self {
844                    message: error.to_string(),
845                    source_chain: source_chain(error),
846                    ..Self::from(inner)
847                };
848            }
849            VectorStoreError::JsonError(_) => (ErrorKind::Json, None),
850            VectorStoreError::DatastoreError(_) => (ErrorKind::Provider, None),
851            VectorStoreError::FilterError(_) | VectorStoreError::SamplesOutOfRange { .. } => {
852                (ErrorKind::Request, None)
853            }
854            VectorStoreError::MissingIdError(_) => (ErrorKind::Response, None),
855            VectorStoreError::Http(crate::http_client::Error::InvalidStatusCodeWithDetails {
856                status,
857                body,
858                headers,
859            }) => (
860                ErrorKind::ProviderResponse,
861                Some(
862                    crate::provider_response::ProviderResponseError::new(*status, body.clone())
863                        .with_headers(Some(headers.clone())),
864                ),
865            ),
866            VectorStoreError::Http(_) => (ErrorKind::Http, None),
867            // A store's own non-2xx reply: the server answered, so the status
868            // classifies it and its retryability, like any provider reply.
869            VectorStoreError::ExternalAPIError(status, body) => (
870                ErrorKind::ProviderResponse,
871                Some(crate::provider_response::ProviderResponseError::new(
872                    *status,
873                    body.clone(),
874                )),
875            ),
876        };
877        let http_status = provider_response
878            .as_ref()
879            .and_then(|response| response.status.map(|status| status.as_u16()));
880        let retryable = match error {
881            VectorStoreError::Http(inner) => transient_transport(inner),
882            VectorStoreError::ExternalAPIError(..) => retryable_status(http_status),
883            _ => false,
884        };
885        let code = provider_response
886            .as_ref()
887            .and_then(|response| response.machine_code());
888        ErrorReport {
889            kind,
890            retryable,
891            message: error.to_string(),
892            code,
893            http_status,
894            refusal: provider_response.as_ref().is_some_and(|r| r.refusal),
895            source_chain: source_chain(error),
896            request_id: None,
897            provider_response,
898        }
899    }
900}
901
902impl From<VectorStoreError> for ErrorReport {
903    fn from(error: VectorStoreError) -> Self {
904        Self::from(&error)
905    }
906}
907
908// The report is the wire error of the effect protocol: it must cross threads
909// and serialize on every target, browser wasm included.
910const _: fn() = || {
911    fn assert_wire<T: Send + Sync + 'static + Serialize + serde::de::DeserializeOwned>() {}
912    assert_wire::<ErrorReport>();
913    assert_wire::<ErrorKind>();
914};
915
916#[cfg(test)]
917mod encode_tests;
918#[cfg(test)]
919mod tests;