Skip to main content

rig_core/
provider_response.rs

1//! Preserved provider error responses and their machine codes.
2//!
3//! ```
4//! use rig_core::ProviderResponseError;
5//!
6//! let reply = ProviderResponseError::new(http::StatusCode::TOO_MANY_REQUESTS, "slow down");
7//! assert!(reply.is_retryable());
8//! ```
9use http::StatusCode;
10
11/// A raw provider error body with captured transport metadata.
12/// Callers must supply the provider's actual payload, not a generated diagnostic.
13/// Serialization omits headers and route; deserialization restores them as `None`.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct ProviderResponseError {
16    /// HTTP status of the provider response, when it was captured alongside the body.
17    pub status: Option<StatusCode>,
18    /// Raw response body as returned by the provider.
19    pub body: String,
20    /// Transport request ID from response headers or SDK metadata, when captured.
21    pub provider_request_id: Option<String>,
22    /// Captured response headers, including rate-limit metadata. `None` means
23    /// not captured, rather than an empty header set. Omitted during serialization.
24    pub headers: Option<http::HeaderMap>,
25    /// The provider's own machine-readable code for the failure, when the
26    /// transport reported one apart from the body: a gRPC status code name
27    /// (`UNAVAILABLE`), an AWS exception type (`ThrottlingException`).
28    /// `None` when the reply carried only a status and a body.
29    pub code: Option<String>,
30    /// Transport retry hint, used when neither a non-success status nor a
31    /// known provider code decides; refusals are never retryable.
32    pub transient: Option<bool>,
33    /// Whether this error represents a content refusal. Refusals are never
34    /// retryable; refusal content in a successful model answer is separate.
35    pub refusal: bool,
36    /// The provider and request path the reply answered, when the operation
37    /// records them for diagnostics (model listing does). Omitted during
38    /// serialization.
39    pub route: Option<String>,
40}
41
42impl ProviderResponseError {
43    /// Preserve a provider error response captured with its HTTP status.
44    pub fn new(status: StatusCode, body: impl Into<String>) -> Self {
45        let status = Some(status);
46        Self {
47            status,
48            ..Self::without_status(body)
49        }
50    }
51
52    /// Preserve a provider error body that has no HTTP status (gRPC / SDK
53    /// transports).
54    pub fn without_status(body: impl Into<String>) -> Self {
55        Self {
56            status: None,
57            body: body.into(),
58            provider_request_id: None,
59            headers: None,
60            code: None,
61            transient: None,
62            refusal: false,
63            route: None,
64        }
65    }
66
67    /// Preserve a provider error body delivered with no HTTP status of its
68    /// own: an error event inside a stream, or a non-HTTP transport's reply.
69    /// An error envelope that nests an HTTP status as its numeric code
70    /// (`{"error":{"code":503}}`) records it as the status, as the same
71    /// envelope on a non-success response would carry it.
72    pub fn from_body(body: impl Into<String>) -> Self {
73        let mut reply = Self::without_status(body);
74        let status = reply.located(|envelope| envelope.http_status().filter(|_| envelope.nested));
75        reply.status = status.and_then(|status| StatusCode::from_u16(status).ok());
76        reply
77    }
78
79    /// Mark the reply as the provider's verdict on the content: a refusal,
80    /// final, never retried.
81    pub fn with_refusal(mut self, refusal: bool) -> Self {
82        self.refusal = refusal;
83        self
84    }
85
86    /// Attach the HTTP status a transport reported beside a reply that was
87    /// first preserved without one (an SDK that hands back the raw HTTP
88    /// response next to its typed exception). A status already set is kept.
89    pub fn with_status(mut self, status: Option<StatusCode>) -> Self {
90        if self.status.is_none() {
91            self.status = status;
92        }
93        self
94    }
95
96    /// Attach the provider's own machine-readable code for the failure.
97    pub fn with_code(mut self, code: Option<String>) -> Self {
98        self.code = code.filter(|code| !code.is_empty());
99        self
100    }
101
102    /// Replaces the transport retry hint. A refusal, a non-success HTTP
103    /// status, and a provider code the table knows each outrank it.
104    pub fn with_transient(mut self, transient: Option<bool>) -> Self {
105        self.transient = transient;
106        self
107    }
108
109    /// Whether the same request may reasonably be sent again, decided in
110    /// this order: a refusal never is; a non-success HTTP status decides
111    /// through [`crate::error::retryable_status`]; then a provider code the
112    /// one table knows (the explicit [`Self::code`], else the code the body's
113    /// error envelope names); then the `transient` hint; otherwise not.
114    ///
115    /// The body is read on every call, so the same envelope gets the same
116    /// verdict from every decoder and transport, streamed or unary.
117    pub fn is_retryable(&self) -> bool {
118        if self.refusal {
119            return false;
120        }
121        if let Some(status) = self.status.filter(|status| !status.is_success()) {
122            return crate::error::retryable_status(Some(status.as_u16()));
123        }
124        let known = match &self.code {
125            Some(code) => Envelope::default().named(code).verdict(),
126            None => self.located(|envelope| envelope.verdict()),
127        };
128        known.or(self.transient).unwrap_or(false)
129    }
130
131    /// Returns the explicit code, else the code the body's error envelope
132    /// names: its first nonempty string `code`, `status` or `type`, or else
133    /// its numeric code in decimal (`"429"`).
134    pub fn machine_code(&self) -> Option<String> {
135        self.code
136            .clone()
137            .or_else(|| self.located(|envelope| envelope.code()))
138    }
139
140    /// The error the body's envelope holds, as `{"error": ...}`, or the
141    /// error event itself; `None` when the body carries no envelope.
142    pub fn envelope_json(&self) -> Option<serde_json::Value> {
143        let doc: serde_json::Value = serde_json::from_str(&self.body).ok()?;
144        let envelope = envelope(&doc)?;
145        let error = envelope.error?.clone();
146        Some(match envelope.nested {
147            true => serde_json::json!({ "error": error }),
148            false => error,
149        })
150    }
151
152    /// `f` applied to the error the body carries, as [`delivered`] finds it.
153    fn located<T>(&self, f: impl FnOnce(Envelope<'_>) -> Option<T>) -> Option<T> {
154        let doc: serde_json::Value = serde_json::from_str(&self.body).ok()?;
155        delivered(&doc).and_then(f)
156    }
157
158    /// Attach the transport request id the failed response reported.
159    pub fn with_provider_request_id(mut self, request_id: Option<String>) -> Self {
160        self.provider_request_id = request_id.filter(|id| !id.is_empty());
161        self
162    }
163
164    /// Replaces captured response headers, including rate-limit metadata.
165    pub fn with_headers(mut self, headers: Option<http::HeaderMap>) -> Self {
166        self.headers = headers;
167        self
168    }
169}
170
171impl std::fmt::Display for ProviderResponseError {
172    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173        match self.status {
174            Some(status) => write!(f, "status {status}: {}", self.body)?,
175            None => write!(f, "{}", self.body)?,
176        }
177        // The id support asks for belongs in the message a caller logs.
178        if let Some(request_id) = &self.provider_request_id {
179            write!(f, " (request id: {request_id})")?;
180        }
181        if let Some(route) = &self.route {
182            write!(f, " [{route}]")?;
183        }
184        Ok(())
185    }
186}
187
188impl std::error::Error for ProviderResponseError {}
189
190/// Serialized error metadata with numeric HTTP status and no headers.
191/// Volatile transport headers are excluded to keep replay records stable.
192#[derive(serde::Serialize, serde::Deserialize)]
193#[serde(deny_unknown_fields)]
194struct ProviderResponseErrorWire {
195    status: Option<u16>,
196    body: String,
197    provider_request_id: Option<String>,
198    #[serde(default, skip_serializing_if = "Option::is_none")]
199    code: Option<String>,
200    #[serde(default, skip_serializing_if = "Option::is_none")]
201    transient: Option<bool>,
202    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
203    refusal: bool,
204}
205
206impl serde::Serialize for ProviderResponseError {
207    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
208        ProviderResponseErrorWire {
209            status: self.status.map(|status| status.as_u16()),
210            body: self.body.clone(),
211            provider_request_id: self.provider_request_id.clone(),
212            code: self.code.clone(),
213            transient: self.transient,
214            refusal: self.refusal,
215        }
216        .serialize(serializer)
217    }
218}
219
220impl<'de> serde::Deserialize<'de> for ProviderResponseError {
221    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
222        use serde::de::Error as _;
223        let wire = ProviderResponseErrorWire::deserialize(deserializer)?;
224        let status = wire
225            .status
226            .map(StatusCode::from_u16)
227            .transpose()
228            .map_err(D::Error::custom)?;
229        Ok(Self {
230            status,
231            body: wire.body,
232            provider_request_id: wire.provider_request_id,
233            headers: None,
234            code: wire.code,
235            transient: wire.transient,
236            refusal: wire.refusal,
237            route: None,
238        })
239    }
240}
241
242/// Where a provider put its error, and the code it gave it: the first
243/// nonempty string and the first integer among its code fields.
244#[derive(Default)]
245struct Envelope<'a> {
246    /// The error value itself: an object, a message string, or the event.
247    error: Option<&'a serde_json::Value>,
248    name: Option<&'a str>,
249    number: Option<i64>,
250    /// The error nests in the document (`error`, `response.error`) rather
251    /// than being the document itself, an `error` event.
252    nested: bool,
253}
254
255/// The error `doc` carries, where [`envelope`] locates it, and whether it
256/// nests in the document rather than being the document itself.
257pub(crate) fn located_error(doc: &serde_json::Value) -> Option<(&serde_json::Value, bool)> {
258    envelope(doc).and_then(|envelope| Some((envelope.error?, envelope.nested)))
259}
260
261/// Codes naming a transient condition, in any case, from every transport:
262/// HTTP envelopes, gRPC status names, AWS exception types.
263const TRANSIENT: &[&str] = &[
264    "overloaded",
265    "overloaded_error",
266    "api_error",
267    "rate_limit_error",
268    "rate_limit_exceeded",
269    "too_many_requests",
270    "server_error",
271    "server_is_overloaded",
272    "slow_down",
273    "unavailable",
274    "resource_exhausted",
275    "deadline_exceeded",
276    "aborted",
277    "ThrottlingException",
278    "ServiceUnavailableException",
279    "InternalServerException",
280    "ModelNotReadyException",
281    "ModelTimeoutException",
282    "ModelStreamErrorException",
283];
284
285/// Codes naming a condition the same request meets again.
286const FINAL: &[&str] = &[
287    "insufficient_quota",
288    "quota_exceeded",
289    "billing_hard_limit_reached",
290    "context_length_exceeded",
291    "invalid_prompt",
292];
293
294/// The one reader of where a provider puts its error. In order: the
295/// Responses `response.failed` nesting (`response.error`); a top-level
296/// `error` object, or a nonempty `error` string (a message, no code); a
297/// document that is itself an `error` event, whose code is its `code`
298/// alone, since its `type` is the event tag. `null`, `{}` and `""` are no
299/// envelope. Whether a frame is an error at all stays its decoder's call.
300fn envelope(doc: &serde_json::Value) -> Option<Envelope<'_>> {
301    use serde_json::Value;
302    const FIELDS: &[&str] = &["code", "status", "type"];
303    let object = |value: &&Value| value.as_object().is_some_and(|fields| !fields.is_empty());
304    if let Some(error) = doc.pointer("/response/error").filter(object) {
305        return Some(Envelope::of(error, FIELDS, true));
306    }
307    match doc.get("error") {
308        Some(error) if object(&error) => Some(Envelope::of(error, FIELDS, true)),
309        Some(error @ Value::String(message)) if !message.is_empty() => {
310            Some(Envelope::of(error, &[], true))
311        }
312        _ => (doc.get("type").and_then(Value::as_str) == Some("error"))
313            .then(|| Envelope::of(doc, &["code"], false)),
314    }
315}
316
317/// The error a body already known to be one carries, for its verdict and
318/// code: where [`envelope`] locates it; in a JSON array (Gemini's non-SSE
319/// stream), the first element that carries one; else the document read as
320/// a bare error object, as a decoder that handed over the inner error
321/// (`{"type":"overloaded_error","message":".."}`) leaves it. A bare error
322/// has a nonempty string `message` and no `error` or `response` of its own,
323/// and its code is its `code`, `status` or `type`.
324fn delivered(doc: &serde_json::Value) -> Option<Envelope<'_>> {
325    if let Some(items) = doc.as_array() {
326        return items.iter().find_map(delivered);
327    }
328    let message = doc.get("message").and_then(serde_json::Value::as_str);
329    let bare = message.is_some_and(|message| !message.is_empty())
330        && doc.get("error").is_none()
331        && doc.get("response").is_none();
332    envelope(doc).or_else(|| bare.then(|| Envelope::of(doc, &["code", "status", "type"], false)))
333}
334
335impl<'a> Envelope<'a> {
336    fn named(mut self, name: &'a str) -> Self {
337        self.name = Some(name);
338        self
339    }
340
341    fn of(error: &'a serde_json::Value, fields: &[&str], nested: bool) -> Self {
342        let values = || fields.iter().filter_map(|field| error.get(field));
343        Self {
344            error: Some(error),
345            name: values().find_map(|value| value.as_str().filter(|name| !name.is_empty())),
346            number: values().find_map(serde_json::Value::as_i64),
347            nested,
348        }
349    }
350
351    /// The name when there is one, as it says more than a number.
352    fn code(&self) -> Option<String> {
353        let number = || self.number.map(|number| number.to_string());
354        self.name.map(str::to_owned).or_else(number)
355    }
356
357    /// The one provider code → retry verdict table, beside
358    /// [`crate::error::retryable_status`]: what the name says, else what
359    /// the number says read as an HTTP status, else nothing. gRPC
360    /// `INTERNAL` stays out, as its transports have always left it.
361    fn verdict(&self) -> Option<bool> {
362        let named = |codes: &[&str]| {
363            let name = self.name.unwrap_or_default();
364            codes.iter().any(|code| code.eq_ignore_ascii_case(name))
365        };
366        let status = || {
367            self.http_status()
368                .map(|s| crate::error::retryable_status(Some(s)))
369        };
370        (named(TRANSIENT).then_some(true))
371            .or_else(|| named(FINAL).then_some(false))
372            .or_else(status)
373    }
374
375    fn http_status(&self) -> Option<u16> {
376        let number = u16::try_from(self.number?).ok();
377        number.filter(|number| (400..=599).contains(number))
378    }
379}
380
381/// An id or model name as a response carries it: an empty one is none.
382pub(crate) fn reported(id: Option<String>) -> Option<String> {
383    id.filter(|id| !id.is_empty())
384}
385
386/// Parses an optional response body as JSON.
387///
388/// Returns:
389/// - `Ok(Some(value))` when a body is present and valid JSON.
390/// - `Ok(None)` when the body is absent or empty.
391/// - `Err(error)` when a body is present but isn't valid JSON.
392pub(crate) fn json(body: Option<&str>) -> Result<Option<serde_json::Value>, serde_json::Error> {
393    body.filter(|body| !body.is_empty())
394        .map(serde_json::from_str)
395        .transpose()
396}
397
398#[cfg(test)]
399mod tests;