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