use std::time::Duration;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorResponse {
pub error: ApiError,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiError {
pub message: String,
#[serde(rename = "type")]
pub error_type: String,
#[serde(default)]
pub param: Option<String>,
#[serde(default)]
pub code: Option<String>,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum LiterLlmError {
#[error("authentication failed: {message}")]
Authentication { message: String },
#[error("rate limited: {message}")]
RateLimited {
message: String,
retry_after: Option<Duration>,
},
#[error("bad request: {message}")]
BadRequest { message: String },
#[error("context window exceeded: {message}")]
ContextWindowExceeded { message: String },
#[error("content policy violation: {message}")]
ContentPolicy { message: String },
#[error("not found: {message}")]
NotFound { message: String },
#[error("server error: {message}")]
ServerError { message: String },
#[error("service unavailable: {message}")]
ServiceUnavailable { message: String },
#[error("request timeout")]
Timeout,
#[cfg(any(feature = "native-http", feature = "wasm-http"))]
#[error(transparent)]
Network(#[from] reqwest::Error),
#[error("streaming error: {message}")]
Streaming { message: String },
#[error("provider {provider} does not support {endpoint}")]
EndpointNotSupported { endpoint: String, provider: String },
#[error("invalid header {name:?}: {reason}")]
InvalidHeader { name: String, reason: String },
#[error("serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("budget exceeded: {message}")]
BudgetExceeded { message: String, model: Option<String> },
#[error("hook rejected: {message}")]
HookRejected { message: String },
#[error("internal error: {message}")]
InternalError { message: String },
}
impl LiterLlmError {
#[must_use]
pub fn is_transient(&self) -> bool {
match self {
Self::RateLimited { .. } | Self::ServiceUnavailable { .. } | Self::Timeout | Self::ServerError { .. } => {
true
}
#[cfg(any(feature = "native-http", feature = "wasm-http"))]
Self::Network(_) => true,
_ => false,
}
}
#[must_use]
pub fn error_type(&self) -> &'static str {
match self {
Self::Authentication { .. } => "Authentication",
Self::RateLimited { .. } => "RateLimited",
Self::BadRequest { .. } => "BadRequest",
Self::ContextWindowExceeded { .. } => "ContextWindowExceeded",
Self::ContentPolicy { .. } => "ContentPolicy",
Self::NotFound { .. } => "NotFound",
Self::ServerError { .. } => "ServerError",
Self::ServiceUnavailable { .. } => "ServiceUnavailable",
Self::Timeout => "Timeout",
#[cfg(any(feature = "native-http", feature = "wasm-http"))]
Self::Network(_) => "Network",
Self::Streaming { .. } => "Streaming",
Self::EndpointNotSupported { .. } => "EndpointNotSupported",
Self::InvalidHeader { .. } => "InvalidHeader",
Self::Serialization(_) => "Serialization",
Self::BudgetExceeded { .. } => "BudgetExceeded",
Self::HookRejected { .. } => "HookRejected",
Self::InternalError { .. } => "InternalError",
}
}
pub fn from_status(status: u16, body: &str, retry_after: Option<Duration>) -> Self {
let parsed = serde_json::from_str::<ErrorResponse>(body).ok();
let code = parsed.as_ref().and_then(|r| r.error.code.clone());
let message = parsed.map(|r| r.error.message).unwrap_or_else(|| body.to_string());
match status {
401 | 403 => Self::Authentication { message },
429 => Self::RateLimited { message, retry_after },
400 | 422 => {
if code.as_deref() == Some("context_length_exceeded") {
Self::ContextWindowExceeded { message }
} else if code.as_deref() == Some("content_policy_violation")
|| code.as_deref() == Some("content_filter")
{
Self::ContentPolicy { message }
}
else if message.contains("context_length_exceeded")
|| message.contains("context window")
|| message.contains("maximum context length")
{
Self::ContextWindowExceeded { message }
} else if message.contains("content_policy") || message.contains("content_filter") {
Self::ContentPolicy { message }
} else {
Self::BadRequest { message }
}
}
404 => Self::NotFound { message },
405 | 413 => Self::BadRequest { message },
408 => Self::Timeout,
500 => Self::ServerError { message },
502..=504 => Self::ServiceUnavailable { message },
400..=499 => Self::BadRequest { message },
_ => Self::ServerError { message },
}
}
}
pub type Result<T> = std::result::Result<T, LiterLlmError>;