Skip to main content

kcode_gemini_api/
error.rs

1use std::{error::Error as StdError, fmt};
2
3use crate::{LimitPeriod, Money};
4
5/// The result type returned by this crate.
6pub type Result<T> = std::result::Result<T, Error>;
7
8/// A local validation, accounting, transport, provider, or protocol failure.
9#[derive(Debug)]
10pub enum Error {
11    /// The API key was empty or invalid as an HTTP header.
12    InvalidApiKey,
13    /// The caller supplied an invalid request.
14    InvalidInput(String),
15    /// The in-memory session accounting state could not be used.
16    Accounting(String),
17    /// The Gemini service could not be reached or timed out.
18    Transport(String),
19    /// Gemini rejected the request.
20    Provider {
21        /// HTTP status returned by Gemini.
22        status: u16,
23        /// Sanitized provider classification, when present.
24        code: Option<String>,
25        /// Sanitized provider message.
26        message: String,
27    },
28    /// A successful HTTP response did not satisfy the Gemini protocol.
29    Protocol(String),
30    /// A local spending window has reached its configured limit.
31    SpendingLimitReached {
32        /// UTC accounting period that blocked the request.
33        period: LimitPeriod,
34        /// Configured limit.
35        limit: Money,
36        /// Accounted spend in the current period.
37        spent: Money,
38    },
39}
40
41impl fmt::Display for Error {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        match self {
44            Self::InvalidApiKey => f.write_str("the Gemini API key is empty or invalid"),
45            Self::InvalidInput(message) => write!(f, "invalid Gemini request: {message}"),
46            Self::Accounting(message) => write!(f, "Gemini accounting failed: {message}"),
47            Self::Transport(message) => write!(f, "Gemini transport failed: {message}"),
48            Self::Provider {
49                status,
50                code,
51                message,
52            } => match code {
53                Some(code) => write!(f, "Gemini returned HTTP {status} ({code}): {message}"),
54                None => write!(f, "Gemini returned HTTP {status}: {message}"),
55            },
56            Self::Protocol(message) => write!(f, "invalid Gemini response: {message}"),
57            Self::SpendingLimitReached {
58                period,
59                limit,
60                spent,
61            } => write!(
62                f,
63                "Gemini {period} spending limit reached (spent {spent}, limit {limit})"
64            ),
65        }
66    }
67}
68
69impl StdError for Error {}
70
71pub(crate) fn transport(error: impl fmt::Display) -> Error {
72    let message = error.to_string();
73    Error::Transport(if message.to_ascii_lowercase().contains("timed out") {
74        "the request timed out".into()
75    } else {
76        "the service could not be reached".into()
77    })
78}
79
80pub(crate) fn clean_message(value: &str, limit: usize) -> String {
81    let value = value
82        .chars()
83        .map(|character| {
84            if character.is_control() {
85                ' '
86            } else {
87                character
88            }
89        })
90        .take(limit)
91        .collect::<String>()
92        .split_whitespace()
93        .collect::<Vec<_>>()
94        .join(" ");
95    if value.is_empty() {
96        "unknown error".into()
97    } else {
98        value
99    }
100}