pub mod actions;
use std::convert::Infallible;
use std::net::SocketAddr;
use std::sync::Arc;
use bytes::Bytes;
use http_body_util::{BodyExt, Full};
use hyper::body::Incoming;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Method, Request, Response, StatusCode};
use hyper_util::rt::TokioIo;
use serde_json::{json, Value};
use tokio::io::AsyncWriteExt;
use tokio::sync::mpsc;
use tracing::{debug, error, warn};
use crate::llm::action_helper::call_llm;
use crate::llm::ollama_client::OllamaClient;
use crate::logging::emit::Log;
use crate::protocol::Event;
use crate::server::connection::ConnectionId;
use crate::server::ollama::actions::{
OllamaProtocol, OLLAMA_CHAT_REQUEST_EVENT, OLLAMA_GENERATE_REQUEST_EVENT,
OLLAMA_MODELS_REQUEST_EVENT,
};
use crate::state::app_state::AppState;
const FIRST_BYTE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
const IDLE_BETWEEN_REQUESTS_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300);
const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
pub const CONNECTION_CAP_REFUSAL: &[u8] = b"HTTP/1.1 503 Service Unavailable\r\n\
Content-Type: application/json\r\nContent-Length: 39\r\nRetry-After: 5\r\n\
Connection: close\r\n\r\n\
{\"error\":\"server at connection limit\"}\n";
pub struct OllamaServer;
impl OllamaServer {
pub async fn spawn_with_llm_actions(
listen_addr: SocketAddr,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
_send_first: bool,
server_id: crate::state::ServerId,
first_byte_timeout_secs: Option<u64>,
idle_timeout_secs: Option<u64>,
) -> anyhow::Result<SocketAddr> {
let first_byte_timeout = first_byte_timeout_secs
.map(std::time::Duration::from_secs)
.unwrap_or(FIRST_BYTE_READ_TIMEOUT);
let idle_timeout = idle_timeout_secs
.map(std::time::Duration::from_secs)
.unwrap_or(IDLE_BETWEEN_REQUESTS_TIMEOUT);
let listener =
crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
let local_addr = listener.local_addr()?;
Log::new(Some(&status_tx)).info(format!("Ollama API server listening on {}", local_addr));
let protocol = Arc::new(OllamaProtocol::new());
let task_registrar = app_state.clone();
let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
let accept_handle = tokio::spawn(async move {
loop {
match crate::server::accept_bounded::accept_bounded(
&listener,
&limiter,
CONNECTION_CAP_REFUSAL,
"Ollama",
Some(&status_tx),
)
.await
{
Ok((stream, remote_addr, permit)) => {
let connection_id =
ConnectionId::new(app_state.get_next_unified_id().await);
let local_addr_conn = stream.local_addr().unwrap_or(local_addr);
Log::new(Some(&status_tx)).info(format!(
"Ollama API connection {} from {}",
connection_id, remote_addr
));
use crate::state::server::{
ConnectionState as ServerConnectionState, ConnectionStatus,
ProtocolConnectionInfo,
};
let now = crate::utils::clock::Instant::now();
let conn_state = ServerConnectionState {
id: connection_id,
remote_addr,
local_addr: local_addr_conn,
bytes_sent: 0,
bytes_received: 0,
packets_sent: 0,
packets_received: 0,
last_activity: now,
status: ConnectionStatus::Active,
status_changed_at: now,
protocol_info: ProtocolConnectionInfo::empty(),
};
app_state
.add_connection_to_server(server_id, conn_state)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
let llm_client_clone = llm_client.clone();
let app_state_clone = app_state.clone();
let status_tx_clone = status_tx.clone();
let protocol_clone = protocol.clone();
let task_owner = app_state.clone();
task_owner
.spawn_server_task(server_id, async move {
let _permit = permit;
let spoke = matches!(
tokio::time::timeout(
first_byte_timeout,
stream.peek(&mut [0u8; 1])
)
.await,
Ok(Ok(n)) if n > 0
);
if spoke {
let io = TokioIo::new(stream);
let status_for_service = status_tx_clone.clone();
let app_state_for_service = app_state_clone.clone();
let activity = Arc::new(
crate::server::accept_bounded::ConnectionActivity::new(),
);
let activity_for_service = Arc::clone(&activity);
let service = service_fn(move |req: Request<Incoming>| {
let llm_clone = llm_client_clone.clone();
let state_clone = app_state_for_service.clone();
let status_clone = status_for_service.clone();
let protocol_clone = protocol_clone.clone();
let activity = Arc::clone(&activity_for_service);
async move {
let _busy = activity.busy();
handle_ollama_request(
req,
connection_id,
llm_clone,
state_clone,
status_clone,
protocol_clone,
server_id,
)
.await
}
});
let conn = http1::Builder::new()
.serve_connection(io, service)
.without_shutdown();
tokio::pin!(conn);
tokio::select! {
result = &mut conn => {
match result {
Ok(parts) => {
let mut stream = parts.io.into_inner();
let _ = stream.shutdown().await;
crate::server::accept_bounded::drain_after_response(&mut stream).await;
}
Err(err) => error!(
"Error serving Ollama API connection: {:?}",
err
),
}
}
_ = crate::server::accept_bounded::watch_idle(
Arc::clone(&activity),
idle_timeout,
) => {
Log::new(Some(&status_tx_clone)).debug(format!(
"Ollama API connection {} idle for {}s; closing \
decision=fail_closed_idle_timeout",
connection_id,
idle_timeout.as_secs()
));
}
}
} else {
Log::new(Some(&status_tx_clone)).debug(format!(
"Ollama API peer {} sent no request within {}s; closing \
decision=fail_closed_first_byte_timeout",
remote_addr,
first_byte_timeout.as_secs()
));
}
app_state_clone
.close_connection_on_server(server_id, connection_id)
.await;
Log::new(Some(&status_tx_clone)).info(format!(
"Ollama API connection {} closed",
connection_id
));
let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
})
.await;
}
Err(e) => {
Log::new(Some(&status_tx))
.error(format!("Failed to accept Ollama API connection: {}", e));
break;
}
}
}
});
task_registrar
.register_server_task(server_id, accept_handle)
.await;
Ok(local_addr)
}
}
async fn handle_ollama_request(
req: Request<Incoming>,
connection_id: ConnectionId,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
protocol: Arc<OllamaProtocol>,
server_id: crate::state::ServerId,
) -> Result<Response<Full<Bytes>>, Infallible> {
let method = req.method().clone();
let uri = req.uri().clone();
let path = uri.path();
Log::new(Some(&status_tx)).debug(format!("Ollama API request: {} {}", method, path));
match (method.clone(), path) {
(Method::GET, "/api/tags") => {
handle_tags_list_v2(
connection_id,
llm_client,
app_state,
status_tx,
protocol,
server_id,
)
.await
}
(Method::POST, "/api/generate") => {
handle_generate_v2(
req,
connection_id,
llm_client,
app_state,
status_tx,
protocol,
server_id,
)
.await
}
(Method::POST, "/api/chat") => {
handle_chat_v2(
req,
connection_id,
llm_client,
app_state,
status_tx,
protocol,
server_id,
)
.await
}
(Method::POST, "/api/embeddings") => {
handle_embeddings(
req,
connection_id,
llm_client,
app_state,
status_tx,
protocol,
server_id,
)
.await
}
(Method::POST, "/api/show") => {
handle_show(
req,
connection_id,
llm_client,
app_state,
status_tx,
protocol,
server_id,
)
.await
}
(Method::POST, "/api/pull") => {
handle_admin(
"pull",
req,
connection_id,
llm_client,
app_state,
status_tx,
protocol,
server_id,
)
.await
}
(Method::POST, "/api/create") => {
handle_admin(
"create",
req,
connection_id,
llm_client,
app_state,
status_tx,
protocol,
server_id,
)
.await
}
(Method::POST, "/api/copy") => {
handle_admin(
"copy",
req,
connection_id,
llm_client,
app_state,
status_tx,
protocol,
server_id,
)
.await
}
(Method::DELETE, "/api/delete") => {
handle_admin(
"delete",
req,
connection_id,
llm_client,
app_state,
status_tx,
protocol,
server_id,
)
.await
}
_ => {
Log::new(Some(&status_tx))
.debug(format!("Ollama API: Unknown endpoint {} {}", method, path));
Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(
json!({
"error": "Not Found"
})
.to_string(),
)))
.unwrap())
}
}
}
async fn handle_tags_list_v2(
connection_id: ConnectionId,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
protocol: Arc<OllamaProtocol>,
server_id: crate::state::ServerId,
) -> Result<Response<Full<Bytes>>, Infallible> {
let log = Log::new(Some(&status_tx));
log.debug("Ollama API: Listing models (LLM controlled)");
let event = Event::new(&OLLAMA_MODELS_REQUEST_EVENT, json!({}));
match call_llm(
&llm_client,
&app_state,
server_id,
Some(connection_id),
&event,
protocol.as_ref(),
)
.await
{
Ok(llm_result) => {
for action in &llm_result.raw_actions {
if action.get("type").and_then(|v| v.as_str()) == Some("ollama_models_response") {
if let Some(models) = action.get("models").and_then(|v| v.as_array()) {
let ollama_models: Vec<Value> = models
.iter()
.map(|model_name| {
let name = model_name.as_str().unwrap_or("unknown");
json!({
"name": name,
"modified_at": "2024-01-01T00:00:00Z",
"size": 0,
"digest": "0000000000000000",
"details": {
"format": "gguf",
"family": "llama"
}
})
})
.collect();
let response = json!({
"models": ollama_models
});
return Ok(Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(response.to_string())))
.unwrap());
}
}
}
let response = json!({"models": []});
Ok(Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(response.to_string())))
.unwrap())
}
Err(e) => {
log.warn(format!("LLM error: {}", e));
Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(
json!({
"error": crate::utils::WireFailure::classify(&e).text()
})
.to_string(),
)))
.unwrap())
}
}
}
async fn handle_generate_v2(
req: Request<Incoming>,
connection_id: ConnectionId,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
protocol: Arc<OllamaProtocol>,
server_id: crate::state::ServerId,
) -> Result<Response<Full<Bytes>>, Infallible> {
let body_bytes = match read_body_limited(req, "/api/generate", &status_tx).await {
Ok(bytes) => bytes,
Err(response) => return Ok(response),
};
let request_json: Value = match serde_json::from_slice(&body_bytes) {
Ok(json) => json,
Err(e) => {
error!("Failed to parse JSON: {}", e);
return Ok(Response::builder()
.status(StatusCode::BAD_REQUEST)
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(
json!({"error": "Invalid JSON"}).to_string(),
)))
.unwrap());
}
};
let model = request_json
.get("model")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string();
let prompt = request_json
.get("prompt")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let stream = request_json
.get("stream")
.and_then(|v| v.as_bool())
.unwrap_or(false);
debug!(
"Generate: model={}, prompt_len={}, stream={}",
model,
prompt.len(),
stream
);
let event = Event::new(
&OLLAMA_GENERATE_REQUEST_EVENT,
json!({
"model": model,
"prompt": prompt,
"stream": stream
}),
);
match call_llm(
&llm_client,
&app_state,
server_id,
Some(connection_id),
&event,
protocol.as_ref(),
)
.await
{
Ok(llm_result) => {
for action in &llm_result.raw_actions {
if action.get("type").and_then(|v| v.as_str()) == Some("ollama_generate_response") {
if let Some(response_text) =
action.get("response_text").and_then(|v| v.as_str())
{
if stream {
return Ok(build_streaming_generate_response(&model, response_text));
} else {
let response = json!({
"model": model,
"created_at": "2024-01-01T00:00:00Z",
"response": response_text,
"done": true
});
return Ok(Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(response.to_string())))
.unwrap());
}
}
}
}
if let Some(refusal) = model_error_response(&llm_result.raw_actions) {
return Ok(refusal);
}
Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(
json!({"error": "No response from LLM"}).to_string(),
)))
.unwrap())
}
Err(e) => {
error!("LLM error: {}", e);
Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(
json!({"error": crate::utils::WireFailure::classify(&e).text()}).to_string(),
)))
.unwrap())
}
}
}
async fn handle_chat_v2(
req: Request<Incoming>,
connection_id: ConnectionId,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
protocol: Arc<OllamaProtocol>,
server_id: crate::state::ServerId,
) -> Result<Response<Full<Bytes>>, Infallible> {
let body_bytes = match read_body_limited(req, "/api/chat", &status_tx).await {
Ok(bytes) => bytes,
Err(response) => return Ok(response),
};
let request_json: Value = match serde_json::from_slice(&body_bytes) {
Ok(json) => json,
Err(e) => {
error!("Failed to parse JSON: {}", e);
return Ok(Response::builder()
.status(StatusCode::BAD_REQUEST)
.body(Full::new(Bytes::from(
json!({"error": "Invalid JSON"}).to_string(),
)))
.unwrap());
}
};
let model = request_json
.get("model")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string();
let messages = request_json
.get("messages")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let stream = request_json
.get("stream")
.and_then(|v| v.as_bool())
.unwrap_or(false);
debug!(
"Chat: model={}, {} messages, stream={}",
model,
messages.len(),
stream
);
let event = Event::new(
&OLLAMA_CHAT_REQUEST_EVENT,
json!({
"model": model,
"messages": messages,
"stream": stream
}),
);
match call_llm(
&llm_client,
&app_state,
server_id,
Some(connection_id),
&event,
protocol.as_ref(),
)
.await
{
Ok(llm_result) => {
for action in &llm_result.raw_actions {
if action.get("type").and_then(|v| v.as_str()) == Some("ollama_chat_response") {
if let Some(message_content) =
action.get("message_content").and_then(|v| v.as_str())
{
if stream {
return Ok(build_streaming_chat_response(&model, message_content));
} else {
let response = json!({
"model": model,
"created_at": "2024-01-01T00:00:00Z",
"message": {
"role": "assistant",
"content": message_content
},
"done": true
});
return Ok(Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(response.to_string())))
.unwrap());
}
}
}
}
if let Some(refusal) = model_error_response(&llm_result.raw_actions) {
return Ok(refusal);
}
Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Full::new(Bytes::from(
json!({"error": "No response from LLM"}).to_string(),
)))
.unwrap())
}
Err(e) => {
error!("LLM error: {}", e);
Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body(Full::new(Bytes::from(
json!({"error": crate::utils::WireFailure::classify(&e).text()}).to_string(),
)))
.unwrap())
}
}
}
pub fn model_error_response(raw_actions: &[serde_json::Value]) -> Option<Response<Full<Bytes>>> {
let action = raw_actions.iter().find(|action| {
action.get("type").and_then(|v| v.as_str()) == Some("ollama_error_response")
})?;
let error_message = action
.get("error_message")
.and_then(|v| v.as_str())
.unwrap_or("request refused");
let status = match action.get("status_code").and_then(|v| v.as_u64()) {
None => StatusCode::BAD_REQUEST,
Some(code) => u16::try_from(code)
.ok()
.filter(|code| (100..=599).contains(code))
.and_then(|code| StatusCode::from_u16(code).ok())
.filter(|status| !status.is_success())
.unwrap_or_else(|| {
warn!(
"Ollama ollama_error_response status_code {} is not a refusal status; \
answering 400 (use 4xx/5xx, e.g. 404 for an unknown model)",
code
);
StatusCode::BAD_REQUEST
}),
};
let body = json!({ "error": error_message }).to_string();
Some(
Response::builder()
.status(status)
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(body.clone())))
.unwrap_or_else(|_| {
let mut response = Response::new(Full::new(Bytes::from(body)));
*response.status_mut() = StatusCode::BAD_REQUEST;
response
}),
)
}
fn build_streaming_generate_response(model: &str, response_text: &str) -> Response<Full<Bytes>> {
let mut ndjson = String::new();
let words: Vec<&str> = response_text.split_whitespace().collect();
for (i, word) in words.iter().enumerate() {
let chunk = json!({
"model": model,
"created_at": "2024-01-01T00:00:00Z",
"response": if i == 0 { word.to_string() } else { format!(" {}", word) },
"done": false
});
ndjson.push_str(&chunk.to_string());
ndjson.push('\n');
}
let final_chunk = json!({
"model": model,
"created_at": "2024-01-01T00:00:00Z",
"response": "",
"done": true
});
ndjson.push_str(&final_chunk.to_string());
ndjson.push('\n');
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "application/x-ndjson")
.body(Full::new(Bytes::from(ndjson)))
.unwrap()
}
fn build_streaming_chat_response(model: &str, content: &str) -> Response<Full<Bytes>> {
let mut ndjson = String::new();
let words: Vec<&str> = content.split_whitespace().collect();
for (i, word) in words.iter().enumerate() {
let chunk = json!({
"model": model,
"created_at": "2024-01-01T00:00:00Z",
"message": {
"role": "assistant",
"content": if i == 0 { word.to_string() } else { format!(" {}", word) }
},
"done": false
});
ndjson.push_str(&chunk.to_string());
ndjson.push('\n');
}
let final_chunk = json!({
"model": model,
"created_at": "2024-01-01T00:00:00Z",
"message": {
"role": "assistant",
"content": ""
},
"done": true
});
ndjson.push_str(&final_chunk.to_string());
ndjson.push('\n');
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "application/x-ndjson")
.body(Full::new(Bytes::from(ndjson)))
.unwrap()
}
#[allow(clippy::too_many_arguments)]
async fn handle_embeddings(
req: Request<Incoming>,
connection_id: ConnectionId,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
protocol: Arc<OllamaProtocol>,
server_id: crate::state::ServerId,
) -> Result<Response<Full<Bytes>>, Infallible> {
let log = Log::new(Some(&status_tx));
let body_bytes = match read_body_limited(req, "/api/embeddings", &status_tx).await {
Ok(bytes) => bytes,
Err(response) => return Ok(response),
};
let request_json: Value = match serde_json::from_slice(&body_bytes) {
Ok(json) => json,
Err(_) => return Ok(bad_request("Invalid JSON")),
};
let model = request_json
.get("model")
.or_else(|| request_json.get("name"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let prompt = request_json
.get("prompt")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let event = Event::new(
&actions::OLLAMA_EMBEDDINGS_REQUEST_EVENT,
json!({ "model": model, "prompt": prompt }),
);
match call_llm(
&llm_client,
&app_state,
server_id,
Some(connection_id),
&event,
protocol.as_ref(),
)
.await
{
Ok(result) => {
for msg in result.messages {
let _ = status_tx.send(msg);
}
if let Some(response) = model_error_response(&result.raw_actions) {
log.info(format!(
"Ollama embeddings refused for '{}' (decision=model_reject)",
model
));
return Ok(response);
}
use crate::llm::actions::protocol_trait::ActionResult;
if let Some(data) = result.protocol_results.iter().find_map(|r| match r {
ActionResult::Custom { name, data } if name == "ollama_embeddings_response" => {
Some(data)
}
_ => None,
}) {
log.debug(format!("Ollama embeddings answered for '{}'", model));
return Ok(Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(data.to_string())))
.unwrap());
}
log.warn(format!(
"Ollama embeddings refused for '{}' (decision=fail_closed_no_action): the \
handler produced no ollama_embeddings_response",
model
));
Ok(server_error(
StatusCode::INTERNAL_SERVER_ERROR,
crate::utils::WireFailure::Unavailable.text(),
))
}
Err(e) => {
let failure = crate::utils::WireFailure::classify(&e);
error!(
"Ollama embeddings for '{}' decision=fail_closed_llm_error category={:?}: {:#}",
model, failure, e
);
log.warn(format!(
"Ollama embeddings refused for '{}' (decision=fail_closed_llm_error)",
model
));
let status = if failure.is_overloaded() {
StatusCode::SERVICE_UNAVAILABLE
} else {
StatusCode::INTERNAL_SERVER_ERROR
};
Ok(server_error(status, failure.text()))
}
}
}
#[allow(clippy::too_many_arguments)]
async fn handle_show(
req: Request<Incoming>,
connection_id: ConnectionId,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
protocol: Arc<OllamaProtocol>,
server_id: crate::state::ServerId,
) -> Result<Response<Full<Bytes>>, Infallible> {
let log = Log::new(Some(&status_tx));
let body_bytes = match read_body_limited(req, "/api/show", &status_tx).await {
Ok(bytes) => bytes,
Err(response) => return Ok(response),
};
let request_json: Value = serde_json::from_slice(&body_bytes).unwrap_or(Value::Null);
let model = request_json
.get("name")
.or_else(|| request_json.get("model"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let event = Event::new(
&actions::OLLAMA_SHOW_REQUEST_EVENT,
json!({ "model": model }),
);
match call_llm(
&llm_client,
&app_state,
server_id,
Some(connection_id),
&event,
protocol.as_ref(),
)
.await
{
Ok(result) => {
for msg in result.messages {
let _ = status_tx.send(msg);
}
if let Some(response) = model_error_response(&result.raw_actions) {
log.info(format!(
"Ollama show refused for '{}' (decision=model_reject)",
model
));
return Ok(response);
}
use crate::llm::actions::protocol_trait::ActionResult;
if let Some(data) = result.protocol_results.iter().find_map(|r| match r {
ActionResult::Custom { name, data } if name == "ollama_show_response" => Some(data),
_ => None,
}) {
log.debug(format!("Ollama show answered for '{}'", model));
return Ok(Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(data.to_string())))
.unwrap());
}
log.warn(format!(
"Ollama show refused for '{}' (decision=fail_closed_no_action): the handler \
produced no ollama_show_response",
model
));
Ok(server_error(
StatusCode::INTERNAL_SERVER_ERROR,
crate::utils::WireFailure::Unavailable.text(),
))
}
Err(e) => {
let failure = crate::utils::WireFailure::classify(&e);
error!(
"Ollama show for '{}' decision=fail_closed_llm_error category={:?}: {:#}",
model, failure, e
);
let status = if failure.is_overloaded() {
StatusCode::SERVICE_UNAVAILABLE
} else {
StatusCode::INTERNAL_SERVER_ERROR
};
Ok(server_error(status, failure.text()))
}
}
}
#[allow(clippy::too_many_arguments)]
async fn handle_admin(
operation: &str,
req: Request<Incoming>,
connection_id: ConnectionId,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
protocol: Arc<OllamaProtocol>,
server_id: crate::state::ServerId,
) -> Result<Response<Full<Bytes>>, Infallible> {
let log = Log::new(Some(&status_tx));
let body_bytes = match read_body_limited(req, operation, &status_tx).await {
Ok(bytes) => bytes,
Err(response) => return Ok(response),
};
let request_json: Value = serde_json::from_slice(&body_bytes).unwrap_or(Value::Null);
let model = request_json
.get("name")
.or_else(|| request_json.get("model"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let destination = request_json
.get("destination")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let event = Event::new(
&actions::OLLAMA_ADMIN_REQUEST_EVENT,
json!({
"operation": operation,
"model": model,
"destination": destination,
}),
);
match call_llm(
&llm_client,
&app_state,
server_id,
Some(connection_id),
&event,
protocol.as_ref(),
)
.await
{
Ok(result) => {
for msg in result.messages {
let _ = status_tx.send(msg);
}
if let Some(response) = model_error_response(&result.raw_actions) {
log.info(format!(
"Ollama {} refused for '{}' (decision=model_reject)",
operation, model
));
return Ok(response);
}
use crate::llm::actions::protocol_trait::ActionResult;
if let Some(data) = result.protocol_results.iter().find_map(|r| match r {
ActionResult::Custom { name, data } if name == "ollama_admin_ok" => Some(data),
_ => None,
}) {
log.info(format!(
"Ollama {} acknowledged for '{}' (decision=model_accept)",
operation, model
));
let mut body = json!({ "status": "success" });
if let Some(d) = data.get("digest") {
body["digest"] = d.clone();
}
if let Some(t) = data.get("total") {
body["total"] = t.clone();
}
return Ok(Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(body.to_string())))
.unwrap());
}
log.warn(format!(
"Ollama {} refused for '{}' (decision=fail_closed_no_action): the handler \
produced no ollama_admin_ok",
operation, model
));
Ok(server_error(
StatusCode::INTERNAL_SERVER_ERROR,
crate::utils::WireFailure::Unavailable.text(),
))
}
Err(e) => {
let failure = crate::utils::WireFailure::classify(&e);
error!(
"Ollama {} for '{}' decision=fail_closed_llm_error category={:?}: {:#}",
operation, model, failure, e
);
log.warn(format!(
"Ollama {} refused for '{}' (decision=fail_closed_llm_error)",
operation, model
));
let status = if failure.is_overloaded() {
StatusCode::SERVICE_UNAVAILABLE
} else {
StatusCode::INTERNAL_SERVER_ERROR
};
Ok(server_error(status, failure.text()))
}
}
}
pub const MAX_REQUEST_BODY_BYTES: usize = 8 * 1024 * 1024;
async fn read_body_limited(
req: Request<Incoming>,
endpoint: &str,
status_tx: &mpsc::UnboundedSender<String>,
) -> Result<Bytes, Response<Full<Bytes>>> {
let limited = http_body_util::Limited::new(req.into_body(), MAX_REQUEST_BODY_BYTES);
match limited.collect().await {
Ok(collected) => Ok(collected.to_bytes()),
Err(e) => {
Log::new(Some(status_tx)).warn(format!(
"Ollama {} decision=fail_closed_body_too_large: {} (limit {} bytes)",
endpoint, e, MAX_REQUEST_BODY_BYTES
));
Err(server_error(
StatusCode::PAYLOAD_TOO_LARGE,
"request body too large",
))
}
}
}
fn bad_request(message: &str) -> Response<Full<Bytes>> {
server_error(StatusCode::BAD_REQUEST, message)
}
fn server_error(status: StatusCode, message: &str) -> Response<Full<Bytes>> {
let body = json!({ "error": message }).to_string();
Response::builder()
.status(status)
.header("Content-Type", "application/json")
.body(Full::new(Bytes::from(body.clone())))
.unwrap_or_else(|_| {
let mut response = Response::new(Full::new(Bytes::from(body)));
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
response
})
}