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 translation generated")]
190 NoTranslationGenerated {
191 response: Box<ResponseMetadata>,
193 },
194
195 #[error("no video generated")]
197 NoVideoGenerated {
198 responses: Vec<ResponseMetadata>,
200 },
201
202 #[error("no such provider `{}`", .0.provider_id)]
204 NoSuchProvider(Box<NoSuchProviderDetails>),
205
206 #[error("no default registry configured for model id `{model_id}`")]
208 NoDefaultRegistry {
209 model_id: String,
211 },
212
213 #[error("invalid stream part: {message}")]
215 InvalidStreamPart {
216 message: String,
218 },
219
220 #[error("stream error: {}", .0.message)]
222 Stream(Box<StreamError>),
223
224 #[error(transparent)]
226 Mcp(BoxError),
227
228 #[error(transparent)]
230 Other(BoxError),
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
235#[serde(rename_all = "kebab-case")]
236pub enum RetryReason {
237 MaxRetriesExceeded,
239 ErrorNotRetryable,
241 Abort,
243}
244
245impl std::fmt::Display for RetryReason {
246 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247 f.write_str(match self {
248 Self::MaxRetriesExceeded => "max retries exceeded",
249 Self::ErrorNotRetryable => "error not retryable",
250 Self::Abort => "aborted",
251 })
252 }
253}
254
255#[derive(Debug, thiserror::Error)]
257#[error("download of {url} failed")]
258pub struct DownloadDetails {
259 pub url: Url,
261 pub status_code: Option<StatusCode>,
263 #[source]
265 pub cause: Option<BoxError>,
266}
267
268#[derive(Debug, thiserror::Error)]
270#[error("invalid input for tool `{tool_name}`: {tool_input}")]
271pub struct InvalidToolInputDetails {
272 pub tool_name: ToolName,
274 pub tool_input: String,
276 #[source]
278 pub cause: BoxError,
279}
280
281#[derive(Debug, thiserror::Error)]
283#[error("{message}")]
284pub struct NoObjectGeneratedDetails {
285 pub message: String,
287 pub text: Option<String>,
289 pub response: ResponseMetadata,
291 pub usage: Usage,
293 pub finish_reason: FinishReason,
295 #[source]
297 pub cause: Option<BoxError>,
298}
299
300#[derive(Debug, Clone, PartialEq, Eq)]
302pub struct NoSuchProviderDetails {
303 pub provider_id: ProviderId,
305 pub available_providers: Vec<ProviderId>,
307 pub model_id: String,
309 pub model_kind: ModelKind,
311}
312
313#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
315#[serde(rename_all = "kebab-case")]
316#[non_exhaustive]
317pub enum ErrorKind {
318 Provider,
320 Retry,
322 Timeout,
324 Cancelled,
326 InvalidInput,
328 Tool,
330 Output,
332 NotFound,
334 Mcp,
336 Other,
338}
339
340impl ErrorKind {
341 #[must_use]
343 pub fn as_str(self) -> &'static str {
344 match self {
345 Self::Provider => "provider",
346 Self::Retry => "retry",
347 Self::Timeout => "timeout",
348 Self::Cancelled => "cancelled",
349 Self::InvalidInput => "invalid-input",
350 Self::Tool => "tool",
351 Self::Output => "output",
352 Self::NotFound => "not-found",
353 Self::Mcp => "mcp",
354 Self::Other => "other",
355 }
356 }
357}
358
359impl std::fmt::Display for ErrorKind {
360 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
361 f.write_str(self.as_str())
362 }
363}
364
365impl Error {
366 #[must_use]
368 pub fn invalid_argument(argument: impl Into<String>, message: impl Into<String>) -> Self {
369 Self::InvalidArgument {
370 argument: argument.into(),
371 message: message.into(),
372 }
373 }
374
375 #[must_use]
377 pub fn invalid_prompt(message: impl Into<String>) -> Self {
378 Self::InvalidPrompt {
379 message: message.into(),
380 }
381 }
382
383 #[must_use]
385 pub fn invalid_data_content(message: impl Into<String>, cause: Option<BoxError>) -> Self {
386 Self::InvalidDataContent {
387 message: message.into(),
388 cause,
389 }
390 }
391
392 #[must_use]
394 pub fn download(url: Url, status_code: Option<StatusCode>, cause: Option<BoxError>) -> Self {
395 Self::Download(Box::new(DownloadDetails {
396 url,
397 status_code,
398 cause,
399 }))
400 }
401
402 #[must_use]
404 pub fn no_such_tool(tool_name: impl Into<ToolName>, available_tools: Vec<ToolName>) -> Self {
405 Self::NoSuchTool {
406 tool_name: tool_name.into(),
407 available_tools,
408 }
409 }
410
411 #[must_use]
413 pub fn invalid_tool_input(
414 tool_name: impl Into<ToolName>,
415 tool_input: impl Into<String>,
416 cause: impl Into<BoxError>,
417 ) -> Self {
418 Self::InvalidToolInput(Box::new(InvalidToolInputDetails {
419 tool_name: tool_name.into(),
420 tool_input: tool_input.into(),
421 cause: cause.into(),
422 }))
423 }
424
425 #[must_use]
427 pub fn no_object_generated(details: NoObjectGeneratedDetails) -> Self {
428 Self::NoObjectGenerated(Box::new(details))
429 }
430
431 #[must_use]
433 pub fn no_such_provider(details: NoSuchProviderDetails) -> Self {
434 Self::NoSuchProvider(Box::new(details))
435 }
436
437 #[must_use]
439 pub fn invalid_stream_part(message: impl Into<String>) -> Self {
440 Self::InvalidStreamPart {
441 message: message.into(),
442 }
443 }
444
445 #[must_use]
447 pub fn stream(error: StreamError) -> Self {
448 Self::Stream(Box::new(error))
449 }
450
451 #[must_use]
453 pub fn other(error: impl std::error::Error + Send + Sync + 'static) -> Self {
454 Self::Other(Box::new(error))
455 }
456
457 #[must_use]
459 pub fn message(message: impl Into<String>) -> Self {
460 Self::Other(message.into().into())
461 }
462
463 #[must_use]
465 pub fn is_retryable(&self) -> bool {
466 match self {
467 Self::Provider(error) => error.is_retryable(),
468 Self::Retry {
469 reason: RetryReason::MaxRetriesExceeded,
470 errors,
471 ..
472 } => errors.last().is_some_and(ProviderError::is_retryable),
473 Self::Stream(error) => error.is_retryable.unwrap_or(false),
474 _ => false,
475 }
476 }
477
478 #[must_use]
480 pub fn is_cancelled(&self) -> bool {
481 matches!(
482 self,
483 Self::Cancelled
484 | Self::Retry {
485 reason: RetryReason::Abort,
486 ..
487 }
488 )
489 }
490
491 #[must_use]
493 pub fn status_code(&self) -> Option<StatusCode> {
494 match self {
495 Self::Provider(error) => error.status_code(),
496 Self::Retry { errors, .. } => errors.last().and_then(ProviderError::status_code),
497 Self::Download(details) => details.status_code,
498 Self::Stream(error) => error
499 .status_code
500 .and_then(|code| StatusCode::from_u16(code).ok()),
501 _ => None,
502 }
503 }
504
505 #[must_use]
507 pub fn as_provider(&self) -> Option<&ProviderError> {
508 match self {
509 Self::Provider(error) => Some(error),
510 _ => None,
511 }
512 }
513
514 #[must_use]
516 pub fn kind(&self) -> ErrorKind {
517 match self {
518 Self::Provider(_) | Self::Stream(_) => ErrorKind::Provider,
519 Self::Retry { .. } => ErrorKind::Retry,
520 Self::Timeout { .. } => ErrorKind::Timeout,
521 Self::Cancelled => ErrorKind::Cancelled,
522 Self::InvalidArgument { .. }
523 | Self::InvalidPrompt { .. }
524 | Self::MessageConversion { .. }
525 | Self::Download(_)
526 | Self::InvalidDataContent { .. }
527 | Self::InvalidStreamPart { .. } => ErrorKind::InvalidInput,
528 Self::NoSuchTool { .. }
529 | Self::InvalidToolInput(_)
530 | Self::ToolCallRepair { .. }
531 | Self::ToolChoiceViolation { .. }
532 | Self::ToolChoiceNotSatisfied { .. }
533 | Self::ToolCallNotFoundForApproval { .. }
534 | Self::InvalidToolApproval { .. } => ErrorKind::Tool,
535 Self::NoObjectGenerated(_)
536 | Self::NoOutputGenerated
537 | Self::NoImageGenerated { .. }
538 | Self::NoSpeechGenerated { .. }
539 | Self::NoTranscriptGenerated { .. }
540 | Self::NoTranslationGenerated { .. }
541 | Self::NoVideoGenerated { .. } => ErrorKind::Output,
542 Self::NoSuchProvider(_) | Self::NoDefaultRegistry { .. } => ErrorKind::NotFound,
543 Self::Mcp(_) => ErrorKind::Mcp,
544 Self::Other(_) => ErrorKind::Other,
545 }
546 }
547}
548
549impl From<ProviderError> for Error {
550 fn from(error: ProviderError) -> Self {
551 match error {
552 ProviderError::Cancelled => Self::Cancelled,
553 other => Self::Provider(Box::new(other)),
554 }
555 }
556}
557
558impl From<ferrin_spec::error::NoSuchModelError> for Error {
559 fn from(error: ferrin_spec::error::NoSuchModelError) -> Self {
560 Self::Provider(Box::new(ProviderError::NoSuchModel(Box::new(error))))
561 }
562}
563
564impl From<ferrin_spec::error::InvalidArgumentError> for Error {
565 fn from(error: ferrin_spec::error::InvalidArgumentError) -> Self {
566 Self::InvalidArgument {
567 argument: error.argument,
568 message: error.message,
569 }
570 }
571}
572
573impl From<ferrin_tool::ToolError> for Error {
574 fn from(error: ferrin_tool::ToolError) -> Self {
575 match error {
576 ferrin_tool::ToolError::Cancelled => Self::Cancelled,
577 other => Self::Other(Box::new(other)),
578 }
579 }
580}