use reqwest::{header::HeaderMap, StatusCode};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use thiserror::Error;
#[derive(Debug, Clone, Error)]
#[error("JSON serialization error: {0}")]
pub struct SerdeError(String);
impl From<serde_json::Error> for SerdeError {
fn from(err: serde_json::Error) -> Self {
SerdeError(err.to_string())
}
}
#[derive(Debug, Clone, Error)]
#[error("HTTP transport error: {0}")]
pub struct TransportError(String);
impl From<reqwest::Error> for TransportError {
fn from(err: reqwest::Error) -> Self {
TransportError(err.to_string())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GroqApiErrorDetails {
pub message: String,
#[serde(rename = "type")]
pub error_type: Option<String>,
pub code: Option<String>,
pub param: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GroqApiError {
#[serde(skip)]
pub status: StatusCode,
pub error: GroqApiErrorDetails,
#[serde(skip)]
pub retry_after: Option<Duration>,
}
impl GroqApiError {
pub fn from_response(status: StatusCode, body: String, headers: &HeaderMap) -> Self {
let retry_after = headers
.get("retry-after")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse::<u64>().ok())
.map(Duration::from_secs);
let error_details = serde_json::from_str::<serde_json::Value>(&body)
.ok()
.and_then(|v| v.get("error").cloned())
.and_then(|e| serde_json::from_value(e).ok())
.unwrap_or_else(|| GroqApiErrorDetails {
message: body,
error_type: None,
code: None,
param: None,
});
Self {
status,
error: error_details,
retry_after,
}
}
}
impl std::fmt::Display for GroqApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Groq API error ({}): {}", self.status, self.error.message)
}
}
impl std::error::Error for GroqApiError {}
#[derive(Debug, Clone, Error)]
pub enum GroqError {
#[error("Invalid API key: {0}")]
InvalidApiKey(String),
#[error("Invalid message: {0}")]
InvalidMessage(String),
#[error("Rate limited - too many requests")]
RateLimited,
#[error("API error: {0}")]
Api(#[from] GroqApiError),
#[error("Transport error: {0}")]
Transport(#[from] TransportError),
#[error("Serialization error: {0}")]
Serde(#[from] SerdeError),
#[error("URL parse error: {0}")]
UrlParse(#[from] url::ParseError),
#[error("Backoff error: {0}")]
Backoff(String),
}
impl From<serde_json::Error> for GroqError {
fn from(err: serde_json::Error) -> Self {
GroqError::Serde(SerdeError::from(err))
}
}
impl From<reqwest::Error> for GroqError {
fn from(err: reqwest::Error) -> Self {
GroqError::Transport(TransportError::from(err))
}
}
impl<E> From<backoff::Error<E>> for GroqError
where
E: Into<GroqError>,
{
fn from(err: backoff::Error<E>) -> Self {
match err {
backoff::Error::Permanent(e) => e.into(),
backoff::Error::Transient { err, .. } => err.into(),
}
}
}
impl GroqError {
pub fn is_retryable(&self) -> bool {
matches!(self, GroqError::RateLimited | GroqError::Transport(_))
}
pub fn is_rate_limited(&self) -> bool {
matches!(self, GroqError::RateLimited)
}
pub fn is_auth_error(&self) -> bool {
matches!(self, GroqError::InvalidApiKey(_))
}
}