| use tokio::time::{sleep, Duration}; |
| use tracing::{debug, info}; |
| use axum::{http::StatusCode, response::{IntoResponse, Response}, Json, extract::State}; |
| use serde_json::{json, Value}; |
| use crate::proxy::server::AppState; |
|
|
| |
|
|
| |
| #[derive(Debug, Clone)] |
| pub enum RetryStrategy { |
| |
| NoRetry, |
| |
| FixedDelay(Duration), |
| |
| LinearBackoff { base_ms: u64 }, |
| |
| ExponentialBackoff { base_ms: u64, max_ms: u64 }, |
| |
| GraceRetry(Duration), |
| } |
|
|
| |
| pub fn determine_retry_strategy( |
| status_code: u16, |
| error_text: &str, |
| retried_without_thinking: bool, |
| ) -> RetryStrategy { |
| match status_code { |
| |
| 400 if !retried_without_thinking |
| && (error_text.contains("Invalid `signature`") |
| || error_text.contains("thinking.signature") |
| || error_text.contains("thinking.thinking") |
| || error_text.contains("Corrupted thought signature")) => |
| { |
| RetryStrategy::FixedDelay(Duration::from_millis(200)) |
| } |
|
|
| |
| 429 => { |
| |
| if let Some(delay_ms) = crate::proxy::upstream::retry::parse_retry_delay(error_text) { |
| |
| if crate::proxy::upstream::retry::should_grace_retry(delay_ms) { |
| let actual_delay = delay_ms.saturating_add(100); |
| tracing::info!("Grace Retry Triggered: Delay {}ms is within window, using same account", actual_delay); |
| RetryStrategy::GraceRetry(Duration::from_millis(actual_delay)) |
| } else { |
| let actual_delay = delay_ms.saturating_add(200).min(30_000); |
| RetryStrategy::FixedDelay(Duration::from_millis(actual_delay)) |
| } |
| } else { |
| |
| RetryStrategy::LinearBackoff { base_ms: 5000 } |
| } |
| } |
|
|
| |
| 503 | 529 => { |
| |
| RetryStrategy::ExponentialBackoff { |
| base_ms: 10000, |
| max_ms: 60000, |
| } |
| } |
|
|
| |
| 500 => { |
| |
| RetryStrategy::LinearBackoff { base_ms: 3000 } |
| } |
|
|
| |
| 401 | 403 => RetryStrategy::FixedDelay(Duration::from_millis(200)), |
|
|
| |
| |
| 404 => RetryStrategy::FixedDelay(Duration::from_millis(300)), |
|
|
| |
| _ => RetryStrategy::NoRetry, |
| } |
| } |
|
|
| |
| pub async fn apply_retry_strategy( |
| strategy: RetryStrategy, |
| attempt: usize, |
| max_attempts: usize, |
| status_code: u16, |
| trace_id: &str, |
| ) -> bool { |
| match strategy { |
| RetryStrategy::NoRetry => { |
| debug!("[{}] Non-retryable error {}, stopping", trace_id, status_code); |
| false |
| } |
|
|
| RetryStrategy::FixedDelay(duration) => { |
| let base_ms = duration.as_millis() as u64; |
| info!( |
| "[{}] ⏱️ Retry with fixed delay: status={}, attempt={}/{}, delay={}ms", |
| trace_id, |
| status_code, |
| attempt + 1, |
| max_attempts, |
| base_ms |
| ); |
| sleep(duration).await; |
| true |
| } |
|
|
| RetryStrategy::LinearBackoff { base_ms } => { |
| let calculated_ms = base_ms * (attempt as u64 + 1); |
| info!( |
| "[{}] ⏱️ Retry with linear backoff: status={}, attempt={}/{}, delay={}ms", |
| trace_id, |
| status_code, |
| attempt + 1, |
| max_attempts, |
| calculated_ms |
| ); |
| sleep(Duration::from_millis(calculated_ms)).await; |
| true |
| } |
|
|
| RetryStrategy::ExponentialBackoff { base_ms, max_ms } => { |
| let calculated_ms = (base_ms * 2_u64.pow(attempt as u32)).min(max_ms); |
| info!( |
| "[{}] ⏱️ Retry with exponential backoff: status={}, attempt={}/{}, delay={}ms", |
| trace_id, |
| status_code, |
| attempt + 1, |
| max_attempts, |
| calculated_ms |
| ); |
| sleep(Duration::from_millis(calculated_ms)).await; |
| true |
| } |
|
|
| RetryStrategy::GraceRetry(duration) => { |
| info!( |
| "[{}] ⚡ Grace Retry: Performing micro-wait ({}ms) on current account...", |
| trace_id, |
| duration.as_millis() |
| ); |
| sleep(duration).await; |
| true |
| } |
| } |
| } |
|
|
| |
| pub fn should_rotate_account(status_code: u16, strategy: Option<&RetryStrategy>) -> bool { |
| |
| if let Some(RetryStrategy::GraceRetry(_)) = strategy { |
| return false; |
| } |
|
|
| match status_code { |
| |
| 429 | 401 | 403 | 404 | 500 => true, |
| |
| 503 | 529 => false, |
| _ => false, |
| } |
| } |
|
|
| |
| |
| pub async fn handle_detect_model( |
| State(state): State<AppState>, |
| Json(body): Json<Value>, |
| ) -> Response { |
| let model_name = body.get("model").and_then(|v| v.as_str()).unwrap_or(""); |
| |
| if model_name.is_empty() { |
| return (StatusCode::BAD_REQUEST, "Missing 'model' field").into_response(); |
| } |
|
|
| |
| let mapped_model = crate::proxy::common::model_mapping::resolve_model_route( |
| model_name, |
| &*state.custom_mapping.read().await, |
| ); |
|
|
| |
| let config = crate::proxy::mappers::common_utils::resolve_request_config( |
| model_name, |
| &mapped_model, |
| &None, |
| None, |
| None, |
| None, |
| None, |
| ); |
|
|
| |
| let mut response = json!({ |
| "model": model_name, |
| "mapped_model": mapped_model, |
| "type": config.request_type, |
| "features": { |
| "has_web_search": config.inject_google_search, |
| "is_image_gen": config.request_type == "image_gen" |
| } |
| }); |
|
|
| if let Some(img_conf) = config.image_config { |
| if let Some(obj) = response.as_object_mut() { |
| obj.insert("config".to_string(), img_conf); |
| } |
| } |
|
|
| Json(response).into_response() |
| } |
|
|