feat: update test
CI / Rust CI (push) Successful in 4m35s
Publish & Deploy / Build and Push to Registry (push) Successful in 1m30s
Publish & Deploy / Deploy via SSH (push) Successful in 18s

This commit is contained in:
2026-04-10 22:26:45 +02:00
parent e31cdf131f
commit fc391b5d0e
6 changed files with 187 additions and 145 deletions
+4 -30
View File
@@ -1,8 +1,6 @@
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use crate::errors::OllamaError;
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct ModelsResponse {
pub models: Vec<ModelInfo>,
@@ -28,37 +26,13 @@ pub struct LoadModelBody {
pub keep_alive: Option<String>,
}
impl LoadModelBody {
pub fn parse_keep_alive(s: &str) -> Result<(), OllamaError> {
let s = s.trim();
if s == "-1" || s.parse::<u64>().is_ok() {
return Ok(());
}
let split = s
.find(|c: char| c.is_alphabetic())
.ok_or_else(|| OllamaError::InvalidKeepAlive(s.to_string()))?;
let (num, unit) = s.split_at(split);
num.parse::<u64>()
.map_err(|_| OllamaError::InvalidKeepAlive(s.to_string()))?;
match unit {
"s" | "m" | "h" => Ok(()),
_ => Err(OllamaError::InvalidKeepAlive(s.to_string())),
}
}
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct UnloadModelResponse {
pub model: String,
pub status: String,
}
#[derive(Debug, Deserialize, Serialize, ToSchema)]
#[derive(Debug, Deserialize, Serialize, ToSchema, Default)]
pub struct BaseLLMRequest {
pub model: String,
@@ -89,13 +63,13 @@ pub struct CompletionRequest {
pub prompt: String,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
#[derive(Debug, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CompletionObject {
TextCompletion,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
#[derive(Debug, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FinishReason {
Stop,
@@ -149,7 +123,7 @@ pub struct Message {
pub content: String,
}
#[derive(Debug, Deserialize, Serialize, ToSchema)]
#[derive(Debug, Deserialize, Serialize, ToSchema, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Role {
System,
+3
View File
@@ -15,6 +15,9 @@ pub enum OllamaError {
#[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 <number><unit> (e.g. 30s, 10m, 2h), a plain integer (seconds), or -1"
)]
+37 -3
View File
@@ -36,6 +36,10 @@ impl OllamaProvider {
Ok(res.models.iter().any(|m| m.name == model))
}
fn has_user_message(&self, messages: &[api::Message]) -> bool {
messages.iter().any(|m| matches!(m.role, api::Role::User))
}
fn extract_completion_params<'a>(
&self,
body: &'a api::CompletionRequest,
@@ -56,6 +60,28 @@ impl OllamaProvider {
Ok((&body.messages, model))
}
pub fn parse_keep_alive(&self, s: &str) -> Result<(), OllamaError> {
let s = s.trim();
if s == "-1" || s.parse::<u64>().is_ok() {
return Ok(());
}
let split = s
.find(|c: char| c.is_alphabetic())
.ok_or_else(|| OllamaError::InvalidKeepAlive(s.to_string()))?;
let (num, unit) = s.split_at(split);
num.parse::<u64>()
.map_err(|_| OllamaError::InvalidKeepAlive(s.to_string()))?;
match unit {
"s" | "m" | "h" => Ok(()),
_ => Err(OllamaError::InvalidKeepAlive(s.to_string())),
}
}
// // ── public endpoints ─────────────────────────────────────────────────────
pub async fn list_models(&self) -> Result<api::ModelsResponse, OllamaError> {
@@ -77,10 +103,14 @@ impl OllamaProvider {
pub async fn load_model(
&self,
model: &str,
keep_alive: &str,
keep_alive: Option<&str>,
) -> Result<api::LoadModelResponse, OllamaError> {
let url = format!("{}/api/generate", self.base_url);
let keep_alive = keep_alive.ok_or(OllamaError::MissingKeepAlive)?;
self.parse_keep_alive(keep_alive)?;
let exists = self.model_exists(model).await?;
if !exists {
return Err(OllamaError::ModelNotFound(model.to_string()));
@@ -99,7 +129,7 @@ impl OllamaProvider {
.json(&payload)
.send()
.await?
.json::<ollama::OllamaGenerateResponse>()
.text()
.await?;
Ok(api::LoadModelResponse {
@@ -130,7 +160,7 @@ impl OllamaProvider {
.json(&payload)
.send()
.await?
.json::<ollama::OllamaGenerateResponse>()
.text()
.await?;
Ok(api::UnloadModelResponse {
@@ -277,6 +307,10 @@ impl OllamaProvider {
return Err(OllamaError::MissingMessages);
}
if !self.has_user_message(&body.messages) {
return Err(OllamaError::MissingMessages);
}
if model.is_empty() {
return Err(OllamaError::MissingModel);
}
+4
View File
@@ -78,6 +78,10 @@ fn ollama_err(e: OllamaError) -> (axum::http::StatusCode, String) {
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}'"),
+5 -5
View File
@@ -20,13 +20,9 @@ pub async fn load_model(
Path(model): Path<String>,
Json(body): Json<api::LoadModelBody>,
) -> Result<Json<api::LoadModelResponse>, (axum::http::StatusCode, String)> {
let keep_alive = body.keep_alive.as_deref().unwrap_or("5m");
api::LoadModelBody::parse_keep_alive(keep_alive).map_err(ollama_err)?;
let response = state
.ollama
.load_model(&model, keep_alive)
.load_model(&model, body.keep_alive.as_deref())
.await
.map_err(ollama_err)?;
@@ -64,6 +60,10 @@ fn ollama_err(e: OllamaError) -> (axum::http::StatusCode, String) {
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"),