feat: add auto doc via utiopia
CI / Rust CI (push) Successful in 4m29s

This commit is contained in:
2026-04-10 15:45:41 +02:00
parent b3ad5249c0
commit a22560c337
10 changed files with 86 additions and 3 deletions
Generated
+27
View File
@@ -177,6 +177,7 @@ dependencies = [
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"utoipa",
"wiremock", "wiremock",
] ]
@@ -692,6 +693,8 @@ checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown",
"serde",
"serde_core",
] ]
[[package]] [[package]]
@@ -1801,6 +1804,30 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utoipa"
version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993"
dependencies = [
"indexmap",
"serde",
"serde_json",
"utoipa-gen",
]
[[package]]
name = "utoipa-gen"
version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b"
dependencies = [
"proc-macro2",
"quote",
"regex",
"syn",
]
[[package]] [[package]]
name = "walkdir" name = "walkdir"
version = "2.5.0" version = "2.5.0"
+1
View File
@@ -9,6 +9,7 @@ tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
[dependencies] [dependencies]
axum = "0.8.8" axum = "0.8.8"
utoipa = { version = "5.4.0", features = ["axum_extras"] }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
+3
View File
@@ -6,6 +6,9 @@ pub enum OllamaError {
#[error("prompt is required and cannot be empty")] #[error("prompt is required and cannot be empty")]
MissingPrompt, MissingPrompt,
#[error("model is required and cannot be empty")]
MissingModel,
#[error("messages must be a non-empty array containing at least one user message")] #[error("messages must be a non-empty array containing at least one user message")]
MissingMessages, MissingMessages,
+1
View File
@@ -1,5 +1,6 @@
mod auth; mod auth;
mod errors; mod errors;
mod openapi;
mod providers; mod providers;
mod routes; mod routes;
mod state; mod state;
+18
View File
@@ -0,0 +1,18 @@
use utoipa::OpenApi;
#[derive(OpenApi)]
#[openapi(
paths(
crate::routes::v1::chat::completions
),
components(
schemas(
// add your request/response structs here later
)
),
tags(
(name = "chat", description = "Chat endpoints"),
(name = "models", description = "Model management")
)
)]
pub struct V1ApiDoc;
+1 -1
View File
@@ -82,7 +82,7 @@ impl OllamaProvider {
let model = body let model = body
.get("model") .get("model")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or("llama3"); .ok_or(OllamaError::MissingModel)?;
Ok((prompt, model)) Ok((prompt, model))
} }
+11
View File
@@ -11,6 +11,13 @@ use serde_json::Value;
use crate::errors::OllamaError; use crate::errors::OllamaError;
use crate::state::app_state::AppState; use crate::state::app_state::AppState;
#[utoipa::path(
post,
path = "/chat/completions",
responses(
(status = 200, description = "Chat completion", body = Value),
)
)]
pub async fn completions( pub async fn completions(
State(state): State<AppState>, State(state): State<AppState>,
Json(body): Json<Value>, Json(body): Json<Value>,
@@ -73,6 +80,10 @@ fn ollama_err(e: OllamaError) -> (axum::http::StatusCode, String) {
axum::http::StatusCode::BAD_REQUEST, axum::http::StatusCode::BAD_REQUEST,
"prompt is required and cannot be empty".to_string(), "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) => ( OllamaError::ModelNotFound(m) => (
axum::http::StatusCode::UNPROCESSABLE_ENTITY, axum::http::StatusCode::UNPROCESSABLE_ENTITY,
format!("model '{m}' is not available — run `ollama pull {m}` first"), format!("model '{m}' is not available — run `ollama pull {m}` first"),
+12 -2
View File
@@ -1,16 +1,26 @@
pub mod chat; pub mod chat;
pub mod models; pub mod models;
mod openapi;
use crate::auth::middleware::auth_middleware; use crate::auth::middleware::auth_middleware;
use crate::state::app_state::AppState; use crate::state::app_state::AppState;
use axum::{Router, middleware, routing::get, routing::post}; use axum::{Router, middleware, routing::get, routing::post};
pub fn router() -> Router<AppState> { fn public_router() -> Router<AppState> {
Router::new().route("/openapi.json", get(openapi::openapi_json))
}
pub fn protected_router() -> Router<AppState> {
Router::new() Router::new()
.route("/models", get(models::list_models)) .route("/models", get(models::list_models))
.route("/completions", post(chat::completions)) .route("/completions", post(chat::completions))
.route("/chat/completions", post(chat::chat_completions)) .route("/chat/completions", post(chat::chat_completions))
.route("/models/{model}/load", post(models::load_model)) .route("/models/{model}/load", post(models::load_model))
.route("/models/{model}/unload", post(models::unload_model)) .route("/models/{model}/unload", post(models::unload_model))
.layer(middleware::from_fn(auth_middleware)) }
pub fn router() -> Router<AppState> {
Router::new()
.merge(public_router())
.merge(protected_router().layer(middleware::from_fn(auth_middleware)))
} }
+4
View File
@@ -53,6 +53,10 @@ fn ollama_err(e: OllamaError) -> (axum::http::StatusCode, String) {
axum::http::StatusCode::NOT_FOUND, axum::http::StatusCode::NOT_FOUND,
format!("model '{m}' not found — run `ollama pull {m}`"), 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 => ( OllamaError::MissingPrompt => (
axum::http::StatusCode::BAD_REQUEST, axum::http::StatusCode::BAD_REQUEST,
"prompt is required".to_string(), "prompt is required".to_string(),
+8
View File
@@ -0,0 +1,8 @@
use axum::Json;
use utoipa::OpenApi;
use crate::openapi::V1ApiDoc;
pub async fn openapi_json() -> Json<utoipa::openapi::OpenApi> {
Json(V1ApiDoc::openapi())
}