1use std::time::Duration;
8
9use ferrin_message::Message;
10use ferrin_spec::ApprovalId;
11use ferrin_spec::FinishReason;
12use ferrin_spec::ProviderId;
13use ferrin_spec::ResponseMetadata;
14use ferrin_spec::ToolCallId;
15use ferrin_spec::ToolName;
16use ferrin_spec::Usage;
17use ferrin_spec::error::ModelKind;
18use ferrin_spec::error::ProviderError;
19use ferrin_spec::language_model::StreamError;
20use http::StatusCode;
21use serde::Deserialize;
22use serde::Serialize;
23use url::Url;
24
25use crate::timeout::TimeoutScope;
26
27pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
29
30#[derive(Debug, thiserror::Error)]
32#[non_exhaustive]
33pub enum Error {
34 #[error(transparent)]
36 Provider(Box<ProviderError>),
37
38 #[error("retries exhausted after {attempts} attempts ({reason})")]
40 Retry {
41 reason: RetryReason,
43 attempts: u32,
45 errors: Vec<ProviderError>,
47 },
48
49 #[error("timeout ({scope}) after {elapsed:?}")]
51 Timeout {
52 scope: TimeoutScope,
54 elapsed: Duration,
56 },
57
58 #[error("operation cancelled")]
60 Cancelled,
61
62 #[error("invalid argument `{argument}`: {message}")]
64 InvalidArgument {
65 argument: String,
67 message: String,
69 },
70
71 #[error("invalid prompt: {message}")]
73 InvalidPrompt {
74 message: String,
76 },
77
78 #[error("message conversion failed: {message}")]
80 MessageConversion {
81 message: String,
83 original_message: Box<Message>,
85 },
86
87 #[error("download failed for {}", .0.url)]
89 Download(#[source] Box<DownloadDetails>),
90
91 #[error("invalid data content: {message}")]
93 InvalidDataContent {
94 message: String,
96 #[source]
98 cause: Option<BoxError>,
99 },
100
101 #[error("no such tool `{tool_name}`")]
103 NoSuchTool {
104 tool_name: ToolName,
106 available_tools: Vec<ToolName>,
108 },
109
110 #[error("invalid input for tool `{}`", .0.tool_name)]
112 InvalidToolInput(#[source] Box<InvalidToolInputDetails>),
113
114 #[error("tool call repair failed")]
116 ToolCallRepair {
117 original: Box<Error>,
119 #[source]
121 cause: BoxError,
122 },
123
124 #[error("tool choice violated: expected `{expected}`, got `{actual}`")]
126 ToolChoiceViolation {
127 expected: ToolName,
129 actual: ToolName,
131 },
132
133 #[error("tool call `{tool_call_id}` not found for approval `{approval_id}`")]
135 ToolCallNotFoundForApproval {
136 tool_call_id: ToolCallId,
138 approval_id: ApprovalId,
140 },
141
142 #[error("tool choice not satisfied: {}", .expected.as_ref().map_or_else(|| "a tool call was required".to_owned(), |name| format!("expected a call to `{name}`")))]
145 ToolChoiceNotSatisfied {
146 expected: Option<ToolName>,
148 },
149
150 #[error("invalid tool approval `{approval_id}`: {message}")]
152 InvalidToolApproval {
153 approval_id: ApprovalId,
155 message: String,
157 },
158
159 #[error("no structured output generated: {}", .0.message)]
161 NoObjectGenerated(#[source] Box<NoObjectGeneratedDetails>),
162
163 #[error("no output generated")]
165 NoOutputGenerated,
166
167 #[error("no image generated")]
169 NoImageGenerated {
170 responses: Vec<ResponseMetadata>,
172 },
173
174 #[error("no speech generated")]
176 NoSpeechGenerated {
177 responses: Vec<ResponseMetadata>,
179 },
180
181 #[error("no transcript generated")]
183 NoTranscriptGenerated {
184 responses: Vec<ResponseMetadata>,
186 },
187
188 #[error("no video generated")]
190 NoVideoGenerated {
191 responses: Vec<ResponseMetadata>,
193 },
194
195 #[error("no such provider `{}`", .0.provider_id)]
197 NoSuchProvider(Box<NoSuchProviderDetails>),
198
199 #[error("no default registry configured for model id `{model_id}`")]
201 NoDefaultRegistry {
202 model_id: String,
204 },
205
206 #[error("invalid stream part: {message}")]
208 InvalidStreamPart {
209 message: String,
211 },
212
213 #[error("stream error: {}", .0.message)]
215 Stream(Box<StreamError>),
216
217 #[error(transparent)]
219 Mcp(BoxError),
220
221 #[error(transparent)]
223 Other(BoxError),
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
228#[serde(rename_all = "kebab-case")]
229pub enum RetryReason {
230 MaxRetriesExceeded,
232 ErrorNotRetryable,
234 Abort,
236}
237
238impl std::fmt::Display for RetryReason {
239 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240 f.write_str(match self {
241 Self::MaxRetriesExceeded => "max retries exceeded",
242 Self::ErrorNotRetryable => "error not retryable",
243 Self::Abort => "aborted",
244 })
245 }
246}
247
248#[derive(Debug, thiserror::Error)]
250#[error("download of {url} failed")]
251pub struct DownloadDetails {
252 pub url: Url,
254 pub status_code: Option<StatusCode>,
256 #[source]
258 pub cause: Option<BoxError>,
259}
260
261#[derive(Debug, thiserror::Error)]
263#[error("invalid input for tool `{tool_name}`: {tool_input}")]
264pub struct InvalidToolInputDetails {
265 pub tool_name: ToolName,
267 pub tool_input: String,
269 #[source]
271 pub cause: BoxError,
272}
273
274#[derive(Debug, thiserror::Error)]
276#[error("{message}")]
277pub struct NoObjectGeneratedDetails {
278 pub message: String,
280 pub text: Option<String>,
282 pub response: ResponseMetadata,
284 pub usage: Usage,
286 pub finish_reason: FinishReason,
288 #[source]
290 pub cause: Option<BoxError>,
291}
292
293#[derive(Debug, Clone, PartialEq, Eq)]
295pub struct NoSuchProviderDetails {
296 pub provider_id: ProviderId,
298 pub available_providers: Vec<ProviderId>,
300 pub model_id: String,
302 pub model_kind: ModelKind,
304}
305
306#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
308#[serde(rename_all = "kebab-case")]
309#[non_exhaustive]
310pub enum ErrorKind {
311 Provider,
313 Retry,
315 Timeout,
317 Cancelled,
319 InvalidInput,
321 Tool,
323 Output,
325 NotFound,
327 Mcp,
329 Other,
331}
332
333impl ErrorKind {
334 #[must_use]
336 pub fn as_str(self) -> &'static str {
337 match self {
338 Self::Provider => "provider",
339 Self::Retry => "retry",
340 Self::Timeout => "timeout",
341 Self::Cancelled => "cancelled",
342 Self::InvalidInput => "invalid-input",
343 Self::Tool => "tool",
344 Self::Output => "output",
345 Self::NotFound => "not-found",
346 Self::Mcp => "mcp",
347 Self::Other => "other",
348 }
349 }
350}
351
352impl std::fmt::Display for ErrorKind {
353 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
354 f.write_str(self.as_str())
355 }
356}
357
358impl Error {
359 #[must_use]
361 pub fn invalid_argument(argument: impl Into<String>, message: impl Into<String>) -> Self {
362 Self::InvalidArgument {
363 argument: argument.into(),
364 message: message.into(),
365 }
366 }
367
368 #[must_use]
370 pub fn invalid_prompt(message: impl Into<String>) -> Self {
371 Self::InvalidPrompt {
372 message: message.into(),
373 }
374 }
375
376 #[must_use]
378 pub fn invalid_data_content(message: impl Into<String>, cause: Option<BoxError>) -> Self {
379 Self::InvalidDataContent {
380 message: message.into(),
381 cause,
382 }
383 }
384
385 #[must_use]
387 pub fn download(url: Url, status_code: Option<StatusCode>, cause: Option<BoxError>) -> Self {
388 Self::Download(Box::new(DownloadDetails {
389 url,
390 status_code,
391 cause,
392 }))
393 }
394
395 #[must_use]
397 pub fn no_such_tool(tool_name: impl Into<ToolName>, available_tools: Vec<ToolName>) -> Self {
398 Self::NoSuchTool {
399 tool_name: tool_name.into(),
400 available_tools,
401 }
402 }
403
404 #[must_use]
406 pub fn invalid_tool_input(
407 tool_name: impl Into<ToolName>,
408 tool_input: impl Into<String>,
409 cause: impl Into<BoxError>,
410 ) -> Self {
411 Self::InvalidToolInput(Box::new(InvalidToolInputDetails {
412 tool_name: tool_name.into(),
413 tool_input: tool_input.into(),
414 cause: cause.into(),
415 }))
416 }
417
418 #[must_use]
420 pub fn no_object_generated(details: NoObjectGeneratedDetails) -> Self {
421 Self::NoObjectGenerated(Box::new(details))
422 }
423
424 #[must_use]
426 pub fn no_such_provider(details: NoSuchProviderDetails) -> Self {
427 Self::NoSuchProvider(Box::new(details))
428 }
429
430 #[must_use]
432 pub fn invalid_stream_part(message: impl Into<String>) -> Self {
433 Self::InvalidStreamPart {
434 message: message.into(),
435 }
436 }
437
438 #[must_use]
440 pub fn stream(error: StreamError) -> Self {
441 Self::Stream(Box::new(error))
442 }
443
444 #[must_use]
446 pub fn other(error: impl std::error::Error + Send + Sync + 'static) -> Self {
447 Self::Other(Box::new(error))
448 }
449
450 #[must_use]
452 pub fn message(message: impl Into<String>) -> Self {
453 Self::Other(message.into().into())
454 }
455
456 #[must_use]
458 pub fn is_retryable(&self) -> bool {
459 match self {
460 Self::Provider(error) => error.is_retryable(),
461 Self::Retry {
462 reason: RetryReason::MaxRetriesExceeded,
463 errors,
464 ..
465 } => errors.last().is_some_and(ProviderError::is_retryable),
466 Self::Stream(error) => error.is_retryable.unwrap_or(false),
467 _ => false,
468 }
469 }
470
471 #[must_use]
473 pub fn is_cancelled(&self) -> bool {
474 matches!(
475 self,
476 Self::Cancelled
477 | Self::Retry {
478 reason: RetryReason::Abort,
479 ..
480 }
481 )
482 }
483
484 #[must_use]
486 pub fn status_code(&self) -> Option<StatusCode> {
487 match self {
488 Self::Provider(error) => error.status_code(),
489 Self::Retry { errors, .. } => errors.last().and_then(ProviderError::status_code),
490 Self::Download(details) => details.status_code,
491 Self::Stream(error) => error
492 .status_code
493 .and_then(|code| StatusCode::from_u16(code).ok()),
494 _ => None,
495 }
496 }
497
498 #[must_use]
500 pub fn as_provider(&self) -> Option<&ProviderError> {
501 match self {
502 Self::Provider(error) => Some(error),
503 _ => None,
504 }
505 }
506
507 #[must_use]
509 pub fn kind(&self) -> ErrorKind {
510 match self {
511 Self::Provider(_) | Self::Stream(_) => ErrorKind::Provider,
512 Self::Retry { .. } => ErrorKind::Retry,
513 Self::Timeout { .. } => ErrorKind::Timeout,
514 Self::Cancelled => ErrorKind::Cancelled,
515 Self::InvalidArgument { .. }
516 | Self::InvalidPrompt { .. }
517 | Self::MessageConversion { .. }
518 | Self::Download(_)
519 | Self::InvalidDataContent { .. }
520 | Self::InvalidStreamPart { .. } => ErrorKind::InvalidInput,
521 Self::NoSuchTool { .. }
522 | Self::InvalidToolInput(_)
523 | Self::ToolCallRepair { .. }
524 | Self::ToolChoiceViolation { .. }
525 | Self::ToolChoiceNotSatisfied { .. }
526 | Self::ToolCallNotFoundForApproval { .. }
527 | Self::InvalidToolApproval { .. } => ErrorKind::Tool,
528 Self::NoObjectGenerated(_)
529 | Self::NoOutputGenerated
530 | Self::NoImageGenerated { .. }
531 | Self::NoSpeechGenerated { .. }
532 | Self::NoTranscriptGenerated { .. }
533 | Self::NoVideoGenerated { .. } => ErrorKind::Output,
534 Self::NoSuchProvider(_) | Self::NoDefaultRegistry { .. } => ErrorKind::NotFound,
535 Self::Mcp(_) => ErrorKind::Mcp,
536 Self::Other(_) => ErrorKind::Other,
537 }
538 }
539}
540
541impl From<ProviderError> for Error {
542 fn from(error: ProviderError) -> Self {
543 match error {
544 ProviderError::Cancelled => Self::Cancelled,
545 other => Self::Provider(Box::new(other)),
546 }
547 }
548}
549
550impl From<ferrin_spec::error::NoSuchModelError> for Error {
551 fn from(error: ferrin_spec::error::NoSuchModelError) -> Self {
552 Self::Provider(Box::new(ProviderError::NoSuchModel(Box::new(error))))
553 }
554}
555
556impl From<ferrin_spec::error::InvalidArgumentError> for Error {
557 fn from(error: ferrin_spec::error::InvalidArgumentError) -> Self {
558 Self::InvalidArgument {
559 argument: error.argument,
560 message: error.message,
561 }
562 }
563}
564
565impl From<ferrin_tool::ToolError> for Error {
566 fn from(error: ferrin_tool::ToolError) -> Self {
567 match error {
568 ferrin_tool::ToolError::Cancelled => Self::Cancelled,
569 other => Self::Other(Box::new(other)),
570 }
571 }
572}