1use std::time::Duration;
23
24use thiserror::Error;
25use uuid::Uuid;
26
27use systemprompt_database::resilience::Outcome;
28use systemprompt_identifiers::{AiRequestId, McpServerId};
29use systemprompt_provider_contracts::LlmProviderError;
30
31#[derive(Debug, Error)]
32pub enum AiError {
33 #[error("Model not specified and no default available for provider {provider}")]
34 ModelNotSpecified { provider: String },
35
36 #[error("Request metadata missing required field: {field}")]
37 MissingMetadata { field: String },
38
39 #[error("User context required for billing and audit trails")]
40 MissingUserContext,
41
42 #[error("Provider {provider} returned empty response")]
43 EmptyProviderResponse { provider: String },
44
45 #[error("Tool call schema validation failed: {reason}")]
46 InvalidToolSchema { reason: String },
47
48 #[error("Authentication required for service {service_id}")]
49 AuthenticationRequired { service_id: McpServerId },
50
51 #[error("Structured output validation failed after {retries} attempts: {details}")]
52 StructuredOutputFailed { retries: usize, details: String },
53
54 #[error("Provider {provider} error: {message}")]
55 ProviderError { provider: String, message: String },
56
57 #[error("No configured provider supports model {model}")]
58 NoProviderForModel { model: String },
59
60 #[error(transparent)]
61 Provider(#[from] LlmProviderError),
62
63 #[error("Serialization failed: {0}")]
64 SerializationError(#[from] serde_json::Error),
65
66 #[error("HTTP request failed: {0}")]
67 Http(#[from] reqwest::Error),
68
69 #[error("I/O error: {0}")]
70 Io(#[from] std::io::Error),
71
72 #[error("Message history cannot be serialized to JSON")]
73 MessageSerializationFailed,
74
75 #[error("Tool {tool_name} missing required field: {field}")]
76 MissingToolField { tool_name: String, field: String },
77
78 #[error("Tool description cannot be empty for tool: {tool_name}")]
79 EmptyToolDescription { tool_name: String },
80
81 #[error("No tool calls found in provider response")]
82 NoToolCalls,
83
84 #[error("Rate limit exceeded for provider {provider}: {details}")]
85 RateLimit { provider: String, details: String },
86
87 #[error("Provider {provider} returned HTTP {status}: {body}")]
88 HttpStatus {
89 provider: String,
90 status: u16,
91 retry_after: Option<Duration>,
92 body: String,
93 },
94
95 #[error("Provider {provider} request timed out after {after_ms}ms")]
96 Timeout { provider: String, after_ms: u64 },
97
98 #[error("Circuit breaker open for provider {provider}; failing fast")]
99 CircuitOpen { provider: String },
100
101 #[error("Provider {provider} unavailable: concurrency limit reached")]
102 DependencyUnavailable { provider: String },
103
104 #[error("Invalid API credentials for provider {provider}")]
105 AuthenticationFailed { provider: String },
106
107 #[error("Configuration error: {message}")]
108 ConfigurationError { message: String },
109
110 #[error("Database operation failed: {message}")]
111 DatabaseError { message: String },
112
113 #[error("MCP service {service_id} not found or not configured")]
114 McpServiceNotFound { service_id: McpServerId },
115
116 #[error("MCP service {service_id} requires OAuth authentication but no token available")]
117 McpAuthenticationMissing { service_id: McpServerId },
118
119 #[error("Failed to determine service authentication requirements: {details}")]
120 ServiceAuthCheckFailed { details: String },
121
122 #[error("Storage operation failed: {message}")]
123 StorageError { message: String },
124
125 #[error("Invalid input: {0}")]
126 InvalidInput(String),
127
128 #[error("Regex error: {0}")]
129 Regex(#[from] regex::Error),
130
131 #[error(transparent)]
132 ToolProvider(#[from] systemprompt_traits::ToolProviderError),
133
134 #[error(transparent)]
135 Secrets(#[from] systemprompt_config::SecretsBootstrapError),
136
137 #[error(transparent)]
138 WireParse(#[from] systemprompt_models::wire::error::WireParseError),
139
140 #[error("internal: {0}")]
141 Internal(String),
142}
143
144#[derive(Debug, Error)]
145pub enum RepositoryError {
146 #[error("AI request not found: {0}")]
147 NotFound(Uuid),
148
149 #[error("Database error: {0}")]
150 Database(#[from] sqlx::Error),
151
152 #[error("Invalid data: {field} - {reason}")]
153 InvalidData { field: String, reason: String },
154
155 #[error("Database pool initialization failed: {0}")]
156 PoolInitialization(String),
157
158 #[error("Settlement of AI request {request_id} rejected: {reason}")]
159 SettlementConflict {
160 request_id: AiRequestId,
161 reason: String,
162 },
163}
164
165impl AiError {
166 pub async fn from_error_response(provider: &str, response: reqwest::Response) -> Self {
167 let status = response.status().as_u16();
168 let retry_after = parse_retry_after(response.headers());
169 let body = response.text().await.unwrap_or_default();
170 Self::HttpStatus {
171 provider: provider.to_owned(),
172 status,
173 retry_after,
174 body,
175 }
176 }
177
178 #[must_use]
179 pub fn classify(&self) -> Outcome {
180 match self {
181 Self::HttpStatus {
182 status,
183 retry_after,
184 ..
185 } => {
186 if matches!(*status, 408 | 425 | 429 | 500 | 502 | 503 | 504) {
187 Outcome::Transient {
188 retry_after: *retry_after,
189 }
190 } else {
191 Outcome::Permanent
192 }
193 },
194 Self::RateLimit { .. } | Self::Timeout { .. } => {
195 Outcome::Transient { retry_after: None }
196 },
197 Self::Http(err) if err.is_timeout() || err.is_connect() => {
198 Outcome::Transient { retry_after: None }
199 },
200 _ => Outcome::Permanent,
201 }
202 }
203}
204
205fn parse_retry_after(headers: &reqwest::header::HeaderMap) -> Option<Duration> {
206 headers
207 .get(reqwest::header::RETRY_AFTER)?
208 .to_str()
209 .ok()?
210 .trim()
211 .parse::<u64>()
212 .ok()
213 .map(Duration::from_secs)
214}
215
216pub type Result<T> = std::result::Result<T, AiError>;
217
218impl From<RepositoryError> for AiError {
219 fn from(error: RepositoryError) -> Self {
220 Self::DatabaseError {
221 message: error.to_string(),
222 }
223 }
224}