use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::time::Duration;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum SerdesAiError {
#[error(transparent)]
AgentRun(#[from] AgentRunError),
#[error(transparent)]
ModelRetry(#[from] ModelRetry),
#[error(transparent)]
ModelApi(#[from] ModelApiError),
#[error(transparent)]
ModelHttp(#[from] ModelHttpError),
#[error(transparent)]
User(#[from] UserError),
#[error(transparent)]
UsageLimit(#[from] UsageLimitExceeded),
#[error(transparent)]
UnexpectedBehavior(#[from] UnexpectedModelBehavior),
#[error(transparent)]
ToolRetry(#[from] ToolRetryError),
#[error(transparent)]
ApprovalRequired(#[from] ApprovalRequired),
#[error(transparent)]
CallDeferred(#[from] CallDeferred),
#[error(transparent)]
IncompleteToolCall(#[from] IncompleteToolCall),
#[error(transparent)]
FallbackGroup(#[from] FallbackExceptionGroup),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Configuration error: {0}")]
Configuration(String),
#[error("Internal error: {0}")]
Internal(String),
}
pub type Result<T> = std::result::Result<T, SerdesAiError>;
#[derive(Error, Debug, Clone)]
pub struct AgentRunError {
pub message: String,
pub run_id: Option<String>,
pub attempts: u32,
pub cause: Option<String>,
}
impl fmt::Display for AgentRunError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Agent run error: {}", self.message)?;
if let Some(ref run_id) = self.run_id {
write!(f, " (run_id: {})", run_id)?;
}
if self.attempts > 1 {
write!(f, " after {} attempts", self.attempts)?;
}
Ok(())
}
}
impl AgentRunError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
run_id: None,
attempts: 1,
cause: None,
}
}
pub fn with_run_id(mut self, run_id: impl Into<String>) -> Self {
self.run_id = Some(run_id.into());
self
}
pub fn with_attempts(mut self, attempts: u32) -> Self {
self.attempts = attempts;
self
}
pub fn with_cause(mut self, cause: impl Into<String>) -> Self {
self.cause = Some(cause.into());
self
}
}
#[derive(Error, Debug, Clone, Serialize, Deserialize)]
pub struct ModelRetry {
pub message: String,
}
impl fmt::Display for ModelRetry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Model retry requested: {}", self.message)
}
}
impl ModelRetry {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
#[derive(Error, Debug, Clone)]
pub struct ModelApiError {
pub status_code: u16,
pub body: String,
pub headers: HashMap<String, String>,
pub message: Option<String>,
pub error_code: Option<String>,
pub retryable: bool,
pub retry_after: Option<u64>,
}
impl fmt::Display for ModelApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Model API error (status {})", self.status_code)?;
if let Some(ref msg) = self.message {
write!(f, ": {}", msg)?;
}
if let Some(ref code) = self.error_code {
write!(f, " [{}]", code)?;
}
Ok(())
}
}
impl ModelApiError {
pub fn new(status_code: u16, body: impl Into<String>) -> Self {
Self {
status_code,
body: body.into(),
headers: HashMap::new(),
message: None,
error_code: None,
retryable: status_code == 429 || status_code >= 500,
retry_after: None,
}
}
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = Some(message.into());
self
}
pub fn with_error_code(mut self, code: impl Into<String>) -> Self {
self.error_code = Some(code.into());
self
}
pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
self.headers = headers;
if let Some(retry_after) = self.headers.get("retry-after") {
self.retry_after = retry_after.parse().ok();
}
self
}
pub fn is_rate_limit(&self) -> bool {
self.status_code == 429
}
pub fn is_server_error(&self) -> bool {
self.status_code >= 500
}
}
#[deprecated(note = "Use ModelApiError instead")]
pub type ModelAPIError = ModelApiError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HttpErrorKind {
Timeout,
Connection,
Request,
Response {
status: Option<u16>,
},
}
#[derive(Error, Debug, Clone)]
pub struct ModelHttpError {
pub kind: HttpErrorKind,
pub message: String,
pub retry_after: Option<Duration>,
}
impl fmt::Display for ModelHttpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.kind {
HttpErrorKind::Timeout => write!(f, "Request timeout: {}", self.message),
HttpErrorKind::Connection => write!(f, "Connection error: {}", self.message),
HttpErrorKind::Request => write!(f, "HTTP request error: {}", self.message),
HttpErrorKind::Response { status } => {
if let Some(status) = status {
write!(
f,
"HTTP response error (status {}): {}",
status, self.message
)
} else {
write!(f, "HTTP response error: {}", self.message)
}
}
}
}
}
impl ModelHttpError {
pub fn new(kind: HttpErrorKind, message: impl Into<String>) -> Self {
Self {
kind,
message: message.into(),
retry_after: None,
}
}
pub fn timeout(message: impl Into<String>) -> Self {
Self::new(HttpErrorKind::Timeout, message)
}
pub fn connection(message: impl Into<String>) -> Self {
Self::new(HttpErrorKind::Connection, message)
}
pub fn request(message: impl Into<String>) -> Self {
Self::new(HttpErrorKind::Request, message)
}
pub fn response(status: Option<u16>, message: impl Into<String>) -> Self {
Self::new(HttpErrorKind::Response { status }, message)
}
pub fn with_retry_after(mut self, retry_after: Duration) -> Self {
self.retry_after = Some(retry_after);
self
}
}
#[deprecated(note = "Use ModelHttpError instead")]
pub type ModelHTTPError = ModelHttpError;
#[derive(Error, Debug, Clone, Serialize, Deserialize)]
pub struct UserError {
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<serde_json::Value>,
}
impl fmt::Display for UserError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "User error: {}", self.message)
}
}
impl UserError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
details: None,
}
}
pub fn with_details(mut self, details: serde_json::Value) -> Self {
self.details = Some(details);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum UsageLimitType {
RequestTokens,
ResponseTokens,
TotalTokens,
Requests,
}
impl fmt::Display for UsageLimitType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::RequestTokens => write!(f, "request_tokens"),
Self::ResponseTokens => write!(f, "response_tokens"),
Self::TotalTokens => write!(f, "total_tokens"),
Self::Requests => write!(f, "requests"),
}
}
}
#[derive(Error, Debug, Clone, Serialize, Deserialize)]
pub struct UsageLimitExceeded {
pub limit_type: UsageLimitType,
pub current: u64,
pub max: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
impl fmt::Display for UsageLimitExceeded {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Usage limit exceeded: {} is {} but max is {}",
self.limit_type, self.current, self.max
)
}
}
impl UsageLimitExceeded {
pub fn new(limit_type: UsageLimitType, current: u64, max: u64) -> Self {
Self {
limit_type,
current,
max,
message: None,
}
}
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = Some(message.into());
self
}
}
#[derive(Error, Debug, Clone)]
pub struct UnexpectedModelBehavior {
pub message: String,
pub response: Option<String>,
}
impl fmt::Display for UnexpectedModelBehavior {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Unexpected model behavior: {}", self.message)
}
}
impl UnexpectedModelBehavior {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
response: None,
}
}
pub fn with_response(mut self, response: impl Into<String>) -> Self {
self.response = Some(response.into());
self
}
}
#[derive(Error, Debug, Clone, Serialize, Deserialize)]
pub struct ToolRetryError {
pub tool_name: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
}
impl fmt::Display for ToolRetryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Tool '{}' retry: {}", self.tool_name, self.message)
}
}
impl ToolRetryError {
pub fn new(tool_name: impl Into<String>, message: impl Into<String>) -> Self {
Self {
tool_name: tool_name.into(),
message: message.into(),
tool_call_id: None,
}
}
pub fn with_tool_call_id(mut self, id: impl Into<String>) -> Self {
self.tool_call_id = Some(id.into());
self
}
}
#[derive(Error, Debug, Clone, Serialize, Deserialize)]
pub struct ApprovalRequired {
pub tool_name: String,
pub args: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
}
impl fmt::Display for ApprovalRequired {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Approval required for tool '{}'", self.tool_name)?;
if let Some(ref reason) = self.reason {
write!(f, ": {}", reason)?;
}
Ok(())
}
}
impl ApprovalRequired {
pub fn new(tool_name: impl Into<String>, args: serde_json::Value) -> Self {
Self {
tool_name: tool_name.into(),
args,
reason: None,
tool_call_id: None,
}
}
pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
self.reason = Some(reason.into());
self
}
pub fn with_tool_call_id(mut self, id: impl Into<String>) -> Self {
self.tool_call_id = Some(id.into());
self
}
}
#[derive(Error, Debug, Clone, Serialize, Deserialize)]
pub struct CallDeferred {
pub tool_name: String,
pub args: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_call_id: Option<String>,
}
impl fmt::Display for CallDeferred {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Tool call '{}' deferred", self.tool_name)?;
if let Some(ref reason) = self.reason {
write!(f, ": {}", reason)?;
}
Ok(())
}
}
impl CallDeferred {
pub fn new(tool_name: impl Into<String>, args: serde_json::Value) -> Self {
Self {
tool_name: tool_name.into(),
args,
reason: None,
tool_call_id: None,
}
}
pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
self.reason = Some(reason.into());
self
}
pub fn with_tool_call_id(mut self, id: impl Into<String>) -> Self {
self.tool_call_id = Some(id.into());
self
}
}
#[derive(Error, Debug, Clone, Serialize, Deserialize)]
pub struct IncompleteToolCall {
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_name: Option<String>,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub partial_data: Option<serde_json::Value>,
}
impl fmt::Display for IncompleteToolCall {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Incomplete tool call")?;
if let Some(ref name) = self.tool_name {
write!(f, " for '{}'", name)?;
}
write!(f, ": {}", self.message)
}
}
impl IncompleteToolCall {
pub fn new(message: impl Into<String>) -> Self {
Self {
tool_name: None,
message: message.into(),
partial_data: None,
}
}
pub fn with_tool_name(mut self, name: impl Into<String>) -> Self {
self.tool_name = Some(name.into());
self
}
pub fn with_partial_data(mut self, data: serde_json::Value) -> Self {
self.partial_data = Some(data);
self
}
}
#[derive(Error, Debug, Clone)]
pub struct FallbackExceptionGroup {
pub message: String,
pub errors: Vec<String>,
}
impl fmt::Display for FallbackExceptionGroup {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} ({} errors)", self.message, self.errors.len())
}
}
impl FallbackExceptionGroup {
pub fn new(message: impl Into<String>, errors: Vec<String>) -> Self {
Self {
message: message.into(),
errors,
}
}
pub fn from_errors<E: std::error::Error>(message: impl Into<String>, errors: Vec<E>) -> Self {
Self {
message: message.into(),
errors: errors.iter().map(|e| e.to_string()).collect(),
}
}
pub fn is_empty(&self) -> bool {
self.errors.is_empty()
}
pub fn len(&self) -> usize {
self.errors.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_model_retry() {
let err = ModelRetry::new("Invalid JSON output");
assert_eq!(err.message, "Invalid JSON output");
assert!(err.to_string().contains("Invalid JSON output"));
}
#[test]
fn test_usage_limit_exceeded() {
let err = UsageLimitExceeded::new(UsageLimitType::TotalTokens, 5000, 4000);
assert_eq!(err.current, 5000);
assert_eq!(err.max, 4000);
assert!(err.to_string().contains("5000"));
}
#[test]
fn test_api_error_is_rate_limit() {
let err = ModelApiError::new(429, "Rate limited");
assert!(err.is_rate_limit());
assert!(err.retryable);
}
#[test]
fn test_fallback_group() {
let group = FallbackExceptionGroup::new(
"All providers failed",
vec!["Error 1".to_string(), "Error 2".to_string()],
);
assert_eq!(group.len(), 2);
assert!(!group.is_empty());
}
}