use async_trait::async_trait;
use crate::event::EventSink;
use crate::output::ModelOutput;
use crate::request::ModelRequest;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ProviderError {
#[error("provider api error: {message}")]
Api {
message: String,
status: Option<u16>,
},
#[error("network error: {0}")]
Network(String),
#[error("invalid request: {0}")]
InvalidRequest(String),
#[error("provider request timed out")]
Timeout,
#[error("mock provider script exhausted")]
Exhausted,
#[error("{0}")]
Other(String),
}
impl ProviderError {
pub fn is_retryable(&self) -> bool {
match self {
ProviderError::Timeout | ProviderError::Network(_) => true,
ProviderError::Api {
status: Some(status),
..
} => matches!(status, 408 | 500..=599),
ProviderError::Api { status: None, .. }
| ProviderError::InvalidRequest(_)
| ProviderError::Exhausted
| ProviderError::Other(_) => false,
}
}
}
#[async_trait]
pub trait ModelProvider: Send + Sync {
async fn complete(
&self,
req: ModelRequest,
sink: &mut dyn EventSink,
) -> Result<ModelOutput, ProviderError>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn transport_and_server_failures_are_retryable() {
assert!(ProviderError::Timeout.is_retryable());
assert!(ProviderError::Network("connection reset".into()).is_retryable());
for status in [408, 500, 503, 529] {
let err = ProviderError::Api {
message: "gemini api error: overloaded".into(),
status: Some(status),
};
assert!(err.is_retryable(), "{status} retries");
}
}
#[test]
fn a_rate_limit_fails_fast() {
let err = ProviderError::Api {
message: "anthropic api error (HTTP 429): rate_limit_error".into(),
status: Some(429),
};
assert!(!err.is_retryable(), "429 does not retry");
}
#[test]
fn request_and_policy_failures_are_not() {
assert!(!ProviderError::InvalidRequest("no model".into()).is_retryable());
for status in [400, 401, 403, 404] {
let err = ProviderError::Api {
message: "openai-compatible api error: bad key".into(),
status: Some(status),
};
assert!(!err.is_retryable(), "{status} does not retry");
}
assert!(
!ProviderError::Api {
message: "malformed SSE chunk: expected `id`".into(),
status: None,
}
.is_retryable()
);
assert!(!ProviderError::Exhausted.is_retryable());
assert!(!ProviderError::Other("wire shape changed".into()).is_retryable());
}
}