use crate::main_modules::model_host::CurrentModel;
use axum::extract::rejection::JsonRejection;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Json, Response};
use serde::{Deserialize, Serialize};
use crate::AppState;
use crate::api::compact::openai_error_response;
pub async fn resolve_request_adapter_slot(
state: &AppState,
adapter: Option<&str>,
model: &str,
) -> Result<i32, Response> {
let selector = match adapter {
Some(a) => Some(a),
None if state.adapter_names.iter().any(|n| n == model)
|| state.is_stageable_name(model) =>
{
Some(model)
}
None => None,
};
if let Some(slot) =
crate::main_modules::app_state::resolve_adapter_slot(&state.adapter_names, selector)
{
return Ok(slot);
}
let name = selector.unwrap_or("");
match state.ensure_adapter_hot_opt(name).await {
Ok(Some(slot)) => Ok(slot),
Ok(None) => Err(openai_error_response(
StatusCode::BAD_REQUEST,
format!(
"unknown adapter '{}'; resident adapters: [{}]",
name,
state.adapter_names.join(", ")
),
)),
Err(crate::main_modules::promotion::PromoteReject::PoolFull(m)) => {
Err(openai_error_response(StatusCode::SERVICE_UNAVAILABLE, m))
}
Err(crate::main_modules::promotion::PromoteReject::Peer(m)) => {
Err(openai_error_response(StatusCode::BAD_GATEWAY, m))
}
}
}
#[derive(Deserialize)]
pub struct SetActiveLoraRequest {
pub adapter: String,
}
#[derive(Serialize)]
struct SetActiveLoraResponse {
object: &'static str,
active: String,
}
pub async fn set_active_lora(
CurrentModel(state): CurrentModel,
body: Result<Json<SetActiveLoraRequest>, JsonRejection>,
) -> Response {
let Json(req) = match body {
Ok(b) => b,
Err(e) => {
return openai_error_response(StatusCode::BAD_REQUEST, format!("invalid body: {e}"));
}
};
let Some(ref tx) = state.rotation_tx else {
return openai_error_response(
StatusCode::BAD_REQUEST,
"no LoRA adapter is loaded (start with --lora-adapter NAME=PATH)".to_string(),
);
};
if !state.adapter_names.iter().any(|n| n == &req.adapter) {
return openai_error_response(
StatusCode::NOT_FOUND,
format!(
"adapter '{}' is not resident (resident: [{}])",
req.adapter,
state.adapter_names.join(", ")
),
);
}
let (ack_tx, ack_rx) = tokio::sync::oneshot::channel();
if tx
.send((
crate::scheduler::LoraCommand::Rotate(req.adapter.clone()),
ack_tx,
))
.await
.is_err()
{
return openai_error_response(
StatusCode::INTERNAL_SERVER_ERROR,
"scheduler rotation channel closed".to_string(),
);
}
match ack_rx.await {
Ok(Ok(_)) => {
if let Ok(mut a) = state.active_adapter.lock() {
*a = Some(req.adapter.clone());
}
Json(SetActiveLoraResponse {
object: "lora.active",
active: req.adapter,
})
.into_response()
}
Ok(Err(reason)) => openai_error_response(StatusCode::BAD_REQUEST, reason),
Err(_) => openai_error_response(
StatusCode::INTERNAL_SERVER_ERROR,
"scheduler dropped the rotation ack (shutting down?)".to_string(),
),
}
}
#[derive(Deserialize)]
pub struct LoadLoraRequest {
pub name: String,
pub path: String,
#[serde(default)]
pub slot: usize,
}
#[derive(Serialize)]
struct LoadLoraResponse {
object: &'static str,
loaded: String,
slot: usize,
}
pub async fn load_lora_into_slot(
CurrentModel(state): CurrentModel,
body: Result<Json<LoadLoraRequest>, JsonRejection>,
) -> Response {
let Json(req) = match body {
Ok(b) => b,
Err(e) => {
return openai_error_response(StatusCode::BAD_REQUEST, format!("invalid body: {e}"));
}
};
if req.name.is_empty() || req.name.len() > 256 {
return openai_error_response(
StatusCode::BAD_REQUEST,
"adapter name must be 1..=256 chars".to_string(),
);
}
if req.path.len() > 4096 {
return openai_error_response(
StatusCode::BAD_REQUEST,
"adapter path too long (max 4096 chars)".to_string(),
);
}
if req.slot > 4096 {
return openai_error_response(
StatusCode::BAD_REQUEST,
format!("slot {} out of range (max 4096)", req.slot),
);
}
let Some(ref tx) = state.rotation_tx else {
return openai_error_response(
StatusCode::BAD_REQUEST,
"no LoRA adapter pool is loaded (start with --lora-adapter NAME=PATH)".to_string(),
);
};
let dir = std::path::PathBuf::from(&req.path);
if !dir.join("adapter_config.json").exists() {
return openai_error_response(
StatusCode::BAD_REQUEST,
format!("no adapter_config.json under path '{}'", req.path),
);
}
let (ack_tx, ack_rx) = tokio::sync::oneshot::channel();
let cmd = crate::scheduler::LoraCommand::LoadIntoSlot {
name: req.name.clone(),
dir,
slot: req.slot,
};
if tx.send((cmd, ack_tx)).await.is_err() {
return openai_error_response(
StatusCode::INTERNAL_SERVER_ERROR,
"scheduler rotation channel closed".to_string(),
);
}
match ack_rx.await {
Ok(Ok(_)) => {
if let Ok(mut a) = state.active_adapter.lock() {
*a = Some(req.name.clone());
}
Json(LoadLoraResponse {
object: "lora.loaded",
loaded: req.name,
slot: req.slot,
})
.into_response()
}
Ok(Err(reason)) => openai_error_response(StatusCode::BAD_REQUEST, reason),
Err(_) => openai_error_response(
StatusCode::INTERNAL_SERVER_ERROR,
"scheduler dropped the load ack (shutting down?)".to_string(),
),
}
}