error.rs
Language Rust3.7 KB94 lines
//! Provider failures, mapped to what a person can act on.//!//! There is no automatic retry anywhere: a retry is another billed request,//! so the UI offers one and the person decides.use serde::Serialize;#[derive(Debug, Clone, PartialEq, thiserror::Error)]pub enum ProviderError {#[error("The API key was rejected. Check it in Settings.")]InvalidKey,#[error("The account is out of credit.")]InsufficientBalance,#[error("Rate limited by the provider{}.", retry_after.map(|s| format!("; try again in {s} s")).unwrap_or_default())]RateLimited { retry_after: Option<u64> },#[error("The provider had an error ({status}): {message}")]Upstream { status: u16, message: String },#[error("The request was refused ({status}): {message}")]BadRequest { status: u16, message: String },#[error("Could not reach {url}. Is the server running?")]Unreachable { url: String },#[error("The stream stopped with an error: {0}")]Stream(String),#[error("{0}")]Other(String),}impl ProviderError {/// A stable machine name, stored with a failed message and used by the UI.pub fn kind(&self) -> &'static str {match self {Self::InvalidKey => "invalid_key",Self::InsufficientBalance => "insufficient_balance",Self::RateLimited { .. } => "rate_limited",Self::Upstream { .. } => "upstream",Self::BadRequest { .. } => "bad_request",Self::Unreachable { .. } => "unreachable",Self::Stream(_) => "stream",Self::Other(_) => "other",}}/// Map an HTTP error response. `body` is the response text, if any.pub fn from_status(status: u16, body: &str, retry_after: Option<u64>) -> Self {let message = error_message(body).unwrap_or_else(|| body.chars().take(300).collect());match status {401 | 403 => Self::InvalidKey,402 => Self::InsufficientBalance,429 => Self::RateLimited { retry_after },500..=599 => Self::Upstream { status, message },_ => Self::BadRequest { status, message },}}}/// `{"error": {"message": ...}}`, `{"error": "..."}` or `{"detail": ...}`.pub fn error_message(body: &str) -> Option<String> {let v: serde_json::Value = serde_json::from_str(body).ok()?;let e = v.get("error").or_else(|| v.get("detail"))?;match e {serde_json::Value::String(s) => Some(s.clone()),serde_json::Value::Object(o) => o.get("message").and_then(|m| m.as_str()).map(str::to_string),_ => None,}}impl Serialize for ProviderError {fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {use serde::ser::SerializeStruct;let mut st = s.serialize_struct("ProviderError", 2)?;st.serialize_field("kind", self.kind())?;st.serialize_field("message", &self.to_string())?;st.end()}}#[cfg(test)]mod tests {use super::*;#[test]fn maps_statuses() {let di = r#"{"error":{"message":"Invalid API key","type":"invalid_request_error","code":"invalid_api_key"}}"#;assert_eq!(ProviderError::from_status(401, di, None), ProviderError::InvalidKey);assert_eq!(ProviderError::from_status(402, "", None).kind(), "insufficient_balance");let r = ProviderError::from_status(429, "{}", Some(7));assert_eq!(r.to_string(), "Rate limited by the provider; try again in 7 s.");assert_eq!(ProviderError::from_status(503, r#"{"detail":"overloaded"}"#, None),ProviderError::Upstream { status: 503, message: "overloaded".into() });assert_eq!(ProviderError::from_status(400, r#"{"error":"bad model"}"#, None).kind(), "bad_request");}}