use std::collections::HashSet;
use anyllm_translate::openai::{ChatCompletionRequest, ChatMessage, ChatRole};
use axum::extract::State;
use axum::http::{header, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::Json;
use serde::Deserialize;
use crate::handler::{
error_response, plan, run_full, status_for, AppState, GenError, Piece, RunControl,
};
use crate::observe::RequestTag;
use crate::ServerState;
fn created_at() -> String {
let secs = crate::handler::now_unix();
let days = (secs / 86_400) as i64;
let rem = secs % 86_400;
let (h, m, s) = (rem / 3600, (rem % 3600) / 60, rem % 60);
let z = days + 719_468;
let era = z.div_euclid(146_097);
let doe = z.rem_euclid(146_097);
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let mo = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if mo <= 2 { y + 1 } else { y };
format!("{y:04}-{mo:02}-{d:02}T{h:02}:{m:02}:{s:02}Z")
}
fn model_entry(row: &crate::registry::ModelRow) -> serde_json::Value {
serde_json::json!({
"name": row.id,
"model": row.id,
"modified_at": created_at(),
"size": 0,
"digest": "",
"details": {
"format": "gturbo",
"family": "turbospark",
"families": ["turbospark"],
"parameter_size": "",
"quantization_level": "",
},
"context_window": row.max_context,
})
}
pub async fn tags(State(state): State<ServerState>) -> Response {
Json(serde_json::json!({
"models": state.registry.rows().iter().map(model_entry).collect::<Vec<_>>(),
}))
.into_response()
}
pub async fn version() -> Response {
Json(serde_json::json!({ "version": env!("CARGO_PKG_VERSION") })).into_response()
}
#[derive(Deserialize)]
pub(crate) struct ShowRequest {
#[serde(alias = "name")]
model: Option<String>,
}
pub async fn show(State(state): State<ServerState>, Json(request): Json<ShowRequest>) -> Response {
let wanted = request.model.unwrap_or_default();
match state.registry.rows().into_iter().find(|r| r.id == wanted) {
Some(row) => Json(serde_json::json!({
"details": model_entry(&row)["details"],
"model_info": { "general.architecture": "turbospark",
"turbospark.context_length": row.max_context },
"capabilities": ["completion"],
}))
.into_response(),
None => error_response(StatusCode::NOT_FOUND, format!("model '{wanted}' not found")),
}
}
#[derive(Deserialize)]
pub(crate) struct OllamaMessage {
role: String,
#[serde(default)]
content: String,
}
#[derive(Deserialize)]
pub(crate) struct ChatRequest {
#[serde(default)]
model: String,
#[serde(default)]
messages: Vec<OllamaMessage>,
stream: Option<bool>,
#[serde(default)]
options: serde_json::Map<String, serde_json::Value>,
}
#[derive(Deserialize)]
pub(crate) struct GenerateRequest {
#[serde(default)]
model: String,
#[serde(default)]
prompt: String,
#[serde(default)]
system: Option<String>,
stream: Option<bool>,
#[serde(default)]
options: serde_json::Map<String, serde_json::Value>,
}
fn apply_options(
options: &serde_json::Map<String, serde_json::Value>,
request: &mut ChatCompletionRequest,
) {
if let Some(v) = options.get("temperature").and_then(|v| v.as_f64()) {
request.temperature = Some(v as f32);
}
if let Some(v) = options.get("top_p").and_then(|v| v.as_f64()) {
request.top_p = Some(v as f32);
}
if let Some(v) = options.get("num_predict").and_then(|v| v.as_i64()) {
if v >= 0 {
request.max_tokens = Some(u32::try_from(v).unwrap_or(u32::MAX));
}
}
for (theirs, ours) in [
("top_k", "top_k"),
("seed", "seed"),
("repeat_penalty", "repetition_penalty"),
] {
if let Some(v) = options.get(theirs) {
request.extra.insert(ours.to_string(), v.clone());
}
}
}
fn role_of(role: &str) -> ChatRole {
match role {
"system" => ChatRole::System,
"assistant" => ChatRole::Assistant,
"tool" => ChatRole::Tool,
_ => ChatRole::User,
}
}
fn chat_message(role: ChatRole, content: String) -> ChatMessage {
ChatMessage {
role,
content: Some(anyllm_translate::openai::ChatContent::Text(content)),
name: None,
tool_calls: None,
tool_call_id: None,
refusal: None,
reasoning_content: None,
thinking_blocks: None,
}
}
fn base_request(model: String, messages: Vec<ChatMessage>) -> ChatCompletionRequest {
ChatCompletionRequest {
model,
messages,
max_tokens: None,
max_completion_tokens: None,
temperature: None,
top_p: None,
stop: None,
tools: None,
tool_choice: None,
stream: None,
stream_options: None,
presence_penalty: None,
frequency_penalty: None,
response_format: None,
user: None,
parallel_tool_calls: None,
extra: Default::default(),
}
}
fn chat_object(model: &str, content: &str, done: Option<&Generated>) -> serde_json::Value {
let mut object = serde_json::json!({
"model": model,
"created_at": created_at(),
"message": { "role": "assistant", "content": content },
"done": done.is_some(),
});
if let Some(g) = done {
merge_counters(&mut object, g);
}
object
}
fn generate_object(model: &str, response: &str, done: Option<&Generated>) -> serde_json::Value {
let mut object = serde_json::json!({
"model": model,
"created_at": created_at(),
"response": response,
"done": done.is_some(),
});
if let Some(g) = done {
merge_counters(&mut object, g);
}
object
}
fn merge_counters(object: &mut serde_json::Value, g: &Generated) {
let map = object.as_object_mut().expect("object literal");
map.insert(
"done_reason".to_string(),
serde_json::json!(crate::response::ollama_done_reason(
g.decode.reason,
!g.calls.is_empty()
)),
);
map.insert(
"prompt_eval_count".to_string(),
serde_json::json!(g.decode.prompt_tokens),
);
map.insert(
"eval_count".to_string(),
serde_json::json!(g.decode.new_tokens),
);
let nanos = |secs: f64| (secs * 1_000_000_000.0).round() as u64;
map.insert("load_duration".to_string(), serde_json::json!(0));
map.insert(
"prompt_eval_duration".to_string(),
serde_json::json!(nanos(g.decode.prefill_seconds)),
);
map.insert(
"eval_duration".to_string(),
serde_json::json!(nanos(g.decode.decode_seconds)),
);
map.insert(
"total_duration".to_string(),
serde_json::json!(nanos(g.decode.prefill_seconds + g.decode.decode_seconds)),
);
}
fn ndjson(
lines: tokio::sync::mpsc::UnboundedReceiver<String>,
cancel: crate::cancel::Cancel,
) -> Response {
use futures::StreamExt;
let stream = crate::cancel::CancelOnDrop::new(
tokio_stream::wrappers::UnboundedReceiverStream::new(lines),
cancel,
)
.map(|line| Ok::<_, std::convert::Infallible>(axum::body::Bytes::from(line)));
(
[(header::CONTENT_TYPE, "application/x-ndjson")],
axum::body::Body::from_stream(stream),
)
.into_response()
}
fn one_object(object: serde_json::Value) -> Response {
Json(object).into_response()
}
use crate::handler::Generated;
pub async fn chat(
State(state): State<ServerState>,
tag: Option<axum::Extension<RequestTag>>,
lease_slot: axum::Extension<crate::handler::RequestLeaseSlot>,
Json(request): Json<ChatRequest>,
) -> Response {
let streaming = request.stream.unwrap_or(true);
let model = match crate::handler::resolve_backend(
&state,
&lease_slot.0,
tag.map(|t| t.0),
Some(request.model.as_str()),
streaming,
)
.await
{
Ok(m) => m,
Err(response) => return response,
};
let mut chat_request = base_request(
request.model.clone(),
request
.messages
.iter()
.map(|m| chat_message(role_of(&m.role), m.content.clone()))
.collect(),
);
apply_options(&request.options, &mut chat_request);
run(state, model, chat_request, request.model, streaming, false).await
}
pub async fn generate(
State(state): State<ServerState>,
tag: Option<axum::Extension<RequestTag>>,
lease_slot: axum::Extension<crate::handler::RequestLeaseSlot>,
Json(request): Json<GenerateRequest>,
) -> Response {
let streaming = request.stream.unwrap_or(true);
let model = match crate::handler::resolve_backend(
&state,
&lease_slot.0,
tag.map(|t| t.0),
Some(request.model.as_str()),
streaming,
)
.await
{
Ok(m) => m,
Err(response) => return response,
};
let mut messages = Vec::new();
if let Some(system) = request.system.filter(|s| !s.is_empty()) {
messages.push(chat_message(ChatRole::System, system));
}
messages.push(chat_message(ChatRole::User, request.prompt.clone()));
let mut chat_request = base_request(request.model.clone(), messages);
apply_options(&request.options, &mut chat_request);
run(state, model, chat_request, request.model, streaming, true).await
}
async fn run(
state: ServerState,
model: AppState,
chat_request: ChatCompletionRequest,
model_name: String,
streaming: bool,
raw: bool,
) -> Response {
let planned = match plan(&model, &chat_request) {
Ok(p) => p,
Err(e) => return error_response(StatusCode::BAD_REQUEST, e),
};
let effort = model.default_reasoning();
let cancel = crate::cancel::new_cancel();
let _ = &state;
if !streaming {
let gate = match model.generation_queue() {
Some(queue) => match queue.acquire(&cancel).await {
Some(permit) => Some(permit),
None => {
let g = Generated {
text: String::new(),
reasoning: String::new(),
calls: Vec::new(),
decode: crate::handler::cancelled_before_start(),
released_span_text: String::new(),
};
let object = if raw {
generate_object(&model_name, &g.text, Some(&g))
} else {
chat_object(&model_name, &g.text, Some(&g))
};
return one_object(object);
}
},
None => None,
};
let mut guard = crate::cancel::CancelGuard::new(cancel.clone());
let result = run_full(
model,
planned.prompt_ids,
planned.config,
planned.images,
HashSet::new(),
effort,
RunControl {
cancel,
permit: gate.clone(),
},
)
.await;
guard.defuse();
return match result {
Ok(g) => {
let object = if raw {
generate_object(&model_name, &g.text, Some(&g))
} else {
chat_object(&model_name, &g.text, Some(&g))
};
one_object(object)
}
Err(GenError::Runtime(e)) => error_response(status_for(&e), e.to_string()),
Err(GenError::Join(m)) => error_response(StatusCode::INTERNAL_SERVER_ERROR, m),
};
}
let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<String>();
let cancel_for_task = cancel.clone();
let gate = model.generation_queue();
let cancel_for_gate = cancel.clone();
tokio::spawn(async move {
let _ = crate::queue::run_gated(gate, &cancel_for_gate, move || {
let send = |object: serde_json::Value| -> bool {
if tx.send(format!("{object}\n")).is_err() {
cancel_for_task.store(true, std::sync::atomic::Ordering::Relaxed);
return false;
}
true
};
let flag = crate::cancel::as_cancel_flag(&cancel_for_task);
let mut emit = |piece: Piece| {
if let Piece::Text(delta) = piece {
let object = if raw {
generate_object(&model_name, &delta, None)
} else {
chat_object(&model_name, &delta, None)
};
send(object);
}
};
let result = crate::handler::stream_blocking(
&model,
&planned.prompt_ids,
&planned.config,
planned.images.as_ref(),
&HashSet::new(),
effort,
&flag,
&mut emit,
);
match result {
Ok((decode, _)) if decode.reason == runtime::StopReason::Cancelled => (),
Ok((decode, _)) => {
let g = Generated {
text: String::new(),
reasoning: String::new(),
calls: Vec::new(),
decode,
released_span_text: String::new(),
};
let object = if raw {
generate_object(&model_name, "", Some(&g))
} else {
chat_object(&model_name, "", Some(&g))
};
send(object);
}
Err(e) => {
send(serde_json::json!({
"model": model_name,
"created_at": created_at(),
"done": true,
"done_reason": "error",
"error": e.to_string(),
}));
}
}
})
.await;
});
ndjson(rx, cancel)
}
#[cfg(test)]
mod tests {
use super::*;
fn base() -> ChatCompletionRequest {
base_request("m".to_string(), Vec::new())
}
#[test]
fn num_predict_past_u32_max_saturates_rather_than_wraps() {
let mut request = base();
let options = serde_json::json!({"num_predict": (u32::MAX as i64) + 1000})
.as_object()
.unwrap()
.clone();
apply_options(&options, &mut request);
assert_eq!(request.max_tokens, Some(u32::MAX));
}
#[test]
fn num_predict_within_range_passes_through_unchanged() {
let mut request = base();
let options = serde_json::json!({"num_predict": 128})
.as_object()
.unwrap()
.clone();
apply_options(&options, &mut request);
assert_eq!(request.max_tokens, Some(128));
}
#[test]
fn num_predict_negative_one_is_still_left_absent() {
let mut request = base();
let options = serde_json::json!({"num_predict": -1})
.as_object()
.unwrap()
.clone();
apply_options(&options, &mut request);
assert_eq!(request.max_tokens, None);
}
}