Skip to main content

deepstrike_sdk/providers/
provider_error.rs

1use serde::{Deserialize, Serialize};
2
3/// Stable provider-failure classification carried across the host/kernel boundary.
4#[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/// Structured provider error. Only these scalar fields cross into the canonical host event.
35#[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}