kcode-intelligence-router 0.2.2

Typed routing for model calls with exact-model and per-user usage accounting
Documentation
use std::fmt;

use crate::UsageReceipt;

/// Broad failure category for callers that need to choose retry or user feedback.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ErrorKind {
    InvalidInput,
    Unavailable,
    Conflict,
    Cancelled,
    Provider,
    Internal,
}

/// A bounded, transport-independent intelligence failure.
#[derive(Debug)]
pub struct Error {
    kind: ErrorKind,
    code: &'static str,
    message: String,
    receipt: Option<Box<UsageReceipt>>,
}

impl Error {
    pub(crate) fn new(kind: ErrorKind, code: &'static str, message: impl Into<String>) -> Self {
        Self {
            kind,
            code,
            message: message.into(),
            receipt: None,
        }
    }

    pub fn invalid(message: impl Into<String>) -> Self {
        Self::new(ErrorKind::InvalidInput, "invalid_request", message)
    }

    pub(crate) fn unavailable(code: &'static str, message: impl Into<String>) -> Self {
        Self::new(ErrorKind::Unavailable, code, message)
    }

    pub(crate) fn conflict(code: &'static str, message: impl Into<String>) -> Self {
        Self::new(ErrorKind::Conflict, code, message)
    }

    pub(crate) fn cancelled() -> Self {
        Self::new(
            ErrorKind::Cancelled,
            "operation_cancelled",
            "The operation was stopped by the user.",
        )
    }

    pub(crate) fn provider(code: &'static str, message: impl Into<String>) -> Self {
        Self::new(ErrorKind::Provider, code, message)
    }

    pub(crate) fn internal(code: &'static str, message: impl Into<String>) -> Self {
        Self::new(ErrorKind::Internal, code, message)
    }

    pub fn kind(&self) -> ErrorKind {
        self.kind
    }

    pub fn code(&self) -> &'static str {
        self.code
    }

    pub fn message(&self) -> &str {
        &self.message
    }

    /// Canonical receipt recorded for a provider call that failed.
    pub fn receipt(&self) -> Option<&UsageReceipt> {
        self.receipt.as_deref()
    }

    pub(crate) fn with_receipt(mut self, receipt: UsageReceipt) -> Self {
        self.receipt = Some(Box::new(receipt));
        self
    }

    /// Whether retrying the provider operation can reasonably succeed.
    pub fn retryable(&self) -> bool {
        matches!(self.kind, ErrorKind::Provider | ErrorKind::Conflict)
            || (self.kind == ErrorKind::Unavailable && self.code != "provider_not_configured")
    }
}

impl fmt::Display for Error {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(&self.message)
    }
}

impl std::error::Error for Error {}

pub type Result<T> = std::result::Result<T, Error>;

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn retryability_distinguishes_configuration_from_transient_provider_failure() {
        assert!(!Error::unavailable("provider_not_configured", "missing key").retryable());
        assert!(Error::unavailable("provider_rate_limited", "try later").retryable());
        assert!(Error::provider("provider_timeout", "try later").retryable());
        assert!(!Error::invalid("bad request").retryable());
    }
}