1use ollama_rs::error::OllamaError;
2use reqwest::StatusCode;
3use thiserror::Error;
4
5#[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
26pub(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
43pub(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}