providers-kit 0.0.2

Portable enum-dispatch Kit over multiple model provider APIs.
Documentation
use std::time::Duration;

use reqwest::Response;
use reqwest::header::RETRY_AFTER;
use tokio::time::Instant;

use crate::KitError;
use crate::config::RetryPolicy;
use crate::error::{ErrorKind, ErrorPhase, Operation, ValidationViolation};

#[derive(Clone, Debug)]
pub(crate) struct RequestExecution {
    timeout: Duration,
    deadline: Option<Instant>,
    retry: Option<RetryPolicy>,
    latest_rejection: Option<KitError>,
}

impl RequestExecution {
    pub(crate) fn fail_fast(timeout: Duration) -> Self {
        Self {
            timeout,
            deadline: None,
            retry: None,
            latest_rejection: None,
        }
    }

    pub(crate) fn retryable(timeout: Duration, policy: RetryPolicy) -> Self {
        Self {
            timeout,
            deadline: None,
            retry: Some(policy),
            latest_rejection: None,
        }
    }

    pub(crate) fn timeout(&self) -> Duration {
        self.timeout
    }

    pub(crate) fn through_deadline(mut self, deadline: Instant) -> Self {
        self.deadline = Some(deadline);
        self
    }

    pub(crate) fn record_rejection(&mut self, error: KitError) {
        self.latest_rejection = Some(error);
    }

    pub(crate) fn attach_rejection(&mut self, error: KitError) -> KitError {
        if error.kind() != ErrorKind::Timeout {
            return error;
        }
        match self.latest_rejection.take() {
            Some(rejection) => error.with_supplementary(rejection),
            None => error,
        }
    }

    pub(crate) fn deadline(&self) -> Result<Instant, KitError> {
        if let Some(deadline) = self.deadline {
            return Ok(deadline);
        }
        Instant::now().checked_add(self.timeout).ok_or_else(|| {
            KitError::config(
                ValidationViolation::OutOfRange {
                    field: "transport.timeout",
                    rule: "must fit the monotonic clock range",
                },
                "request timeout exceeds the supported clock range",
            )
        })
    }

    pub(crate) fn remaining_timeout(&self, deadline: Instant) -> Result<Duration, KitError> {
        let remaining = deadline
            .checked_duration_since(Instant::now())
            .ok_or_else(|| self.timeout_error())?;
        if remaining.is_zero() {
            return Err(self.timeout_error());
        }
        Ok(remaining)
    }

    pub(crate) fn timeout_error(&self) -> KitError {
        KitError::new(
            ErrorKind::Timeout,
            Operation::Unspecified,
            ErrorPhase::RequestSend,
        )
        .with_timeout(self.timeout)
    }

    pub(crate) fn retry_delay_for_response(
        &self,
        attempt: u32,
        response: &Response,
    ) -> Option<Duration> {
        let policy = self.retry.as_ref()?;
        let fallback_delay = policy.delay_for_attempt(attempt)?;
        let status = response.status().as_u16();
        if !matches!(status, 408 | 409 | 429 | 500 | 502 | 503 | 504 | 529) {
            return None;
        }
        if status == 429 {
            return response
                .headers()
                .get(RETRY_AFTER)
                .and_then(|value| value.to_str().ok())
                .and_then(|value| value.parse::<u64>().ok())
                .map(Duration::from_secs)
                .or(Some(fallback_delay));
        }
        Some(fallback_delay)
    }

    pub(crate) fn retry_delay_for_error(
        &self,
        attempt: u32,
        err: &reqwest::Error,
    ) -> Option<Duration> {
        if err.is_connect() || err.is_request() {
            return self.retry.as_ref()?.delay_for_attempt(attempt);
        }
        None
    }
}