use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ProviderKind {
OpenAI,
Anthropic,
OpenRouter,
#[serde(rename = "openai-compatible")]
OpenAICompatible,
}
impl ProviderKind {
pub fn feature_name(&self) -> &'static str {
match self {
ProviderKind::OpenAI | ProviderKind::OpenAICompatible => "openai",
ProviderKind::Anthropic => "anthropic",
ProviderKind::OpenRouter => "openrouter",
}
}
}
impl std::fmt::Display for ProviderKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProviderKind::OpenAI => write!(f, "openai"),
ProviderKind::Anthropic => write!(f, "anthropic"),
ProviderKind::OpenRouter => write!(f, "openrouter"),
ProviderKind::OpenAICompatible => write!(f, "openai-compatible"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Capability {
ToolCalling,
StructuredOutput,
}
impl Capability {
pub fn as_str(&self) -> &'static str {
match self {
Capability::ToolCalling => "tool_calling",
Capability::StructuredOutput => "structured_output",
}
}
}
impl std::fmt::Display for Capability {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Capability::ToolCalling => write!(f, "tool calling"),
Capability::StructuredOutput => write!(f, "structured output"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ToolArgumentIssue {
pub path: String,
pub schema_path: String,
pub message: String,
}
#[derive(Error, Debug)]
pub enum Error {
#[error("authentication error for {provider}: {message}")]
Auth {
provider: ProviderKind,
message: String,
},
#[error("API request failed for {provider}: {message}")]
Request {
provider: ProviderKind,
message: String,
},
#[error("rate limit exceeded for {provider}: {message}")]
RateLimit {
provider: ProviderKind,
message: String,
},
#[error("invalid request: {0}")]
InvalidRequest(String),
#[error("model not available: {model} for provider {provider}")]
ModelNotAvailable {
provider: ProviderKind,
model: String,
},
#[error("provider {0} is not configured")]
ProviderNotConfigured(ProviderKind),
#[error(
"provider {0} feature is not enabled — enable the '{feature}' feature in Cargo.toml",
feature = .0.feature_name()
)]
ProviderNotEnabled(ProviderKind),
#[error("{capability} is not supported by the {provider} endpoint at {base_url}: {message}")]
CapabilityUnsupported {
provider: ProviderKind,
capability: Capability,
base_url: String,
message: String,
},
#[error("content filtered by {provider}: {reason}")]
ContentFiltered {
provider: ProviderKind,
reason: String,
},
#[error("configuration error: {0}")]
Config(String),
#[error("serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("stream error: {0}")]
Stream(String),
#[error("request timed out for {provider}")]
Timeout {
provider: ProviderKind,
},
#[error("tool calling is not supported for provider {provider}")]
ToolProviderUnsupported {
provider: ProviderKind,
},
#[error("invalid arguments for tool '{name}': {message}")]
ToolArguments {
name: String,
message: String,
issues: Vec<ToolArgumentIssue>,
},
#[error("tool not found: {name}")]
ToolNotFound {
name: String,
},
#[error("tool execution exceeded the maximum number of rounds ({max_rounds})")]
ToolLoopLimitExceeded {
max_rounds: usize,
},
#[error("structured output validation failed for {provider} model {model}: {message}")]
StructuredOutput {
provider: ProviderKind,
model: String,
message: String,
},
}
impl Error {
pub fn is_retryable(&self) -> bool {
matches!(
self,
Error::RateLimit { .. } | Error::Timeout { .. } | Error::Http(_)
)
}
pub fn is_auth_error(&self) -> bool {
matches!(self, Error::Auth { .. })
}
pub fn is_rate_limit(&self) -> bool {
matches!(self, Error::RateLimit { .. })
}
pub fn unsupported_capability(&self) -> Option<Capability> {
match self {
Error::CapabilityUnsupported { capability, .. } => Some(*capability),
_ => None,
}
}
pub fn kind_str(&self) -> &'static str {
match self {
Error::Auth { .. } => "auth",
Error::Request { .. } => "request",
Error::RateLimit { .. } => "rate_limit",
Error::InvalidRequest(_) => "invalid_request",
Error::ModelNotAvailable { .. } => "model_not_available",
Error::ProviderNotConfigured(_) => "provider_not_configured",
Error::ProviderNotEnabled(_) => "provider_not_enabled",
Error::CapabilityUnsupported { .. } => "capability_unsupported",
Error::ContentFiltered { .. } => "content_filtered",
Error::Config(_) => "config",
Error::Serialization(_) => "serialization",
Error::Http(_) => "http",
Error::Stream(_) => "stream",
Error::Timeout { .. } => "timeout",
Error::ToolProviderUnsupported { .. } => "tool_provider_unsupported",
Error::ToolArguments { .. } => "tool_arguments",
Error::ToolNotFound { .. } => "tool_not_found",
Error::ToolLoopLimitExceeded { .. } => "tool_loop_limit_exceeded",
Error::StructuredOutput { .. } => "structured_output",
}
}
pub fn provider(&self) -> Option<ProviderKind> {
match self {
Error::Auth { provider, .. }
| Error::Request { provider, .. }
| Error::RateLimit { provider, .. }
| Error::ModelNotAvailable { provider, .. }
| Error::ProviderNotConfigured(provider)
| Error::ProviderNotEnabled(provider)
| Error::CapabilityUnsupported { provider, .. }
| Error::ContentFiltered { provider, .. }
| Error::Timeout { provider }
| Error::ToolProviderUnsupported { provider }
| Error::StructuredOutput { provider, .. } => Some(*provider),
_ => None,
}
}
}
pub type Result<T> = std::result::Result<T, Error>;