use crate::error::{Result, ShimError};
use crate::log::{LogEntry, Logger, RequestTimer};
use crate::router::Router;
use crate::SHARED_CLIENT;
use serde_json::Value;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct FallbackConfig {
pub models: Vec<String>,
pub max_retries: u32,
pub initial_backoff: Duration,
pub retryable_statuses: Vec<u16>,
}
impl Default for FallbackConfig {
fn default() -> Self {
Self {
models: Vec::new(),
max_retries: 2,
initial_backoff: Duration::from_millis(500),
retryable_statuses: vec![429, 500, 502, 503, 529],
}
}
}
impl FallbackConfig {
pub fn new(models: Vec<String>) -> Self {
Self {
models,
..Default::default()
}
}
pub fn max_retries(mut self, n: u32) -> Self {
self.max_retries = n;
self
}
pub fn initial_backoff(mut self, d: Duration) -> Self {
self.initial_backoff = d;
self
}
}
fn is_retryable(err: &ShimError, retryable_statuses: &[u16]) -> bool {
match err {
ShimError::ProviderError { status, .. } => retryable_statuses.contains(status),
ShimError::Http(_) => true, _ => false,
}
}
pub async fn completion_with_fallback(
router: &Router,
request: &Value,
config: &FallbackConfig,
logger: Option<&Logger>,
) -> Result<Value> {
let models = if config.models.is_empty() {
vec![request
.get("model")
.and_then(|m| m.as_str())
.ok_or(ShimError::MissingModel)?
.to_string()]
} else {
config.models.clone()
};
let mut errors: Vec<String> = Vec::new();
let client = &*SHARED_CLIENT;
for model_str in &models {
let mut req = request.clone();
req["model"] = Value::String(model_str.clone());
let (provider, model) = match router.resolve(model_str) {
Ok(r) => r,
Err(e) => {
errors.push(format!("{}: {}", model_str, e));
continue;
}
};
let mut backoff = config.initial_backoff;
for attempt in 0..=config.max_retries {
let timer = RequestTimer::start();
match client.completion(provider, &model, &req).await {
Ok(result) => {
if let Some(logger) = logger {
logger.log(&LogEntry::from_response(
provider.name(),
model_str,
&result,
timer.elapsed(),
));
}
return Ok(result);
}
Err(e) => {
if is_retryable(&e, &config.retryable_statuses) && attempt < config.max_retries
{
errors.push(format!("{} (attempt {}): {}", model_str, attempt + 1, e));
tokio::time::sleep(backoff).await;
backoff *= 2;
continue;
}
if let Some(logger) = logger {
logger.log(&LogEntry::from_error(
provider.name(),
model_str,
&e.to_string(),
timer.elapsed(),
));
}
errors.push(format!("{}: {}", model_str, e));
break; }
}
}
}
Err(ShimError::AllFailed(errors))
}