From b77715268d9ebd04b7593602a554104889405e8d Mon Sep 17 00:00:00 2001 From: LucasDLTG Date: Mon, 11 May 2026 11:16:16 +0200 Subject: [PATCH] refactor: ollama errors centralized --- src/errors.rs | 28 ---------------- src/lib.rs | 1 - src/main.rs | 1 - src/providers/ollama/client.rs | 2 +- src/providers/ollama/errors.rs | 58 ++++++++++++++++++++++++++++++++++ src/providers/ollama/mod.rs | 1 + src/routes/v1/chat.rs | 40 +++-------------------- src/routes/v1/models.rs | 39 +++-------------------- tests/ollama_provider.rs | 19 +++++------ 9 files changed, 78 insertions(+), 111 deletions(-) delete mode 100644 src/errors.rs create mode 100644 src/providers/ollama/errors.rs diff --git a/src/errors.rs b/src/errors.rs deleted file mode 100644 index e380c84..0000000 --- a/src/errors.rs +++ /dev/null @@ -1,28 +0,0 @@ -// errors.rs -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum OllamaError { - #[error("prompt is required and cannot be empty")] - MissingPrompt, - - #[error("model is required and cannot be empty")] - MissingModel, - - #[error("messages must be a non-empty array containing at least one user message")] - MissingMessages, - - #[error("model '{0}' is not available — run `ollama pull {0}` first")] - ModelNotFound(String), - - #[error("keep_alive is required and cannot be empty")] - MissingKeepAlive, - - #[error( - "invalid keep_alive format '{0}' — expected (e.g. 30s, 10m, 2h), a plain integer (seconds), or -1" - )] - InvalidKeepAlive(String), - - #[error(transparent)] - Http(#[from] reqwest::Error), -} diff --git a/src/lib.rs b/src/lib.rs index 5abe37d..fe7cb5c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,5 @@ pub mod databases; pub mod dto; -pub mod errors; pub mod middlewares; pub mod providers; pub mod state; diff --git a/src/main.rs b/src/main.rs index b9ed9c7..2ada6cb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,6 @@ mod databases; mod docs; mod dto; -mod errors; mod middlewares; mod providers; mod routes; diff --git a/src/providers/ollama/client.rs b/src/providers/ollama/client.rs index 8719f69..50a63be 100644 --- a/src/providers/ollama/client.rs +++ b/src/providers/ollama/client.rs @@ -1,5 +1,5 @@ +use super::errors::OllamaError; use crate::dto::{api, ollama}; -use crate::errors::OllamaError; use axum::response::sse::Event; use futures::StreamExt; use reqwest::Client; diff --git a/src/providers/ollama/errors.rs b/src/providers/ollama/errors.rs new file mode 100644 index 0000000..2484a95 --- /dev/null +++ b/src/providers/ollama/errors.rs @@ -0,0 +1,58 @@ +// errors.rs +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum OllamaError { + #[error("prompt is required and cannot be empty")] + MissingPrompt, + + #[error("model is required and cannot be empty")] + MissingModel, + + #[error("messages must be a non-empty array containing at least one user message")] + MissingMessages, + + #[error("model '{0}' is not available — run `ollama pull {0}` first")] + ModelNotFound(String), + + #[error("keep_alive is required and cannot be empty")] + MissingKeepAlive, + + #[error( + "invalid keep_alive format '{0}' — expected (e.g. 30s, 10m, 2h), a plain integer (seconds), or -1" + )] + InvalidKeepAlive(String), + + #[error(transparent)] + Http(#[from] reqwest::Error), +} + +pub fn into_http_response(e: OllamaError) -> (axum::http::StatusCode, String) { + match e { + OllamaError::MissingPrompt => ( + axum::http::StatusCode::BAD_REQUEST, + "prompt is required and cannot be empty".to_string(), + ), + OllamaError::MissingModel => ( + axum::http::StatusCode::BAD_REQUEST, + "model is required and cannot be empty".to_string(), + ), + OllamaError::ModelNotFound(m) => ( + axum::http::StatusCode::UNPROCESSABLE_ENTITY, + format!("model '{m}' is not available — run `ollama pull {m}` first"), + ), + OllamaError::MissingKeepAlive => ( + axum::http::StatusCode::BAD_REQUEST, + "keep alive is required and cannot be empty".to_string(), + ), + OllamaError::InvalidKeepAlive(v) => ( + axum::http::StatusCode::BAD_REQUEST, + format!("invalid keep_alive '{v}'"), + ), + OllamaError::MissingMessages => ( + axum::http::StatusCode::BAD_REQUEST, + "messages array with at least one user message is required".to_string(), + ), + OllamaError::Http(e) => (axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), + } +} diff --git a/src/providers/ollama/mod.rs b/src/providers/ollama/mod.rs index 66ad831..d04f432 100644 --- a/src/providers/ollama/mod.rs +++ b/src/providers/ollama/mod.rs @@ -1,2 +1,3 @@ pub mod client; +pub mod errors; pub mod mapper; diff --git a/src/routes/v1/chat.rs b/src/routes/v1/chat.rs index 90fc1ba..0bb00f5 100644 --- a/src/routes/v1/chat.rs +++ b/src/routes/v1/chat.rs @@ -8,7 +8,7 @@ use axum::{ }; use crate::dto::api; -use crate::errors::OllamaError; +use crate::providers::ollama::errors::into_http_response; use crate::state::app_state::AppState; #[utoipa::path( @@ -54,7 +54,7 @@ pub async fn completions( tracing::debug!("Received /completion with body {:?}", body); if body.base.stream { let stream = state.ollama.completions_stream(&body).await.map_err(|e| { - let (code, msg) = ollama_err(e); + let (code, msg) = into_http_response(e); (code, Json(api::ErrorResponse::new(msg))) })?; @@ -63,7 +63,7 @@ pub async fn completions( .into_response()) } else { let response = state.ollama.completions(&body).await.map_err(|e| { - let (code, msg) = ollama_err(e); + let (code, msg) = into_http_response(e); (code, Json(api::ErrorResponse::new(msg))) })?; @@ -123,7 +123,7 @@ pub async fn chat_completions( .ollama .chat_completions_stream(&body) .await - .map_err(ollama_err)?; + .map_err(into_http_response)?; Ok(Sse::new(stream) .keep_alive(KeepAlive::default()) @@ -133,38 +133,8 @@ pub async fn chat_completions( .ollama .chat_completions(&body) .await - .map_err(ollama_err)?; + .map_err(into_http_response)?; Ok(Json(response).into_response()) } } - -fn ollama_err(e: OllamaError) -> (axum::http::StatusCode, String) { - match e { - OllamaError::MissingPrompt => ( - axum::http::StatusCode::BAD_REQUEST, - "prompt is required and cannot be empty".to_string(), - ), - OllamaError::MissingModel => ( - axum::http::StatusCode::BAD_REQUEST, - "model is required and cannot be empty".to_string(), - ), - OllamaError::ModelNotFound(m) => ( - axum::http::StatusCode::UNPROCESSABLE_ENTITY, - format!("model '{m}' is not available — run `ollama pull {m}` first"), - ), - OllamaError::MissingKeepAlive => ( - axum::http::StatusCode::BAD_REQUEST, - "keep alive is required and cannot be empty".to_string(), - ), - OllamaError::InvalidKeepAlive(v) => ( - axum::http::StatusCode::BAD_REQUEST, - format!("invalid keep_alive '{v}'"), - ), - OllamaError::MissingMessages => ( - axum::http::StatusCode::BAD_REQUEST, - "messages array with at least one user message is required".to_string(), - ), - OllamaError::Http(e) => (axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), - } -} diff --git a/src/routes/v1/models.rs b/src/routes/v1/models.rs index 58a1d7a..39ae278 100644 --- a/src/routes/v1/models.rs +++ b/src/routes/v1/models.rs @@ -4,7 +4,8 @@ use axum::{ }; use crate::dto::api; -use crate::{errors::OllamaError, state::app_state::AppState}; +use crate::providers::ollama::errors::into_http_response; +use crate::state::app_state::AppState; #[utoipa::path( get, @@ -30,7 +31,7 @@ pub async fn list_models( ) -> Result, (axum::http::StatusCode, String)> { match state.ollama.list_models().await { Ok(models) => Ok(Json(models)), - Err(e) => Err(ollama_err(e)), + Err(e) => Err(into_http_response(e)), } } @@ -86,7 +87,7 @@ pub async fn load_model( .ollama .load_model(&model, body.keep_alive.as_deref()) .await - .map_err(ollama_err)?; + .map_err(into_http_response)?; Ok(Json(response)) } @@ -127,37 +128,7 @@ pub async fn unload_model( .ollama .unload_model(&model) .await - .map_err(ollama_err)?; + .map_err(into_http_response)?; Ok(Json(response)) } - -fn ollama_err(e: OllamaError) -> (axum::http::StatusCode, String) { - match e { - OllamaError::ModelNotFound(m) => ( - axum::http::StatusCode::NOT_FOUND, - format!("model '{m}' not found — run `ollama pull {m}`"), - ), - OllamaError::MissingModel => ( - axum::http::StatusCode::BAD_REQUEST, - "model is required and cannot be empty".to_string(), - ), - OllamaError::MissingPrompt => ( - axum::http::StatusCode::BAD_REQUEST, - "prompt is required".to_string(), - ), - OllamaError::MissingMessages => ( - axum::http::StatusCode::BAD_REQUEST, - "messages array with at least one user message is required".to_string(), - ), - OllamaError::MissingKeepAlive => ( - axum::http::StatusCode::BAD_REQUEST, - "keep alive is required and cannot be empty".to_string(), - ), - OllamaError::InvalidKeepAlive(v) => ( - axum::http::StatusCode::BAD_REQUEST, - format!("invalid keep_alive '{v}' — use 30s / 10m / 2h, a plain integer, or -1"), - ), - OllamaError::Http(e) => (axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), - } -} diff --git a/tests/ollama_provider.rs b/tests/ollama_provider.rs index ce7c819..ecc557c 100644 --- a/tests/ollama_provider.rs +++ b/tests/ollama_provider.rs @@ -3,8 +3,8 @@ use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; use chat::dto::api; -use chat::errors::OllamaError; use chat::providers::ollama::client::OllamaProvider; +use chat::providers::ollama::errors::OllamaError; // ── helpers ────────────────────────────────────────────────────────────────── @@ -147,7 +147,7 @@ async fn test_completions_model_not_found() { let err = provider.completions(&req).await.unwrap_err(); - assert!(matches!(err, chat::errors::OllamaError::ModelNotFound(_))); + assert!(matches!(err, OllamaError::ModelNotFound(_))); } // ── chat_completions ────────────────────────────────────────────────────────── @@ -223,7 +223,7 @@ async fn test_chat_completions_missing_messages() { let err = provider.chat_completions(&req).await.unwrap_err(); - assert!(matches!(err, chat::errors::OllamaError::MissingMessages)); + assert!(matches!(err, OllamaError::MissingMessages)); } #[tokio::test] @@ -250,7 +250,7 @@ async fn test_chat_completions_no_user_message() { let err = provider.chat_completions(&req).await.unwrap_err(); - assert!(matches!(err, chat::errors::OllamaError::MissingMessages)); + assert!(matches!(err, OllamaError::MissingMessages)); } #[tokio::test] @@ -277,7 +277,7 @@ async fn test_chat_completions_model_not_found() { let err = provider.chat_completions(&req).await.unwrap_err(); - assert!(matches!(err, chat::errors::OllamaError::ModelNotFound(_))); + assert!(matches!(err, OllamaError::ModelNotFound(_))); } // // ── load_model ──────────────────────────────────────────────────────────────── @@ -321,7 +321,7 @@ async fn test_load_model_not_found() { let err = provider.load_model("gpt-4", Some("10m")).await.unwrap_err(); - assert!(matches!(err, chat::errors::OllamaError::ModelNotFound(_))); + assert!(matches!(err, OllamaError::ModelNotFound(_))); } #[tokio::test] @@ -339,10 +339,7 @@ async fn test_load_model_invalid_keep_alive() { .await .unwrap_err(); - assert!(matches!( - err, - chat::errors::OllamaError::InvalidKeepAlive(_) - )); + assert!(matches!(err, OllamaError::InvalidKeepAlive(_))); } #[tokio::test] @@ -414,5 +411,5 @@ async fn test_unload_model_not_found() { .await; let err = provider.unload_model("gpt-4").await.unwrap_err(); - assert!(matches!(err, chat::errors::OllamaError::ModelNotFound(_))); + assert!(matches!(err, OllamaError::ModelNotFound(_))); }