deepstrike_sdk/providers/
provider_error.rs1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case")]
6pub enum ProviderErrorKind {
7 Transport,
8 Auth,
9 RateLimit,
10 ContextOverflow,
11 InvalidRequest,
12 Modality,
13 ModelUnavailable,
14 Protocol,
15 Unknown,
16}
17
18impl ProviderErrorKind {
19 pub(crate) const fn as_str(self) -> &'static str {
20 match self {
21 Self::Transport => "transport",
22 Self::Auth => "auth",
23 Self::RateLimit => "rate_limit",
24 Self::ContextOverflow => "context_overflow",
25 Self::InvalidRequest => "invalid_request",
26 Self::Modality => "modality",
27 Self::ModelUnavailable => "model_unavailable",
28 Self::Protocol => "protocol",
29 Self::Unknown => "unknown",
30 }
31 }
32}
33
34#[derive(Debug, Clone, thiserror::Error)]
36#[error("{message}")]
37pub struct ProviderError {
38 pub provider: String,
39 pub kind: ProviderErrorKind,
40 pub retryable: bool,
41 pub message: String,
42 pub http_status: Option<u16>,
43 pub provider_code: Option<String>,
44}
45
46impl ProviderError {
47 pub fn new(
48 provider: impl Into<String>,
49 kind: ProviderErrorKind,
50 retryable: bool,
51 message: impl Into<String>,
52 ) -> Self {
53 Self {
54 provider: provider.into(),
55 kind,
56 retryable,
57 message: message.into(),
58 http_status: None,
59 provider_code: None,
60 }
61 }
62
63 pub fn transport(provider: impl Into<String>, message: impl Into<String>) -> Self {
64 Self::new(provider, ProviderErrorKind::Transport, true, message)
65 }
66
67 pub fn from_http(provider: impl Into<String>, status: u16, body: impl Into<String>) -> Self {
68 let provider = provider.into();
69 let body = body.into();
70 let provider_code = provider_code(&body);
71 let kind = classify_http(status, provider_code.as_deref());
72 let retryable = matches!(
73 kind,
74 ProviderErrorKind::Transport
75 | ProviderErrorKind::RateLimit
76 | ProviderErrorKind::ModelUnavailable
77 );
78 Self {
79 message: format!("{provider} HTTP {status}: {body}"),
80 provider,
81 kind,
82 retryable,
83 http_status: Some(status),
84 provider_code,
85 }
86 }
87}
88
89fn provider_code(body: &str) -> Option<String> {
90 let value: serde_json::Value = serde_json::from_str(body).ok()?;
91 [
92 value.pointer("/error/code"),
93 value.pointer("/error/error_code"),
94 value.pointer("/error/type"),
95 value.get("code"),
96 value.get("error_code"),
97 ]
98 .into_iter()
99 .flatten()
100 .find_map(|value| value.as_str().filter(|value| !value.is_empty()))
101 .map(str::to_owned)
102}
103
104fn classify_http(status: u16, provider_code: Option<&str>) -> ProviderErrorKind {
105 if status == 413
106 || provider_code.is_some_and(|code| {
107 code.eq_ignore_ascii_case("context_length_exceeded")
108 || code.eq_ignore_ascii_case("prompt_too_long")
109 })
110 {
111 return ProviderErrorKind::ContextOverflow;
112 }
113 match status {
114 401 | 403 => ProviderErrorKind::Auth,
115 429 => ProviderErrorKind::RateLimit,
116 404 | 500..=599 => ProviderErrorKind::ModelUnavailable,
117 408 | 409 => ProviderErrorKind::Transport,
118 400 | 422 => ProviderErrorKind::InvalidRequest,
119 _ => ProviderErrorKind::Unknown,
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn http_context_overflow_uses_status_or_structured_code() {
129 let by_status = ProviderError::from_http("test", 413, "too large");
130 assert_eq!(by_status.kind, ProviderErrorKind::ContextOverflow);
131
132 let by_code = ProviderError::from_http(
133 "test",
134 400,
135 r#"{"error":{"code":"context_length_exceeded"}}"#,
136 );
137 assert_eq!(by_code.kind, ProviderErrorKind::ContextOverflow);
138 assert_eq!(
139 by_code.provider_code.as_deref(),
140 Some("context_length_exceeded")
141 );
142 }
143
144 #[test]
145 fn prose_does_not_determine_failure_kind() {
146 let error = ProviderError::from_http("test", 400, "413 prompt too long");
147 assert_eq!(error.kind, ProviderErrorKind::InvalidRequest);
148 }
149}