1pub mod api;
4pub mod dashboard;
5pub mod openai;
6pub mod streaming;
7
8use axum::{routing::get, Json, Router};
9use cortiq_engine::{CortiqRuntime, Pipeline};
10use std::sync::Arc;
11use tokio::sync::{Mutex, OwnedMutexGuard, OwnedSemaphorePermit, Semaphore};
12use tower_http::cors::CorsLayer;
13
14pub struct PipelinePool {
23 slots: Vec<Arc<Mutex<Pipeline>>>,
24 sem: Arc<Semaphore>,
25}
26
27pub struct SlotGuard {
30 _permit: OwnedSemaphorePermit,
31 pub pipe: OwnedMutexGuard<Pipeline>,
32}
33
34impl PipelinePool {
35 pub fn new(pipelines: Vec<Pipeline>) -> Self {
36 assert!(!pipelines.is_empty(), "pipeline pool needs at least one slot");
37 let sem = Arc::new(Semaphore::new(pipelines.len()));
38 Self {
39 slots: pipelines.into_iter().map(|p| Arc::new(Mutex::new(p))).collect(),
40 sem,
41 }
42 }
43
44 pub fn n_slots(&self) -> usize {
45 self.slots.len()
46 }
47
48 pub async fn acquire(&self) -> SlotGuard {
51 let permit = self
52 .sem
53 .clone()
54 .acquire_owned()
55 .await
56 .expect("slot semaphore closed");
57 for s in &self.slots {
58 if let Ok(pipe) = s.clone().try_lock_owned() {
59 return SlotGuard { _permit: permit, pipe };
60 }
61 }
62 unreachable!("semaphore permit held but every slot is locked")
63 }
64}
65
66pub struct AppState {
69 pub runtime: CortiqRuntime,
70 pub tokenizer: Arc<cortiq_engine::tokenizer::Tokenizer>,
71 pub slots: PipelinePool,
72}
73
74async fn healthz() -> Json<serde_json::Value> {
78 Json(serde_json::json!({ "status": "ok" }))
79}
80
81pub fn build_router(state: Arc<AppState>) -> Router {
83 Router::new()
84 .route("/healthz", get(healthz))
85 .merge(openai::routes())
86 .merge(api::routes())
87 .merge(dashboard::routes())
88 .layer(CorsLayer::permissive())
89 .with_state(state)
90}