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: {0}")]
Api(String),
#[error("invalid request: {0}")]
InvalidRequest(String),
#[error("provider request timed out")]
Timeout,
#[error("{0}")]
Other(String),
}
impl ProviderError {
pub fn is_retryable(&self) -> bool {
match self {
ProviderError::Timeout => true,
ProviderError::Other(msg) => msg.starts_with("network error: "),
ProviderError::Api(msg) => {
matches!(http_status(msg), Some(408 | 429 | 500..=599))
}
ProviderError::InvalidRequest(_) => false,
}
}
}
fn http_status(msg: &str) -> Option<u16> {
let rest = msg.split("(HTTP ").nth(1)?;
let digits = rest.split(')').next()?;
digits.parse().ok()
}
#[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::Other("network error: connection reset".into()).is_retryable());
for status in [
"(HTTP 408)",
"(HTTP 429)",
"(HTTP 500)",
"(HTTP 503)",
"(HTTP 529)",
] {
let err = ProviderError::Api(format!("gemini api error {status}: overloaded"));
assert!(err.is_retryable(), "{status} retries");
}
}
#[test]
fn request_and_policy_failures_are_not() {
assert!(!ProviderError::InvalidRequest("no model".into()).is_retryable());
for status in ["(HTTP 400)", "(HTTP 401)", "(HTTP 403)", "(HTTP 404)"] {
let err = ProviderError::Api(format!("openai-compatible api error {status}: bad key"));
assert!(!err.is_retryable(), "{status} does not retry");
}
assert!(!ProviderError::Api("malformed SSE chunk: expected `id`".into()).is_retryable());
assert!(!ProviderError::Other("wire shape changed".into()).is_retryable());
}
#[test]
fn the_status_reader_takes_the_first_group_and_nothing_else() {
assert_eq!(http_status("x (HTTP 429): rate limited"), Some(429));
assert_eq!(
http_status("openrouter api error (HTTP 502): upstream said (HTTP 429)"),
Some(502)
);
assert_eq!(http_status("no status here"), None);
assert_eq!(http_status("(HTTP ): empty"), None);
assert_eq!(http_status("(HTTP not-a-number)"), None);
}
}