pub mod api;
pub mod dashboard;
pub mod openai;
pub mod streaming;
use axum::{Json, Router, routing::get};
use cortiq_engine::{CortiqRuntime, Pipeline};
use std::sync::Arc;
use tokio::sync::{Mutex, OwnedMutexGuard, OwnedSemaphorePermit, Semaphore};
use tower_http::cors::CorsLayer;
pub struct PipelinePool {
slots: Vec<Arc<Mutex<Pipeline>>>,
sem: Arc<Semaphore>,
}
pub struct SlotGuard {
pub pipe: OwnedMutexGuard<Pipeline>,
_permit: OwnedSemaphorePermit,
}
impl PipelinePool {
pub fn new(pipelines: Vec<Pipeline>) -> Self {
assert!(
!pipelines.is_empty(),
"pipeline pool needs at least one slot"
);
let sem = Arc::new(Semaphore::new(pipelines.len()));
Self {
slots: pipelines
.into_iter()
.map(|p| Arc::new(Mutex::new(p)))
.collect(),
sem,
}
}
pub fn n_slots(&self) -> usize {
self.slots.len()
}
pub async fn acquire(&self) -> SlotGuard {
let permit = self
.sem
.clone()
.acquire_owned()
.await
.expect("slot semaphore closed");
for s in &self.slots {
if let Ok(pipe) = s.clone().try_lock_owned() {
return SlotGuard {
pipe,
_permit: permit,
};
}
}
unreachable!("semaphore permit held but every slot is locked")
}
}
pub struct AppState {
pub runtime: CortiqRuntime,
pub tokenizer: Arc<cortiq_engine::tokenizer::Tokenizer>,
pub slots: PipelinePool,
}
async fn healthz() -> Json<serde_json::Value> {
Json(serde_json::json!({ "status": "ok" }))
}
pub fn build_router(state: Arc<AppState>) -> Router {
Router::new()
.route("/healthz", get(healthz))
.merge(openai::routes())
.merge(api::routes())
.merge(dashboard::routes())
.layer(CorsLayer::permissive())
.with_state(state)
}