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