Skip to main content

ollama_kit/
error.rs

1use ollama_rs::error::OllamaError;
2use reqwest::StatusCode;
3use thiserror::Error;
4
5/// Errors surfaced by the runtime layer (config, guards, and model lifecycle).
6#[derive(Debug, Error, Clone, PartialEq, Eq)]
7pub enum RuntimeError {
8    #[error("network error")]
9    Network,
10    #[error("request timed out")]
11    Timeout,
12    #[error("unauthorized")]
13    Unauthorized,
14    #[error("not found")]
15    NotFound,
16    #[error("model not found: {0}")]
17    ModelNotFound(String),
18    #[error("server error")]
19    ServerError,
20    #[error("{0}")]
21    Other(String),
22}
23
24pub type Result<T> = std::result::Result<T, RuntimeError>;
25
26/// Used by [`crate::guard::ExecutionGuard`] to decide whether another attempt is allowed.
27pub(crate) fn runtime_error_is_retryable(err: &RuntimeError) -> bool {
28    matches!(
29        err,
30        RuntimeError::Network | RuntimeError::Timeout | RuntimeError::ServerError
31    )
32}
33
34fn looks_like_model_missing_message(msg: &str) -> bool {
35    let m = msg.to_ascii_lowercase();
36    m.contains("model")
37        && (m.contains("not found")
38            || m.contains("unknown model")
39            || m.contains("does not exist")
40            || m.contains("pull"))
41}
42
43/// Whether an [`OllamaError`] should be retried by [`crate::guard::ExecutionGuard`].
44///
45/// Note: many Ollama HTTP failures are returned as [`OllamaError::Other`] with only a response
46/// body, so HTTP status cannot always be recovered. In those cases we do **not** retry, to avoid
47/// retrying client errors.
48pub(crate) fn ollama_error_is_retryable(err: &OllamaError) -> bool {
49    match err {
50        OllamaError::ReqwestError(e) => reqwest_error_is_retryable(e),
51        OllamaError::JsonError(_)
52        | OllamaError::InternalError(_)
53        | OllamaError::ToolCallError(_)
54        | OllamaError::Other(_) => false,
55    }
56}
57
58fn reqwest_error_is_retryable(err: &reqwest::Error) -> bool {
59    if err.is_timeout() || err.is_connect() {
60        return true;
61    }
62    if let Some(status) = err.status() {
63        return status.is_server_error();
64    }
65    false
66}
67
68pub(crate) fn map_ollama_error(err: OllamaError) -> RuntimeError {
69    match err {
70        OllamaError::ReqwestError(e) => map_reqwest_error(e),
71        OllamaError::JsonError(e) => RuntimeError::Other(e.to_string()),
72        OllamaError::InternalError(e) => {
73            if looks_like_model_missing_message(&e.message) {
74                RuntimeError::ModelNotFound(e.message)
75            } else {
76                RuntimeError::Other(e.message)
77            }
78        }
79        OllamaError::ToolCallError(e) => RuntimeError::Other(e.to_string()),
80        OllamaError::Other(s) => map_ollama_other_string(s),
81    }
82}
83
84fn map_ollama_other_string(s: String) -> RuntimeError {
85    if looks_like_model_missing_message(&s) {
86        return RuntimeError::ModelNotFound(s);
87    }
88    if let Ok(v) = serde_json::from_str::<serde_json::Value>(&s) {
89        if let Some(err) = v.get("error").and_then(|e| e.as_str()) {
90            if looks_like_model_missing_message(err) {
91                return RuntimeError::ModelNotFound(err.to_string());
92            }
93            return RuntimeError::Other(err.to_string());
94        }
95    }
96    RuntimeError::Other(s)
97}
98
99fn map_reqwest_error(err: reqwest::Error) -> RuntimeError {
100    if err.is_timeout() {
101        return RuntimeError::Timeout;
102    }
103    if err.is_connect() {
104        return RuntimeError::Network;
105    }
106    if let Some(status) = err.status() {
107        return map_http_status(status);
108    }
109    RuntimeError::Network
110}
111
112fn map_http_status(status: StatusCode) -> RuntimeError {
113    if status.is_server_error() {
114        return RuntimeError::ServerError;
115    }
116    match status {
117        StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => RuntimeError::Unauthorized,
118        StatusCode::NOT_FOUND => RuntimeError::NotFound,
119        _ => RuntimeError::Other(status.to_string()),
120    }
121}