1use std::sync::Arc;
4
5use axum::extract::State;
6use axum::http::{HeaderMap, StatusCode};
7use axum::response::sse::{Event, KeepAlive, Sse};
8use axum::response::{IntoResponse, Response};
9use axum::routing::{get, post};
10use axum::{Json, Router};
11use serde::Deserialize;
12use tokio_stream::wrappers::ReceiverStream;
13use tokio_stream::StreamExt;
14
15use crate::{Daemon, DaemonError};
16pub use crate::inference::{TokenLimits, SamplingOverrides, SamplingFallback, RequestQos};
17pub use crate::http_state::{HttpQosPolicy, QOS_HEADER, BATCH_INVARIANT_HEADER};
18use crate::inference::{
19 advertised_generation_defaults, repeat_penalty_for, next_id, unix_now,
20 ChatRequest, CompletionRequest, RequestPolicy, DEFAULT_MAX_CONTEXT,
21 Kind,
22};
23use crate::http_state::{
24 AppState, AuthenticatedCredential, error_response, error_response_code,
25 error_response_typed, daemon_error,
26};
27use crate::openai_output::generation_response;
28
29#[derive(Clone)]
30pub struct ServeConfig {
31 pub api_key: Option<String>,
32 pub rate_limit_per_minute: u32,
33 pub default_max_tokens: Option<u32>,
34 pub max_context: u32,
35 pub drain_timeout_secs: u64,
36 pub sampling: SamplingOverrides,
37 pub qos: HttpQosPolicy,
38 pub keys: Option<Arc<crate::keypolicy::KeyTable>>,
39 pub nonstream_keepalive_secs: u64,
40 pub surface: HttpSurface,
41 pub model_naming: ModelNaming,
42 pub identity: ServerIdentity,
43 pub idempotency_dir: Option<std::path::PathBuf>,
46}
47
48impl Default for ServeConfig {
49 fn default() -> ServeConfig {
50 ServeConfig {
51 api_key: None,
52 rate_limit_per_minute: 0,
53 default_max_tokens: None,
54 max_context: DEFAULT_MAX_CONTEXT,
55 drain_timeout_secs: 60,
56 sampling: SamplingOverrides::default(),
57 qos: HttpQosPolicy::default(),
58 keys: None,
59 nonstream_keepalive_secs: 0,
60 surface: HttpSurface::default(),
61 model_naming: ModelNaming::default(),
62 identity: ServerIdentity::default(),
63 idempotency_dir: None,
64 }
65 }
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct ServerIdentity {
73 pub name: String,
74 pub version: String,
75}
76
77impl Default for ServerIdentity {
78 fn default() -> ServerIdentity {
79 ServerIdentity { name: "superfluid".into(), version: env!("CARGO_PKG_VERSION").into() }
80 }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
85pub enum ModelNaming {
86 #[default]
88 Exact,
89 Loose,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub struct HttpSurface {
98 pub anthropic: bool,
100 pub ollama: bool,
102}
103
104impl HttpSurface {
105 pub const OPENAI_ONLY: HttpSurface = HttpSurface { anthropic: false, ollama: false };
106
107 pub fn also_served(&self) -> &'static str {
109 match (self.anthropic, self.ollama) {
110 (true, true) => " (also Anthropic /v1/messages and Ollama /api/*)",
111 (true, false) => " (also Anthropic /v1/messages)",
112 (false, true) => " (also Ollama /api/*)",
113 (false, false) => "",
114 }
115 }
116}
117
118impl Default for HttpSurface {
119 fn default() -> HttpSurface {
120 HttpSurface { anthropic: true, ollama: true }
121 }
122}
123
124pub fn serve_blocking(
125 listener: std::net::TcpListener,
126 daemon: Arc<Daemon>,
127 model_name: String,
128) -> std::io::Result<()> {
129 let registry = Arc::new(crate::registry::ModelRegistry::single(model_name, daemon));
130 serve_blocking_registry(listener, registry)
131}
132
133pub(crate) fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
134 if a.len() != b.len() {
135 return false;
136 }
137 let mut diff = 0u8;
138 for (x, y) in a.iter().zip(b.iter()) {
139 diff |= x ^ y;
140 }
141 diff == 0
142}
143
144#[derive(Clone)]
145struct AuthState {
146 cfg: ServeConfig,
147 api_key_digest: Option<[u8; 32]>,
148 ip_limiter: Arc<crate::ratelimit::RateLimiter>,
149}
150
151async fn auth_layer(
152 axum::extract::State(auth): axum::extract::State<AuthState>,
153 mut req: axum::extract::Request,
154 next: axum::middleware::Next,
155) -> Response {
156 let cfg = &auth.cfg;
157 if cfg.api_key.is_none() && cfg.keys.is_none() {
158 return next.run(req).await;
159 }
160 let path = req.uri().path();
161 if path == "/health" {
162 return next.run(req).await;
163 }
164 let exempt = matches!(path, "/metrics" | "/v1/metrics");
165 let m = req.method().clone();
166 let is_cancel = is_exact_batch_cancel(&m, path);
167 let lightweight = (m == axum::http::Method::GET
168 && (path == "/v1/batches"
169 || path == "/v1/files"
170 || path.strip_prefix("/v1/batches/").is_some_and(|rest| !rest.contains('/'))
171 || (path.starts_with("/v1/files/") && !path.ends_with("/content"))
172 || path.starts_with("/v1/responses/")))
173 || is_cancel
174 || (m == axum::http::Method::DELETE && (path.starts_with("/v1/files/") || path.starts_with("/v1/responses/")));
175 let takes_slot = !lightweight;
176 let spends_rate = !is_cancel;
177 let h = req.headers();
178 let bearer = h
179 .get(axum::http::header::AUTHORIZATION)
180 .and_then(|v| v.to_str().ok())
181 .and_then(|v| v.strip_prefix("Bearer "));
182 let x_api_key = h.get("x-api-key").and_then(|v| v.to_str().ok());
183 let keyed = cfg
184 .keys
185 .as_deref()
186 .and_then(|t| bearer.and_then(|v| t.lookup(v)).or_else(|| x_api_key.and_then(|v| t.lookup(v))));
187 let admin_route = *req.method() == axum::http::Method::POST
188 && matches!(path, "/v1/models/load" | "/v1/models/unload" | "/v1/lora/load" | "/v1/lora/unload")
189 || (*req.method() == axum::http::Method::DELETE && path.starts_with("/v1/models/"));
190 if let Some(key) = keyed {
191 if admin_route && !key.admin {
192 if key.admit(false, true).is_err() {
193 return error_response_typed(
194 StatusCode::TOO_MANY_REQUESTS,
195 format!("API key '{}' exceeded its rate limit ({}/min)", key.name, key.rate_limit_rpm),
196 "rate_limit_error",
197 "rate_limit_exceeded",
198 );
199 }
200 let ip_ok = key.rate_limit_rpm > 0
201 || req
202 .extensions()
203 .get::<axum::extract::ConnectInfo<std::net::SocketAddr>>()
204 .is_none_or(|c| auth.ip_limiter.allow(c.0.ip()));
205 if !ip_ok {
206 return error_response_typed(
207 StatusCode::TOO_MANY_REQUESTS,
208 "Rate limit exceeded".to_string(),
209 "rate_limit_error",
210 "rate_limit_exceeded",
211 );
212 }
213 key.note_admin_reject();
214 return (
215 StatusCode::FORBIDDEN,
216 Json(serde_json::json!({"error": {
217 "message": format!(
218 "API key '{}' may not administer the server (model and LoRA load/unload); \
219 use the global --api-key or grant \"admin\": true on this key",
220 key.name
221 ),
222 "type": "permission_error",
223 "param": null,
224 "code": "admin_required",
225 }})),
226 )
227 .into_response();
228 }
229 let slot = if exempt {
230 None
231 } else {
232 match key.admit(takes_slot, spends_rate) {
233 Ok(slot) => Some(slot),
234 Err(why) => {
235 tracing::debug!(key = %key.name, reason = ?why, "key policy refused request");
236 let (message, code) = match why {
237 crate::keypolicy::Reject::Concurrency => (
238 format!("API key '{}' is at its concurrent-request limit ({})", key.name, key.max_concurrent),
239 "concurrency_limit_exceeded",
240 ),
241 _ => (
242 format!("API key '{}' exceeded its rate limit ({}/min)", key.name, key.rate_limit_rpm),
243 "rate_limit_exceeded",
244 ),
245 };
246 return (
247 StatusCode::TOO_MANY_REQUESTS,
248 Json(serde_json::json!({"error": {
249 "message": message,
250 "type": "rate_limit_error",
251 "param": null,
252 "code": code,
253 }})),
254 )
255 .into_response();
256 }
257 }
258 };
259 let slot = slot.map(Arc::new);
260 req.extensions_mut().insert(AuthenticatedCredential::policy_key(&key.name));
261 req.extensions_mut().insert(crate::keypolicy::ClientKey(key, slot.clone()));
262 let resp = match &slot {
263 Some(slot) => {
264 let held = Arc::clone(slot);
265 match tokio::spawn(async move {
266 let resp = next.run(req).await;
267 drop(held);
268 resp
269 })
270 .await
271 {
272 Ok(resp) => resp,
273 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, format!("handler failed: {e}")),
274 }
275 }
276 None => next.run(req).await,
277 };
278 return match slot {
279 Some(slot) => {
280 let (parts, body) = resp.into_parts();
281 Response::from_parts(parts, axum::body::Body::new(HeldBody { inner: body, _slot: slot }))
282 }
283 None => resp,
284 };
285 }
286 let ok = auth.api_key_digest.as_ref().is_some_and(|want| {
287 let is = |v: &str| constant_time_eq(&crate::keypolicy::sha256(v), want);
288 bearer.is_some_and(is) || x_api_key.is_some_and(is)
289 });
290 if ok {
291 if let Some(key) = cfg.api_key.as_deref() {
292 req.extensions_mut().insert(AuthenticatedCredential::global_key(key));
293 }
294 return next.run(req).await;
295 }
296 (
297 StatusCode::UNAUTHORIZED,
298 Json(serde_json::json!({"error": {
299 "message": "Invalid or missing API key",
300 "type": "authentication_error",
301 "param": null,
302 "code": "invalid_api_key",
303 }})),
304 )
305 .into_response()
306}
307
308struct HeldBody {
309 inner: axum::body::Body,
310 _slot: Arc<crate::keypolicy::InFlight>,
311}
312
313impl axum::body::HttpBody for HeldBody {
314 type Data = axum::body::Bytes;
315 type Error = axum::Error;
316
317 fn poll_frame(
318 self: std::pin::Pin<&mut Self>,
319 cx: &mut std::task::Context<'_>,
320 ) -> std::task::Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
321 std::pin::Pin::new(&mut self.get_mut().inner).poll_frame(cx)
322 }
323
324 fn is_end_stream(&self) -> bool {
325 self.inner.is_end_stream()
326 }
327
328 fn size_hint(&self) -> http_body::SizeHint {
329 self.inner.size_hint()
330 }
331}
332
333fn is_exact_batch_cancel(m: &axum::http::Method, path: &str) -> bool {
334 *m == axum::http::Method::POST
335 && path
336 .strip_prefix("/v1/batches/")
337 .and_then(|r| r.strip_suffix("/cancel"))
338 .is_some_and(|id| !id.is_empty() && id.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-'))
339}
340
341#[derive(Clone)]
342struct DeferredIpCharge(Arc<crate::ratelimit::RateLimiter>, std::net::IpAddr);
343
344async fn rate_limit_layer(
345 axum::extract::State(limiter): axum::extract::State<Arc<crate::ratelimit::RateLimiter>>,
346 axum::extract::ConnectInfo(peer): axum::extract::ConnectInfo<std::net::SocketAddr>,
347 req: axum::extract::Request,
348 next: axum::middleware::Next,
349) -> Response {
350 let path = req.uri().path();
351 let key_metered = req
352 .extensions()
353 .get::<crate::keypolicy::ClientKey>()
354 .is_some_and(|k| k.0.rate_limit_rpm > 0);
355 if matches!(path, "/health" | "/metrics" | "/v1/metrics") || key_metered {
356 return next.run(req).await;
357 }
358 let keyed = req.extensions().get::<crate::keypolicy::ClientKey>().is_some();
359 if keyed && is_exact_batch_cancel(req.method(), path) {
360 let mut req = req;
361 req.extensions_mut().insert(DeferredIpCharge(Arc::clone(&limiter), peer.ip()));
362 return next.run(req).await;
363 }
364 if limiter.allow(peer.ip()) {
365 return next.run(req).await;
366 }
367 (
368 StatusCode::TOO_MANY_REQUESTS,
369 Json(serde_json::json!({"error": {
370 "message": "Rate limit exceeded",
371 "type": "rate_limit_error",
372 "param": null,
373 "code": "rate_limit_exceeded",
374 }})),
375 )
376 .into_response()
377}
378
379const MAX_BODY_SIZE: usize = 100 * 1024 * 1024;
380
381pub fn serve_blocking_registry(
382 listener: std::net::TcpListener,
383 registry: Arc<crate::registry::ModelRegistry>,
384) -> std::io::Result<()> {
385 serve_blocking_config(listener, registry, ServeConfig::default())
386}
387
388fn shutdown_registry_bounded(
389 registry: Arc<crate::registry::ModelRegistry>,
390 budget: std::time::Duration,
391) -> bool {
392 let (tx, rx) = std::sync::mpsc::channel();
393 std::thread::Builder::new()
394 .name("superfluid-shutdown".into())
395 .spawn(move || {
396 registry.shutdown_all();
397 let _ = tx.send(());
398 })
399 .is_ok()
400 && rx.recv_timeout(budget).is_ok()
401}
402
403fn is_loopback_host(host: &str) -> bool {
404 let host = host.trim_end_matches('.').to_ascii_lowercase();
405 if host == "localhost" || host.ends_with(".localhost") {
406 return true;
407 }
408 let bare = host.strip_prefix('[').and_then(|h| h.strip_suffix(']')).unwrap_or(&host);
409 bare.parse::<std::net::IpAddr>().is_ok_and(|ip| ip.is_loopback())
410}
411
412fn host_of_authority(authority: &str) -> &str {
413 if let Some(rest) = authority.strip_prefix('[') {
414 return rest.split_once(']').map_or(authority, |(h, _)| h);
415 }
416 match authority.rsplit_once(':') {
417 Some((h, port)) if !h.contains(':') && port.chars().all(|c| c.is_ascii_digit()) => h,
418 _ => authority,
419 }
420}
421
422#[derive(Clone)]
426pub(crate) enum HostGuard {
427 Loopback,
428 ThisMachine(Arc<[String]>),
429 Any,
430}
431
432impl HostGuard {
433 pub(crate) fn for_listener(cfg: &ServeConfig, listens_on_loopback: bool) -> HostGuard {
434 if listens_on_loopback {
435 HostGuard::Loopback
436 } else if cfg.api_key.is_none() && cfg.keys.is_none() {
437 HostGuard::ThisMachine(machine_host_names().into())
438 } else {
439 HostGuard::Any
440 }
441 }
442}
443
444fn machine_host_names() -> Vec<String> {
445 let mut buf = [0u8; 256];
446 if unsafe { libc::gethostname(buf.as_mut_ptr().cast(), buf.len()) } != 0 {
448 return Vec::new();
449 }
450 let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
451 let name = String::from_utf8_lossy(&buf[..len]).trim_end_matches('.').to_ascii_lowercase();
452 if name.is_empty() {
453 return Vec::new();
454 }
455 let short = name.strip_suffix(".local").unwrap_or(&name).to_string();
456 let mut names = vec![name.clone(), format!("{short}.local")];
457 if short != name {
458 names.push(short);
459 }
460 names
461}
462
463fn names_this_machine(host: &str, names: &[String]) -> bool {
464 if is_loopback_host(host) {
465 return true;
466 }
467 let host = host.trim_end_matches('.').to_ascii_lowercase();
468 let bare = host.strip_prefix('[').and_then(|h| h.strip_suffix(']')).unwrap_or(&host);
469 bare.parse::<std::net::IpAddr>().is_ok() || names.contains(&host)
470}
471
472pub(crate) fn browser_refusal(headers: &HeaderMap, guard: &HostGuard) -> Option<String> {
476 if let Some(origin) = headers.get(axum::http::header::ORIGIN) {
477 let origin = origin.to_str().unwrap_or("");
478 let authority = origin.split_once("://").map(|(_, rest)| rest).unwrap_or("");
479 let authority = authority.split('/').next().unwrap_or("");
480 if !is_loopback_host(host_of_authority(authority)) {
481 return Some(format!(
482 "requests from web pages on other origins are refused (Origin: {origin}); call the API from a \
483 server-side client, or from a page served on localhost"
484 ));
485 }
486 }
487 let host = headers.get(axum::http::header::HOST).and_then(|h| h.to_str().ok()).unwrap_or("");
488 if host.is_empty() {
489 return None;
490 }
491 match guard {
492 HostGuard::Loopback if !is_loopback_host(host_of_authority(host)) => Some(format!(
493 "this server listens on loopback and answers only to a loopback Host (got Host: {host})"
494 )),
495 HostGuard::ThisMachine(names) if !names_this_machine(host_of_authority(host), names) => Some(format!(
496 "this server has no API key and answers only to an IP address or this machine's own name \
497 (got Host: {host}); start it with --api-key or --key-policy to serve other host names, or \
498 connect by IP address"
499 )),
500 _ => None,
501 }
502}
503
504async fn browser_guard_layer(
505 axum::extract::State(guard): axum::extract::State<HostGuard>,
506 req: axum::extract::Request,
507 next: axum::middleware::Next,
508) -> Response {
509 match browser_refusal(req.headers(), &guard) {
510 None => next.run(req).await,
511 Some(why) => error_response_typed(StatusCode::FORBIDDEN, why, "permission_error", "origin_refused"),
512 }
513}
514
515pub(crate) fn access_layers(
516 app: Router,
517 cfg: &ServeConfig,
518 ip_limiter: Arc<crate::ratelimit::RateLimiter>,
519 listens_on_loopback: bool,
520) -> Router {
521 let records = Arc::new(crate::idempotency::Records::new(cfg.idempotency_dir.clone()));
522 app.layer(axum::middleware::from_fn_with_state(records, crate::idempotency::layer))
523 .layer(axum::middleware::from_fn_with_state(Arc::clone(&ip_limiter), rate_limit_layer))
524 .layer(axum::middleware::from_fn_with_state(
525 AuthState {
526 api_key_digest: cfg.api_key.as_deref().map(crate::keypolicy::sha256),
527 cfg: cfg.clone(),
528 ip_limiter,
529 },
530 auth_layer,
531 ))
532 .layer(axum::middleware::from_fn_with_state(
533 HostGuard::for_listener(cfg, listens_on_loopback),
534 browser_guard_layer,
535 ))
536}
537
538pub fn serve_blocking_config(
539 listener: std::net::TcpListener,
540 registry: Arc<crate::registry::ModelRegistry>,
541 cfg: ServeConfig,
542) -> std::io::Result<()> {
543 if let Some(keys) = &cfg.keys {
544 keys.check_global_key(cfg.api_key.as_deref())
545 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
546 }
547 let listens_on_loopback = listener.local_addr()?.ip().is_loopback();
548 let rt = tokio::runtime::Runtime::new()?;
549 let shutdown_registry = Arc::clone(®istry);
550 rt.block_on(async move {
551 listener.set_nonblocking(true)?;
552 let listener = tokio::net::TcpListener::from_std(listener)?;
553 let ip_limiter = Arc::new(crate::ratelimit::RateLimiter::new(cfg.rate_limit_per_minute));
554 let mut app = Router::new()
555 .route("/v1/models/load", post(models_load))
556 .route("/v1/models/unload", post(models_unload))
557 .route("/v1/models/{model}", get(models_retrieve).delete(models_delete))
558 .route("/v1/chat/completions", post(chat_completions))
559 .route("/v1/completions", post(completions))
560 .route("/v1/embeddings", post(embeddings))
561 .route("/v1/rerank", post(rerank))
562 .route("/v1/audio/transcriptions", post(audio_transcriptions))
563 .route("/v1/audio/translations", post(audio_translations))
564 .route("/v1/files", post(files_upload).get(files_list))
565 .route("/v1/files/{id}", get(files_get).delete(files_delete))
566 .route("/v1/files/{id}/content", get(files_content))
567 .route("/v1/batches", post(batches_create).get(batches_list))
568 .route("/v1/batches/{id}", get(batches_get))
569 .route("/v1/batches/{id}/cancel", post(batches_cancel))
570 .route("/v1/responses", post(crate::responses::create))
571 .route("/v1/responses/{id}", get(crate::responses::retrieve).delete(crate::responses::delete))
572 .route("/v1/responses/{id}/input_items", get(crate::responses::input_items_list))
573 .route("/v1/responses/{id}/cancel", post(crate::responses::cancel))
574 .route("/v1/lora/load", post(lora_load))
575 .route("/v1/lora/unload", post(lora_unload))
576 .route("/v1/lora", get(lora_list));
577 if cfg.surface.anthropic {
578 app = app.route("/v1/messages", post(crate::anthropic::messages));
579 }
580 if cfg.surface.ollama {
581 app = app.merge(crate::ollama::routes());
582 }
583 let app = app
584 .route("/v1/models", get(models))
585 .route("/v1/tokenize", post(tokenize))
586 .route("/metrics", get(metrics))
587 .route("/v1/metrics", get(metrics))
588 .route("/health", get(health))
589 .route("/props", get(props))
590 .route("/slots", get(slots))
591 .route("/slots/{id}/save", post(slots_retired))
592 .route("/slots/{id}/restore", post(slots_retired))
593 .layer(axum::extract::DefaultBodyLimit::max(MAX_BODY_SIZE))
594 .with_state(AppState {
595 registry,
596 limits: TokenLimits {
597 default_max_tokens: cfg.default_max_tokens,
598 max_context: cfg.max_context,
599 },
600 sampling: cfg.sampling,
601 qos: cfg.qos,
602 keys: cfg.keys.clone(),
603 fim_buckets: Arc::new(crate::completion_bucket::ClientBuckets::new()),
604 nonstream_keepalive: (cfg.nonstream_keepalive_secs > 0)
605 .then(|| std::time::Duration::from_secs(cfg.nonstream_keepalive_secs)),
606 model_naming: cfg.model_naming,
607 identity: Arc::new(cfg.identity.clone()),
608 });
609 let app = access_layers(app, &cfg, ip_limiter, listens_on_loopback);
610 let app = if cfg.surface.ollama {
611 app.layer(axum::middleware::from_fn(crate::ollama::error_layer))
612 } else {
613 app
614 };
615 let drain = cfg.drain_timeout_secs;
616 let draining = Arc::new(std::sync::atomic::AtomicBool::new(false));
617 let draining_sig = Arc::clone(&draining);
618 let drain_deadline = Arc::new(std::sync::OnceLock::<std::time::Instant>::new());
619 let drain_deadline_sig = Arc::clone(&drain_deadline);
620 let watchdog_registry = Arc::clone(&shutdown_registry);
621 let shutdown = async move {
622 let mut sig = match tokio::signal::unix::signal(
623 tokio::signal::unix::SignalKind::user_defined1(),
624 ) {
625 Ok(s) => s,
626 Err(_) => return std::future::pending::<()>().await,
627 };
628 sig.recv().await;
629 let _ = drain_deadline_sig
630 .set(std::time::Instant::now() + std::time::Duration::from_secs(drain));
631 draining_sig.store(true, std::sync::atomic::Ordering::SeqCst);
632 eprintln!(
633 "superfluid: SIGUSR1 — no longer accepting connections, draining for up to {drain}s"
634 );
635 tokio::spawn(async move {
636 tokio::time::sleep(std::time::Duration::from_secs(drain)).await;
637 eprintln!("superfluid: drain window elapsed with requests still in flight — exiting");
638 if !shutdown_registry_bounded(watchdog_registry, std::time::Duration::from_secs(5))
639 {
640 eprintln!(
641 "superfluid: scheduler teardown did not finish in 5s — exiting anyway. A \
642 park artifact of a session that ended during the drain may be left \
643 behind; the next startup sweep reclaims it only if --park is on with \
644 a non-zero --park-budget-gb AND the directory is over that budget"
645 );
646 }
647 std::process::exit(0);
648 });
649 };
650 axum::serve(
651 listener,
652 app.into_make_service_with_connect_info::<std::net::SocketAddr>(),
653 )
654 .with_graceful_shutdown(shutdown)
655 .await?;
656 if draining.load(std::sync::atomic::Ordering::SeqCst) {
657 eprintln!("superfluid: drained — exiting");
658 if !shutdown_registry_bounded(shutdown_registry, std::time::Duration::from_secs(30)) {
659 eprintln!(
660 "superfluid: scheduler teardown did not finish in 30s — exiting anyway. A park \
661 artifact of a session that ended during the drain may be left behind; the \
662 next startup sweep reclaims it only if --park is on with a non-zero \
663 --park-budget-gb AND the directory is over that budget"
664 );
665 }
666 let left = drain_deadline
667 .get()
668 .map(|d| d.saturating_duration_since(std::time::Instant::now()))
669 .unwrap_or_default();
670 crate::otlp::shutdown(left.min(std::time::Duration::from_secs(2)));
671 std::process::exit(0);
672 }
673 Ok(())
674 })
675}
676
677async fn slots_retired() -> Response {
678 error_response_code(
679 StatusCode::NOT_IMPLEMENTED,
680 concat!(
681 "POST /slots/{id}/save|restore is retired. A request's tokens are ",
682 "durable without it: the WAL session log is truth, and a repeated ",
683 "prefix is served from the prefix cache rather than re-prefilled. KV ",
684 "itself is NOT sealed to disk by default — that is `--park`, it applies ",
685 "to sessions you resume by id through the session API, and a session ",
686 "minted per HTTP request is never resumable by one. See OPERATIONS.md.",
687 )
688 .into(),
689 "route_retired",
690 )
691}
692
693async fn metrics(
694 State(state): State<AppState>,
695 key: Option<axum::Extension<crate::keypolicy::ClientKey>>,
696) -> impl IntoResponse {
697 (
698 [(axum::http::header::CONTENT_TYPE, "text/plain; version=0.0.4")],
699 {
700 let mut text = state.default_daemon().metrics_text();
701 if let Some(keys) = &state.keys {
702 let only = key.as_ref().map(|k| &*k.0 .0).filter(|k| !k.admin);
703 text.push_str(&keys.metrics_text_for(only));
704 }
705 text
706 },
707 )
708}
709
710pub(crate) fn chat_once(daemon: &Daemon, req: &ChatRequest, model_name: &str, policy: RequestPolicy) -> Result<serde_json::Value, DaemonError> {
711 let frame = crate::inference::chat_once(daemon, req, policy)?;
712 Ok(crate::openai_output::frame_json(Kind::Chat, &next_id(), model_name, unix_now(), frame, false))
713}
714
715#[derive(Deserialize)]
716struct BatchCreateRequest {
717 input_file_id: String,
718 #[serde(default = "default_endpoint")]
719 endpoint: String,
720 #[serde(default = "default_window")]
721 completion_window: String,
722}
723fn default_endpoint() -> String {
724 "/v1/chat/completions".to_string()
725}
726fn default_window() -> String {
727 "24h".to_string()
728}
729
730async fn batches_create(
731 State(st): State<AppState>,
732 key: Option<axum::Extension<crate::keypolicy::ClientKey>>,
733 headers: HeaderMap,
734 body: axum::body::Bytes,
735) -> Response {
736 let req: BatchCreateRequest = match serde_json::from_slice(&body) {
737 Ok(r) => r,
738 Err(e) => return error_response(StatusCode::BAD_REQUEST, format!("invalid request: {e}")),
739 };
740 let qos = match st.qos.resolve(&headers, key.as_ref().map(|k| &*k.0 .0)) {
741 Ok(q) => q,
742 Err(resp) => return resp,
743 };
744 let caller = owner_of(&key);
745 let visible_input = st.default_daemon().files().get(&req.input_file_id).is_some_and(|m| visible(&m.owner, caller));
746 let input = match st.default_daemon().files().content(&req.input_file_id).filter(|_| visible_input) {
747 Some(b) => b,
748 None => return error_response(StatusCode::BAD_REQUEST, format!("no such input_file_id: {}", req.input_file_id)),
749 };
750 let total = String::from_utf8_lossy(&input).lines().filter(|l| !l.trim().is_empty()).count() as u64;
751 let store = st.default_daemon().batches();
752 let daemon = st.default_daemon();
753 let model_name = st.default_name();
754 let pace = key.as_ref().map(|k| Arc::clone(&k.0 .0)).filter(|k| k.rate_limit_rpm > 0);
755 let hold = key.as_ref().map(|k| k.0.clone());
756 let worker_store = Arc::clone(&store);
757 match store.start(&req.endpoint, &req.input_file_id, &req.completion_window, total, caller, move |id| {
758 let _hold = hold;
759 crate::batches::process(daemon, worker_store, model_name, id, qos, pace)
760 }) {
761 Ok(batch) => (StatusCode::OK, Json(public(&batch))).into_response(),
762 Err(crate::batches::BatchesBusy) => error_response_typed(
763 StatusCode::TOO_MANY_REQUESTS,
764 format!(
765 "{} batches are already waiting to run; retry when some finish or cancel one",
766 crate::batches::MAX_QUEUED_BATCHES
767 ),
768 "rate_limit_error",
769 "batch_queue_full",
770 ),
771 }
772}
773
774fn owner_of(key: &Option<axum::Extension<crate::keypolicy::ClientKey>>) -> Option<&str> {
775 key.as_ref().map(|k| k.0 .0.name.as_str())
776}
777
778fn visible(owner: &Option<String>, caller: Option<&str>) -> bool {
779 caller.is_none_or(|c| owner.as_deref() == Some(c))
780}
781
782fn public<T: serde::Serialize>(v: &T) -> serde_json::Value {
783 let mut j = serde_json::to_value(v).unwrap_or(serde_json::Value::Null);
784 if let Some(o) = j.as_object_mut() {
785 o.remove("owner");
786 }
787 j
788}
789
790async fn batches_list(State(st): State<AppState>, key: Option<axum::Extension<crate::keypolicy::ClientKey>>) -> Response {
791 let caller = owner_of(&key);
792 let data: Vec<serde_json::Value> = st
793 .default_daemon()
794 .batches()
795 .list()
796 .into_iter()
797 .filter(|b| visible(&b.owner, caller))
798 .map(|b| public(&b))
799 .collect();
800 (StatusCode::OK, Json(serde_json::json!({"object": "list", "data": data}))).into_response()
801}
802
803async fn batches_get(
804 State(st): State<AppState>,
805 key: Option<axum::Extension<crate::keypolicy::ClientKey>>,
806 axum::extract::Path(id): axum::extract::Path<String>,
807) -> Response {
808 match st.default_daemon().batches().get(&id).filter(|b| visible(&b.owner, owner_of(&key))) {
809 Some(b) => (StatusCode::OK, Json(public(&b))).into_response(),
810 None => error_response(StatusCode::NOT_FOUND, format!("no such batch: {id}")),
811 }
812}
813
814async fn batches_cancel(
815 State(st): State<AppState>,
816 key: Option<axum::Extension<crate::keypolicy::ClientKey>>,
817 ip_charge: Option<axum::Extension<DeferredIpCharge>>,
818 axum::extract::Path(id): axum::extract::Path<String>,
819) -> Response {
820 let charge = || -> Option<Response> {
821 let k = key.as_ref().map(|k| &*k.0 .0)?;
822 if !k.charge_rate() {
823 return Some(error_response_typed(
824 StatusCode::TOO_MANY_REQUESTS,
825 format!("API key '{}' exceeded its rate limit ({}/min)", k.name, k.rate_limit_rpm),
826 "rate_limit_error",
827 "rate_limit_exceeded",
828 ));
829 }
830 if let Some(axum::Extension(DeferredIpCharge(limiter, ip))) = &ip_charge {
831 if !limiter.allow(*ip) {
832 return Some(error_response_typed(
833 StatusCode::TOO_MANY_REQUESTS,
834 "Rate limit exceeded".to_string(),
835 "rate_limit_error",
836 "rate_limit_exceeded",
837 ));
838 }
839 }
840 None
841 };
842 let store = st.default_daemon().batches();
843 let not_found = || charge().unwrap_or_else(|| error_response(StatusCode::NOT_FOUND, format!("no such batch: {id}")));
844 if !store.get(&id).is_some_and(|b| visible(&b.owner, owner_of(&key))) {
845 return not_found();
846 }
847 match store.cancel(&id) {
848 Some((b, running)) => {
849 if !running {
850 if let Some(refused) = charge() {
851 return refused;
852 }
853 }
854 (StatusCode::OK, Json(public(&b))).into_response()
855 }
856 None => not_found(),
857 }
858}
859
860async fn files_upload(
861 State(st): State<AppState>,
862 key: Option<axum::Extension<crate::keypolicy::ClientKey>>,
863 mut mp: axum::extract::Multipart,
864) -> Response {
865 let mut filename = String::new();
866 let mut purpose = String::new();
867 let mut bytes: Option<Vec<u8>> = None;
868 loop {
869 let field = match mp.next_field().await {
870 Ok(Some(f)) => f,
871 Ok(None) => break,
872 Err(e) => return multipart_error(&e, "invalid multipart"),
873 };
874 let name = field.name().map(|s| s.to_string());
875 let fname = field.file_name().map(|s| s.to_string());
876 match name.as_deref() {
877 Some("file") => {
878 filename = fname.unwrap_or_else(|| "upload".to_string());
879 match field.bytes().await {
880 Ok(b) => bytes = Some(b.to_vec()),
881 Err(e) => return multipart_error(&e, "file read"),
882 }
883 }
884 Some("purpose") => purpose = field.text().await.unwrap_or_default(),
885 _ => {}
886 }
887 }
888 let Some(bytes) = bytes else {
889 return error_response(StatusCode::BAD_REQUEST, "missing `file` part".into());
890 };
891 match st.default_daemon().files().put(&filename, &purpose, &bytes, owner_of(&key)) {
892 Ok(meta) => (StatusCode::OK, Json(public(&meta))).into_response(),
893 Err(e @ DaemonError::FilesQuota { .. }) => error_response_typed(
894 StatusCode::PAYLOAD_TOO_LARGE,
895 e.to_string(),
896 "quota_exceeded",
897 "files_quota_exceeded",
898 ),
899 Err(e) => daemon_error(e),
900 }
901}
902
903async fn files_list(State(st): State<AppState>, key: Option<axum::Extension<crate::keypolicy::ClientKey>>) -> Response {
904 let caller = owner_of(&key);
905 let data: Vec<serde_json::Value> = st
906 .default_daemon()
907 .files()
908 .list()
909 .into_iter()
910 .filter(|m| visible(&m.owner, caller))
911 .map(|m| public(&m))
912 .collect();
913 (StatusCode::OK, Json(serde_json::json!({"object": "list", "data": data}))).into_response()
914}
915
916async fn files_get(
917 State(st): State<AppState>,
918 key: Option<axum::Extension<crate::keypolicy::ClientKey>>,
919 axum::extract::Path(id): axum::extract::Path<String>,
920) -> Response {
921 match st.default_daemon().files().get(&id).filter(|m| visible(&m.owner, owner_of(&key))) {
922 Some(m) => (StatusCode::OK, Json(public(&m))).into_response(),
923 None => error_response(StatusCode::NOT_FOUND, format!("no such file: {id}")),
924 }
925}
926
927async fn files_content(
928 State(st): State<AppState>,
929 key: Option<axum::Extension<crate::keypolicy::ClientKey>>,
930 axum::extract::Path(id): axum::extract::Path<String>,
931) -> Response {
932 let daemon = st.default_daemon();
933 let files = daemon.files();
934 let mine = files.get(&id).is_some_and(|m| visible(&m.owner, owner_of(&key)));
935 match files.content(&id).filter(|_| mine) {
936 Some(bytes) => (
937 StatusCode::OK,
938 [(axum::http::header::CONTENT_TYPE, "application/octet-stream")],
939 bytes,
940 )
941 .into_response(),
942 None => error_response(StatusCode::NOT_FOUND, format!("no such file: {id}")),
943 }
944}
945
946async fn files_delete(
947 State(st): State<AppState>,
948 key: Option<axum::Extension<crate::keypolicy::ClientKey>>,
949 axum::extract::Path(id): axum::extract::Path<String>,
950) -> Response {
951 let daemon = st.default_daemon();
952 let files = daemon.files();
953 let mine = files.get(&id).is_some_and(|m| visible(&m.owner, owner_of(&key)));
954 let deleted = mine && files.delete(&id);
955 let status = if deleted { StatusCode::OK } else { StatusCode::NOT_FOUND };
956 (status, Json(serde_json::json!({"id": id, "object": "file", "deleted": deleted}))).into_response()
957}
958
959#[derive(Deserialize)]
960struct RerankRequest {
961 model: Option<String>,
962 query: String,
963 #[serde(default)]
964 documents: Vec<String>,
965 top_n: Option<usize>,
966 #[serde(default)]
967 return_documents: bool,
968}
969
970fn cosine(a: &[f32], b: &[f32]) -> f32 {
971 let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
972 let na = a.iter().map(|x| x * x).sum::<f32>().sqrt();
973 let nb = b.iter().map(|x| x * x).sum::<f32>().sqrt();
974 if na == 0.0 || nb == 0.0 {
975 0.0
976 } else {
977 dot / (na * nb)
978 }
979}
980
981async fn rerank(
982 State(st): State<AppState>,
983 headers: HeaderMap,
984 key: Option<axum::Extension<crate::keypolicy::ClientKey>>,
985 body: axum::body::Bytes,
986) -> Response {
987 let class = match st.sync_class(&headers, &key) {
988 Ok(c) => c,
989 Err(resp) => return resp,
990 };
991 let req: RerankRequest = match serde_json::from_slice(&body) {
992 Ok(r) => r,
993 Err(e) => return error_response(StatusCode::BAD_REQUEST, format!("invalid request: {e}")),
994 };
995 let (model_name, daemon) = match st.resolve(req.model.as_deref()) {
996 Ok(pair) => pair,
997 Err(resp) => return resp,
998 };
999 if req.documents.is_empty() {
1000 return error_response(StatusCode::BAD_REQUEST, "documents must be non-empty".into());
1001 }
1002 let hold = key.as_ref().map(|k| k.0.clone());
1003 let out = tokio::task::spawn_blocking(move || {
1004 let _hold = hold;
1005 let q = daemon.embed_as(&req.query, class)?;
1006 let mut scored: Vec<(usize, f32)> = Vec::with_capacity(req.documents.len());
1007 for (i, doc) in req.documents.iter().enumerate() {
1008 let d = daemon.embed_as(doc, class)?;
1009 scored.push((i, cosine(&q, &d)));
1010 }
1011 scored.sort_by(|a, b| b.1.total_cmp(&a.1));
1012 scored.truncate(req.top_n.unwrap_or(req.documents.len()));
1013 let results: Vec<serde_json::Value> = scored
1014 .into_iter()
1015 .map(|(i, s)| {
1016 let mut r = serde_json::json!({"index": i, "relevance_score": s});
1017 if req.return_documents {
1018 r["document"] = serde_json::json!({"text": req.documents[i]});
1019 }
1020 r
1021 })
1022 .collect();
1023 Ok::<_, DaemonError>(results)
1024 })
1025 .await;
1026 match out {
1027 Ok(Ok(results)) => (
1028 StatusCode::OK,
1029 Json(serde_json::json!({
1030 "id": format!("rerank-{}", next_id().rsplit('-').next().unwrap_or("0")),
1031 "results": results,
1032 "model": model_name,
1033 "usage": {"prompt_tokens": 0, "total_tokens": 0},
1034 })),
1035 )
1036 .into_response(),
1037 Ok(Err(e)) => daemon_error(e),
1038 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
1039 }
1040}
1041
1042async fn audio_transcriptions(
1043 State(st): State<AppState>,
1044 headers: HeaderMap,
1045 key: Option<axum::Extension<crate::keypolicy::ClientKey>>,
1046 mp: axum::extract::Multipart,
1047) -> Response {
1048 let class = match st.sync_class(&headers, &key) {
1049 Ok(c) => c,
1050 Err(resp) => return resp,
1051 };
1052 audio_op(st, key, class, mp, false).await
1053}
1054
1055async fn audio_translations(
1056 State(st): State<AppState>,
1057 headers: HeaderMap,
1058 key: Option<axum::Extension<crate::keypolicy::ClientKey>>,
1059 mp: axum::extract::Multipart,
1060) -> Response {
1061 let class = match st.sync_class(&headers, &key) {
1062 Ok(c) => c,
1063 Err(resp) => return resp,
1064 };
1065 audio_op(st, key, class, mp, true).await
1066}
1067
1068fn ms_to_secs(ms: i32) -> f64 {
1069 ms as f64 / 1000.0
1070}
1071
1072fn subtitle_ts(ms: i32, comma: bool) -> String {
1073 let ms = ms.max(0);
1074 let (h, m, s, milli) = (ms / 3_600_000, (ms / 60_000) % 60, (ms / 1000) % 60, ms % 1000);
1075 let sep = if comma { ',' } else { '.' };
1076 format!("{h:02}:{m:02}:{s:02}{sep}{milli:03}")
1077}
1078
1079async fn audio_stream(
1080 daemon: std::sync::Arc<Daemon>,
1081 tmp_path: String,
1082 params: superfluid_engine::TranscribeParams,
1083 hold: Option<crate::keypolicy::ClientKey>,
1084 class: Option<u8>,
1085) -> Response {
1086 let (tx, rx) = tokio::sync::mpsc::channel::<serde_json::Value>(32);
1087 tokio::task::spawn_blocking(move || {
1088 let _hold = hold;
1089 let started = daemon.transcribe_streaming_as(&tmp_path, ¶ms, class);
1090 let (segs, done) = match started {
1091 Ok(pair) => pair,
1092 Err(e) => {
1093 let _ = std::fs::remove_file(&tmp_path);
1094 let _ = tx.blocking_send(serde_json::json!({"error": e.to_string()}));
1095 return;
1096 }
1097 };
1098 let mut full = String::new();
1099 let mut n = 0usize;
1100 loop {
1101 match segs.recv_timeout(std::time::Duration::from_millis(50)) {
1102 Ok(seg) => {
1103 let delta = if n == 0 { seg.text } else { format!(" {}", seg.text) };
1104 n += 1;
1105 full.push_str(&delta);
1106 if tx
1107 .blocking_send(
1108 serde_json::json!({"type": "transcript.text.delta", "delta": delta}),
1109 )
1110 .is_err()
1111 {
1112 break;
1113 }
1114 }
1115 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
1116 if tx.is_closed() {
1117 break;
1118 }
1119 }
1120 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
1121 }
1122 }
1123 drop(segs);
1124 let result = done.recv();
1125 let _ = std::fs::remove_file(&tmp_path);
1126 match result {
1127 Ok(Ok(t)) => {
1128 let _ = tx.blocking_send(serde_json::json!({
1129 "type": "transcript.text.done",
1130 "text": full,
1131 "languages": [{"code": t.language}],
1132 }));
1133 }
1134 Ok(Err(e)) => {
1135 let _ = tx.blocking_send(serde_json::json!({"error": format!("Transcription failed: {e}")}));
1136 }
1137 Err(_) => {
1138 let _ = tx.blocking_send(serde_json::json!({"error": "transcription worker went away"}));
1139 }
1140 }
1141 });
1142 let stream = ReceiverStream::new(rx)
1143 .map(|v| Event::default().data(v.to_string()))
1144 .chain(tokio_stream::once(Event::default().data("[DONE]")))
1145 .map(Ok::<_, std::convert::Infallible>);
1146 Sse::new(stream).keep_alive(KeepAlive::default()).into_response()
1147}
1148
1149fn audio_extension(filename: &str) -> Option<&str> {
1150 match std::path::Path::new(filename).extension() {
1151 None => Some("wav"),
1152 Some(e) => e
1153 .to_str()
1154 .filter(|e| (1..=8).contains(&e.len()) && e.bytes().all(|b| b.is_ascii_alphanumeric())),
1155 }
1156}
1157
1158fn stage_audio(dir: &std::path::Path, ext: &str, audio: &[u8]) -> std::io::Result<std::path::PathBuf> {
1161 use std::hash::{BuildHasher, Hasher};
1162 use std::io::Write;
1163 use std::os::unix::fs::OpenOptionsExt;
1164 let mut last = None;
1165 for _ in 0..8 {
1166 let nonce = std::collections::hash_map::RandomState::new().build_hasher().finish();
1167 let path = dir.join(format!("superfluid-audio-{nonce:016x}.{ext}"));
1168 match std::fs::OpenOptions::new().write(true).create_new(true).mode(0o600).open(&path) {
1169 Ok(mut f) => {
1170 if let Err(e) = f.write_all(audio) {
1171 let _ = std::fs::remove_file(&path);
1172 return Err(e);
1173 }
1174 return Ok(path);
1175 }
1176 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => last = Some(e),
1177 Err(e) => return Err(e),
1178 }
1179 }
1180 Err(last.unwrap_or_else(|| std::io::Error::other("no free staging name")))
1181}
1182
1183async fn audio_op(
1184 st: AppState,
1185 key: Option<axum::Extension<crate::keypolicy::ClientKey>>,
1186 class: Option<u8>,
1187 mut mp: axum::extract::Multipart,
1188 translate: bool,
1189) -> Response {
1190 let mut audio: Option<Vec<u8>> = None;
1191 let mut filename = String::from("audio.wav");
1192 let mut model: Option<String> = None;
1193 let mut language: Option<String> = None;
1194 let mut language_set = false;
1195 let mut response_format = String::from("json");
1196 let mut want_granularities = false;
1197 let mut prompt = String::new();
1198 let mut task: Option<String> = None;
1199 let mut stream = false;
1200 loop {
1201 let field = match mp.next_field().await {
1202 Ok(Some(f)) => f,
1203 Ok(None) => break,
1204 Err(e) => return multipart_error(&e, "invalid multipart"),
1205 };
1206 let name = field.name().map(|s| s.to_string());
1207 let fname = field.file_name().map(|s| s.to_string());
1208 match name.as_deref() {
1209 Some("file") => {
1210 if let Some(f) = fname {
1211 filename = f;
1212 }
1213 match field.bytes().await {
1214 Ok(b) => audio = Some(b.to_vec()),
1215 Err(e) => return multipart_error(&e, "file read"),
1216 }
1217 }
1218 Some("model") => model = Some(field.text().await.unwrap_or_default()),
1219 Some("language") => {
1220 language = Some(field.text().await.unwrap_or_default());
1221 language_set = true;
1222 }
1223 Some("response_format") => response_format = field.text().await.unwrap_or_else(|_| "json".into()),
1224 Some("prompt") => prompt = field.text().await.unwrap_or_default(),
1225 Some("task") if !translate => task = Some(field.text().await.unwrap_or_default()),
1226 Some("stream") => {
1227 let v = field.text().await.unwrap_or_default();
1228 stream = v == "true" || v == "1";
1229 }
1230 Some("timestamp_granularities") | Some("timestamp_granularities[]") => {
1231 want_granularities = true;
1232 }
1233 _ => {}
1234 }
1235 }
1236 let translate = translate || task.as_deref() == Some("translate");
1237 if model.is_none() && st.requires_model() {
1238 return error_response(StatusCode::BAD_REQUEST, "missing required field: model".into());
1239 }
1240 let (_model_name, daemon) = match st.resolve(model.as_deref()) {
1241 Ok(pair) => pair,
1242 Err(resp) => return resp,
1243 };
1244 let Some(audio) = audio else {
1245 return error_response(StatusCode::BAD_REQUEST, "missing `file` part".into());
1246 };
1247
1248 let Some(ext) = audio_extension(&filename) else {
1249 return error_response(
1250 StatusCode::BAD_REQUEST,
1251 format!("unsupported audio file name {filename:?}: the extension must be 1-8 ASCII letters or digits"),
1252 );
1253 };
1254 let tmp = match stage_audio(&std::env::temp_dir(), ext, &audio) {
1255 Ok(p) => p,
1256 Err(e) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, format!("stage audio: {e}")),
1257 };
1258 if want_granularities && !matches!(response_format.as_str(), "verbose_json" | "srt" | "vtt") {
1259 response_format = "verbose_json".into();
1260 }
1261 let want_timestamps = matches!(response_format.as_str(), "verbose_json" | "srt" | "vtt");
1262 let lang = match language {
1263 Some(l) if language_set => Some(l),
1264 _ if translate => Some(String::new()),
1265 _ => None,
1266 };
1267 if prompt.len() > 8192 {
1268 let mut cut = prompt.len() - 8192;
1269 while cut < prompt.len() && !prompt.is_char_boundary(cut) {
1270 cut += 1;
1271 }
1272 prompt.drain(..cut);
1273 }
1274 let params = superfluid_engine::TranscribeParams {
1275 language: lang,
1276 translate,
1277 timestamps: want_timestamps,
1278 prompt: (!prompt.is_empty()).then_some(prompt),
1279 };
1280 let tmp_path = tmp.to_string_lossy().into_owned();
1281
1282 if stream {
1283 return audio_stream(daemon, tmp_path, params, key.map(|k| k.0), class).await;
1284 }
1285
1286 let hold = key.as_ref().map(|k| k.0.clone());
1287 let out = tokio::task::spawn_blocking(move || {
1288 let _hold = hold;
1289 let r = daemon.transcribe_as(&tmp_path, ¶ms, class);
1290 let _ = std::fs::remove_file(&tmp_path);
1291 r
1292 })
1293 .await;
1294 let t = match out {
1295 Ok(Ok(t)) => t,
1296 Ok(Err(e)) => return error_response(StatusCode::BAD_REQUEST, e.to_string()),
1297 Err(e) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
1298 };
1299
1300 match response_format.as_str() {
1301 "text" => (StatusCode::OK, [(axum::http::header::CONTENT_TYPE, "text/plain; charset=utf-8")], t.text).into_response(),
1302 "srt" => {
1303 let mut s = String::new();
1304 for (i, seg) in t.segments.iter().enumerate() {
1305 s.push_str(&format!(
1306 "{}\n{} --> {}\n{}\n\n",
1307 i + 1,
1308 subtitle_ts(seg.start_ms, true),
1309 subtitle_ts(seg.end_ms, true),
1310 seg.text.trim()
1311 ));
1312 }
1313 (StatusCode::OK, [(axum::http::header::CONTENT_TYPE, "text/plain; charset=utf-8")], s).into_response()
1314 }
1315 "vtt" => {
1316 let mut s = String::from("WEBVTT\n\n");
1317 for seg in &t.segments {
1318 s.push_str(&format!(
1319 "{} --> {}\n{}\n\n",
1320 subtitle_ts(seg.start_ms, false),
1321 subtitle_ts(seg.end_ms, false),
1322 seg.text.trim()
1323 ));
1324 }
1325 (StatusCode::OK, [(axum::http::header::CONTENT_TYPE, "text/plain; charset=utf-8")], s).into_response()
1326 }
1327 "verbose_json" => {
1328 let segments: Vec<serde_json::Value> = t
1329 .segments
1330 .iter()
1331 .enumerate()
1332 .map(|(i, s)| {
1333 serde_json::json!({
1334 "id": i,
1335 "seek": 0,
1336 "start": ms_to_secs(s.start_ms),
1337 "end": ms_to_secs(s.end_ms),
1338 "text": s.text,
1339 "tokens": [],
1340 "temperature": s.temperature,
1341 "avg_logprob": s.avg_logprob,
1342 "compression_ratio": s.compression_ratio,
1343 "no_speech_prob": s.no_speech_prob,
1344 })
1345 })
1346 .collect();
1347 let joined = t
1348 .segments
1349 .iter()
1350 .map(|s| s.text.as_str())
1351 .collect::<Vec<_>>()
1352 .join(" ");
1353 Json(serde_json::json!({
1354 "task": if translate { "translate" } else { "transcribe" },
1355 "language": t.language,
1356 "duration": ms_to_secs(t.duration_ms),
1357 "text": joined,
1358 "segments": segments,
1359 }))
1360 .into_response()
1361 }
1362 _ => Json(serde_json::json!({ "text": t.text })).into_response(),
1363 }
1364}
1365
1366#[derive(Deserialize)]
1367struct LoraLoadRequest {
1368 #[serde(default)]
1369 path: String,
1370 #[serde(default)]
1371 adapter: Option<String>,
1372}
1373
1374async fn lora_load(
1375 State(st): State<AppState>,
1376 headers: HeaderMap,
1377 key: Option<axum::Extension<crate::keypolicy::ClientKey>>,
1378 body: axum::body::Bytes,
1379) -> Response {
1380 let class = match st.sync_class(&headers, &key) {
1381 Ok(c) => c,
1382 Err(resp) => return resp,
1383 };
1384 let req: LoraLoadRequest = match serde_json::from_slice(&body) {
1385 Ok(r) => r,
1386 Err(e) => return error_response(StatusCode::BAD_REQUEST, format!("invalid request: {e}")),
1387 };
1388 let path = req.adapter.unwrap_or(req.path);
1389 if path.is_empty() {
1390 return error_response(StatusCode::BAD_REQUEST, "missing adapter `path`".into());
1391 }
1392 let daemon = st.default_daemon();
1393 match tokio::task::spawn_blocking(move || crate::with_sync_class(class, || daemon.lora_load(&path))).await {
1394 Ok(Ok(id)) => (StatusCode::OK, Json(serde_json::json!({"loaded": true, "id": id}))).into_response(),
1395 Ok(Err(e)) => daemon_error(e),
1396 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
1397 }
1398}
1399
1400async fn lora_unload(
1401 State(st): State<AppState>,
1402 headers: HeaderMap,
1403 key: Option<axum::Extension<crate::keypolicy::ClientKey>>,
1404) -> Response {
1405 let class = match st.sync_class(&headers, &key) {
1406 Ok(c) => c,
1407 Err(resp) => return resp,
1408 };
1409 let daemon = st.default_daemon();
1410 match tokio::task::spawn_blocking(move || crate::with_sync_class(class, || daemon.lora_unload())).await {
1411 Ok(Ok(_)) => (StatusCode::OK, Json(serde_json::json!({"unloaded": true}))).into_response(),
1412 Ok(Err(e)) => daemon_error(e),
1413 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
1414 }
1415}
1416
1417async fn lora_list(
1418 State(st): State<AppState>,
1419 headers: HeaderMap,
1420 key: Option<axum::Extension<crate::keypolicy::ClientKey>>,
1421) -> Response {
1422 let class = match st.sync_class(&headers, &key) {
1423 Ok(c) => c,
1424 Err(resp) => return resp,
1425 };
1426 let model = st.default_name();
1427 let daemon = st.default_daemon();
1428 match tokio::task::spawn_blocking(move || crate::with_sync_class(class, || daemon.lora_id())).await {
1429 Ok(Ok(id)) => {
1430 let data = vec![serde_json::json!({
1431 "model": model,
1432 "adapter": id.clone().unwrap_or_default(),
1433 "active": id.is_some(),
1434 })];
1435 (StatusCode::OK, Json(serde_json::json!({"object": "list", "data": data}))).into_response()
1436 }
1437 Ok(Err(e)) => daemon_error(e),
1438 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
1439 }
1440}
1441
1442#[derive(Deserialize)]
1443struct EmbeddingRequest {
1444 model: Option<String>,
1445 input: serde_json::Value,
1446 #[serde(default)]
1447 encoding_format: Option<String>,
1448}
1449
1450async fn embeddings(
1451 State(st): State<AppState>,
1452 headers: HeaderMap,
1453 key: Option<axum::Extension<crate::keypolicy::ClientKey>>,
1454 body: axum::body::Bytes,
1455) -> Response {
1456 let class = match st.sync_class(&headers, &key) {
1457 Ok(c) => c,
1458 Err(resp) => return resp,
1459 };
1460 let req: EmbeddingRequest = match serde_json::from_slice(&body) {
1461 Ok(r) => r,
1462 Err(e) => return error_response(StatusCode::BAD_REQUEST, format!("invalid request: {e}")),
1463 };
1464 if req.model.is_none() && st.requires_model() {
1465 return error_response(StatusCode::BAD_REQUEST, "missing required field: model".into());
1466 }
1467 let (model_name, daemon) = match st.resolve(req.model.as_deref()) {
1468 Ok(pair) => pair,
1469 Err(resp) => return resp,
1470 };
1471 let inputs: Vec<String> = match &req.input {
1472 serde_json::Value::String(s) => vec![s.clone()],
1473 serde_json::Value::Array(a) => {
1474 let mut v = Vec::with_capacity(a.len());
1475 for item in a {
1476 match item.as_str() {
1477 Some(s) => v.push(s.to_string()),
1478 None => return error_response(StatusCode::BAD_REQUEST, "input array must be strings".into()),
1479 }
1480 }
1481 v
1482 }
1483 _ => return error_response(StatusCode::BAD_REQUEST, "input must be a string or array of strings".into()),
1484 };
1485 if inputs.is_empty() {
1486 return error_response(StatusCode::BAD_REQUEST, "input must be non-empty".into());
1487 }
1488 let base64 = req.encoding_format.as_deref() == Some("base64");
1489 let hold = key.as_ref().map(|k| k.0.clone());
1490 let out = tokio::task::spawn_blocking(move || {
1491 let _hold = hold;
1492 let mut data = Vec::with_capacity(inputs.len());
1493 let mut prompt_tokens = 0u64;
1494 for (i, text) in inputs.iter().enumerate() {
1495 prompt_tokens += daemon.tokenize(text).len() as u64;
1496 let v = daemon.embed_as(text, class)?;
1497 let embedding = if base64 {
1498 use base64::Engine;
1499 let mut bytes = Vec::with_capacity(v.len() * 4);
1500 for f in &v {
1501 bytes.extend_from_slice(&f.to_le_bytes());
1502 }
1503 serde_json::Value::String(base64::engine::general_purpose::STANDARD.encode(bytes))
1504 } else {
1505 serde_json::json!(v)
1506 };
1507 data.push(serde_json::json!({"object": "embedding", "index": i, "embedding": embedding}));
1508 }
1509 Ok::<_, DaemonError>((data, prompt_tokens))
1510 })
1511 .await;
1512 let (data, prompt_tokens) = match out {
1513 Ok(Ok(v)) => v,
1514 Ok(Err(e)) => return daemon_error(e),
1515 Err(e) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
1516 };
1517 let json = serde_json::json!({
1518 "object": "list",
1519 "data": data,
1520 "model": *model_name,
1521 "usage": {"prompt_tokens": prompt_tokens, "total_tokens": prompt_tokens},
1522 });
1523 (StatusCode::OK, Json(json)).into_response()
1524}
1525
1526async fn completions(
1527 State(st): State<AppState>,
1528 key: Option<axum::Extension<crate::keypolicy::ClientKey>>,
1529 axum::extract::ConnectInfo(peer): axum::extract::ConnectInfo<std::net::SocketAddr>,
1530 cred: Option<axum::Extension<AuthenticatedCredential>>,
1531 headers: HeaderMap,
1532 body: axum::body::Bytes,
1533) -> Response {
1534 let req: CompletionRequest = match serde_json::from_slice(&body) {
1535 Ok(req) => req,
1536 Err(e) => return error_response(StatusCode::BAD_REQUEST, format!("invalid request: {e}")),
1537 };
1538 let keepalive = st.nonstream_keepalive.filter(|_| !req.stream && req.suffix.is_none());
1539 crate::keepalive::respond(keepalive, move |hooks| async move {
1540 match crate::http_generation::completions(State(st), key, axum::extract::ConnectInfo(peer), cred, headers, req, hooks).await {
1541 Ok(generation) => generation_response(generation),
1542 Err(response) => response,
1543 }
1544 })
1545 .await
1546}
1547
1548fn multipart_error(e: &axum::extract::multipart::MultipartError, what: &str) -> Response {
1549 let status = e.status();
1550 if status == StatusCode::PAYLOAD_TOO_LARGE {
1551 return error_response_code(status, "Request body too large".into(), "payload_too_large");
1552 }
1553 error_response(status, format!("{what}: {e}"))
1554}
1555
1556pub(crate) struct ModelObject<'a> {
1557 pub(crate) id: &'a str,
1558 pub(crate) created: u64,
1559 pub(crate) n_ctx: Option<u32>,
1560 pub(crate) owned_by: &'a str,
1561 pub(crate) loaded: bool,
1562 pub(crate) capabilities: Option<serde_json::Value>,
1563}
1564
1565impl ModelObject<'_> {
1566 pub(crate) fn build(self) -> serde_json::Value {
1567 let mut meta = serde_json::Map::new();
1568 if let Some(n) = self.n_ctx {
1569 meta.insert("n_ctx".into(), n.into());
1570 }
1571 let mut v = serde_json::json!({
1572 "id": self.id,
1573 "object": "model",
1574 "created": self.created,
1575 "owned_by": self.owned_by,
1576 "loaded": self.loaded,
1577 "meta": meta,
1578 "architecture": {"input_modalities": input_modalities(self.capabilities.as_ref())},
1579 });
1580 if let Some(caps) = self.capabilities {
1581 if let Some(rt) = caps.get("runtime").filter(|r| r.is_object()) {
1582 v["runtime"] = rt.clone();
1583 }
1584 v["capabilities"] = caps;
1585 }
1586 v
1587 }
1588}
1589
1590fn input_modalities(capabilities: Option<&serde_json::Value>) -> Vec<&'static str> {
1591 let mut m = vec!["text"];
1592 let Some(mods) = capabilities.and_then(|c| c.get("modalities")) else {
1593 return m;
1594 };
1595 let supported = |k: &str| mods.get(k).and_then(|v| v.as_bool()).unwrap_or(false);
1596 if supported("image_encode") {
1597 m.push("image");
1598 }
1599 if supported("gemma_audio_encode") {
1600 m.push("audio");
1601 }
1602 m
1603}
1604
1605fn capabilities_for(st: &AppState, id: &str, loaded: bool) -> Option<serde_json::Value> {
1606 let resolved = loaded.then(|| st.registry.resolve(Some(id))).flatten();
1607 let (_, d) = resolved.as_ref()?;
1608 let dialect = serde_json::json!({
1609 "enable_thinking": d.supports_enable_thinking(),
1610 "reasoning_effort": d.supports_reasoning_effort(),
1611 "reasoning_effort_levels": d.reasoning_effort_levels(),
1612 "fim": d.fim_supported(),
1613 "fim_model": d.fim_satellite_name(),
1614 });
1615 match d.capability_descriptor() {
1616 Some(mut c) => {
1617 match c.as_object_mut() {
1618 Some(obj) => {
1619 obj.insert("dialect".into(), dialect);
1620 Some(c)
1621 }
1622 None => Some(serde_json::json!({"dialect": dialect})),
1623 }
1624 }
1625 None => Some(serde_json::json!({"dialect": dialect})),
1626 }
1627}
1628
1629async fn models(
1630 State(st): State<AppState>,
1631 headers: HeaderMap,
1632 key: Option<axum::Extension<crate::keypolicy::ClientKey>>,
1633) -> Response {
1634 let class = match st.sync_class(&headers, &key) {
1635 Ok(c) => c,
1636 Err(resp) => return resp,
1637 };
1638 let created = unix_now();
1639 let st2 = st.clone();
1640 let data = tokio::task::spawn_blocking(move || {
1641 crate::with_sync_class(class, || models_data(&st2, created))
1642 })
1643 .await;
1644 match data {
1645 Ok(data) => Json(serde_json::json!({"object": "list", "data": data})).into_response(),
1646 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
1647 }
1648}
1649
1650fn models_data(st: &AppState, created: u64) -> Vec<serde_json::Value> {
1651 st.registry
1652 .all_names()
1653 .into_iter()
1654 .map(|id| {
1655 let loaded = st.registry.is_loaded(&id);
1656 let capabilities = capabilities_for(st, &id, loaded);
1657 ModelObject {
1658 id: &id,
1659 created,
1660 n_ctx: Some(st.window_of(&id)),
1661 owned_by: &st.identity.name,
1662 loaded,
1663 capabilities,
1664 }
1665 .build()
1666 })
1667 .collect()
1668}
1669
1670async fn models_retrieve(
1671 State(st): State<AppState>,
1672 headers: HeaderMap,
1673 key: Option<axum::Extension<crate::keypolicy::ClientKey>>,
1674 axum::extract::Path(model): axum::extract::Path<String>,
1675) -> Response {
1676 let class = match st.sync_class(&headers, &key) {
1677 Ok(c) => c,
1678 Err(resp) => return resp,
1679 };
1680 let loaded = st.registry.is_loaded(&model);
1681 if loaded || st.registry.is_known(&model) {
1682 let capabilities = {
1683 let (st, model) = (st.clone(), model.clone());
1684 match tokio::task::spawn_blocking(move || {
1685 crate::with_sync_class(class, || capabilities_for(&st, &model, loaded))
1686 })
1687 .await
1688 {
1689 Ok(c) => c,
1690 Err(e) => return error_response(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
1691 }
1692 };
1693 let obj = ModelObject {
1694 id: &model,
1695 created: unix_now(),
1696 n_ctx: Some(st.window_of(&model)),
1697 owned_by: &st.identity.name,
1698 loaded,
1699 capabilities,
1700 }
1701 .build();
1702 (StatusCode::OK, Json(obj)).into_response()
1703 } else {
1704 error_response(StatusCode::NOT_FOUND, format!("model '{model}' not found"))
1705 }
1706}
1707
1708async fn models_delete(
1709 State(st): State<AppState>,
1710 axum::extract::Path(model): axum::extract::Path<String>,
1711) -> Response {
1712 unload_response(&st, model).await
1713}
1714
1715async fn unload_response(st: &AppState, id: String) -> Response {
1716 let registry = Arc::clone(&st.registry);
1717 let name = id.clone();
1718 match tokio::task::spawn_blocking(move || registry.unload(&name)).await {
1719 Ok(Ok(removed)) => (
1720 StatusCode::OK,
1721 Json(serde_json::json!({"id": id, "object": "model", "deleted": removed})),
1722 )
1723 .into_response(),
1724 Ok(Err(e)) => daemon_error(e),
1725 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
1726 }
1727}
1728
1729#[derive(Deserialize)]
1730struct ModelLoadRequest {
1731 #[serde(alias = "model", alias = "name")]
1732 id: String,
1733 #[serde(default)]
1734 runtime: Option<String>,
1735}
1736
1737async fn models_load(
1738 State(st): State<AppState>,
1739 headers: HeaderMap,
1740 key: Option<axum::Extension<crate::keypolicy::ClientKey>>,
1741 body: axum::body::Bytes,
1742) -> Response {
1743 let class = match st.sync_class(&headers, &key) {
1744 Ok(c) => c,
1745 Err(resp) => return resp,
1746 };
1747 let req: ModelLoadRequest = match serde_json::from_slice(&body) {
1748 Ok(r) => r,
1749 Err(e) => return error_response(StatusCode::BAD_REQUEST, format!("invalid request: {e}")),
1750 };
1751 if req.id.is_empty() {
1752 return error_response(StatusCode::BAD_REQUEST, "missing model `id`".into());
1753 }
1754 let st2 = st.clone();
1755 let name = req.id.clone();
1756 let wanted = req.runtime.clone();
1757 let loaded = tokio::task::spawn_blocking(move || {
1758 let runtime = match wanted.as_deref() {
1759 None => None,
1760 Some(r) => Some(st2.registry.runtime_named(r).map_err(|why| {
1761 DaemonError::Unsupported(crate::Refusal::new("unsupported_runtime", Some("runtime"), why))
1762 })?),
1763 };
1764 st2.registry.load_on(&name, runtime)?;
1765 Ok::<_, DaemonError>(crate::with_sync_class(class, || capabilities_for(&st2, &name, true)))
1766 })
1767 .await;
1768 match loaded {
1769 Ok(Ok(capabilities)) => (
1770 StatusCode::OK,
1771 Json(
1772 ModelObject {
1773 id: &req.id,
1774 created: unix_now(),
1775 n_ctx: Some(st.window_of(&req.id)),
1776 owned_by: &st.identity.name,
1777 loaded: true,
1778 capabilities,
1779 }
1780 .build(),
1781 ),
1782 )
1783 .into_response(),
1784 Ok(Err(e)) => daemon_error(e),
1785 Err(e) => error_response(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
1786 }
1787}
1788
1789async fn models_unload(State(st): State<AppState>, body: axum::body::Bytes) -> Response {
1790 let req: ModelLoadRequest = match serde_json::from_slice(&body) {
1791 Ok(r) => r,
1792 Err(e) => return error_response(StatusCode::BAD_REQUEST, format!("invalid request: {e}")),
1793 };
1794 unload_response(&st, req.id).await
1795}
1796
1797async fn health(State(st): State<AppState>) -> Response {
1800 let names = st.registry.names();
1801 let known = st.registry.all_names().len();
1802 let Some((up, nodes)) = st.default_daemon().fleet_nodes() else {
1803 return Json(serde_json::json!({"status": "ok", "models_loaded": names.len(), "models_known": known})).into_response();
1804 };
1805 let mut models = serde_json::Map::new();
1806 for name in &names {
1807 if let Some((serving, _)) = st.registry.resolve(Some(name)).and_then(|(_, d)| d.fleet_serving()) {
1808 models.insert(name.clone(), serde_json::json!({"nodes_up": serving}));
1809 }
1810 }
1811 let servable = models.values().any(|m| m["nodes_up"].as_u64().is_some_and(|n| n > 0));
1812 let status = if servable { axum::http::StatusCode::OK } else { axum::http::StatusCode::SERVICE_UNAVAILABLE };
1813 let body = serde_json::json!({
1814 "status": if servable { "ok" } else { "no_nodes" },
1815 "models_loaded": names.len(),
1816 "models_known": known,
1817 "nodes_up": up,
1818 "nodes_known": nodes,
1819 "models": models,
1820 });
1821 (status, Json(body)).into_response()
1822}
1823
1824#[derive(Deserialize, Default)]
1825struct PropsQuery {
1826 model: Option<String>,
1827 autoload: Option<String>,
1828}
1829
1830impl PropsQuery {
1831 fn autoloads(&self) -> bool {
1832 matches!(self.autoload.as_deref(), Some("1" | "true"))
1833 }
1834}
1835
1836async fn props(
1837 State(st): State<AppState>,
1838 axum::extract::Query(query): axum::extract::Query<PropsQuery>,
1839 headers: HeaderMap,
1840 key: Option<axum::Extension<crate::keypolicy::ClientKey>>,
1841) -> Response {
1842 let class = match st.sync_class(&headers, &key) {
1843 Ok(c) => c,
1844 Err(resp) => return resp,
1845 };
1846 let (default_name, default_daemon) = st
1847 .registry
1848 .resolve(None)
1849 .expect("a default model is always loaded");
1850 let (name, daemon, loaded) = match query.model.as_deref().filter(|m| !m.is_empty()) {
1851 None => (default_name.clone(), default_daemon, true),
1852 Some(m) if st.registry.is_loaded(m) || (query.autoloads() && st.registry.is_known(m)) => {
1853 match st.resolve(Some(m)) {
1854 Ok((n, d)) => (n, d, true),
1855 Err(resp) => return resp,
1856 }
1857 }
1858 Some(m) if st.registry.is_known(m) => (m.to_string(), default_daemon, false),
1859 Some(m) => {
1860 return error_response_code(
1861 StatusCode::NOT_FOUND,
1862 format!("model '{m}' not found; loaded: {}", st.registry.names().join(", ")),
1863 "model_not_found",
1864 )
1865 }
1866 };
1867 let sampling_daemon = daemon.clone();
1868 let (resolved, capabilities) = if loaded {
1869 tokio::task::spawn_blocking(move || {
1870 crate::with_sync_class(class, || {
1871 (sampling_daemon.try_model_sampling_defaults(), sampling_daemon.capability_descriptor())
1872 })
1873 })
1874 .await
1875 .unwrap_or((None, None))
1876 } else {
1877 (None, None)
1878 };
1879 let chat_template = if loaded { daemon.chat_template() } else { None };
1880 let eff_repeat = (resolved.is_some() || st.sampling.repeat_penalty.is_some() || st.sampling.fallback.is_some()).then(|| {
1881 match repeat_penalty_for(None, &st.sampling, &resolved.clone().unwrap_or_default()) {
1882 0.0 => 1.0,
1883 v => v,
1884 }
1885 });
1886 let window = match daemon.max_stream_tokens() {
1887 n if loaded && n > 0 => u32::try_from(n).unwrap_or(u32::MAX),
1888 _ => st.limits.max_context,
1889 };
1890 let mut settings = serde_json::json!({ "max_context": window });
1891 if let Some(v) = eff_repeat {
1892 settings["repeat_penalty"] = v.into();
1893 }
1894 let eff = advertised_generation_defaults(&resolved.clone().unwrap_or_default(), &st.sampling.for_daemon(&daemon));
1895 if resolved.is_some() || st.sampling.temperature.is_some() || st.sampling.fallback.is_some() {
1896 settings["temperature"] = eff.temperature.into();
1897 }
1898 if resolved.is_some() || st.sampling.truncates() {
1899 settings["top_p"] = eff.top_p.into();
1900 settings["top_k"] = eff.top_k.into();
1901 settings["min_p"] = eff.min_p.into();
1902 }
1903 let advertised = st.limits.default_max_tokens.unwrap_or(st.limits.max_context);
1904 settings["n_predict"] = advertised.into();
1905 settings["max_tokens"] = advertised.into();
1906 settings["n_ctx"] = window.into();
1907 let mut body = serde_json::json!({
1908 "model": name,
1909 "default_model": default_name,
1910 "models": st.registry.names(),
1911 "max_context": window,
1912 "total_slots": 1,
1913 "default_generation_settings": settings,
1914 "build": {"version": st.identity.version, "target": engine_target()},
1915 "server": st.identity.name,
1916 "max_request_bytes": MAX_BODY_SIZE,
1917 "capabilities": capabilities.unwrap_or(serde_json::Value::Null),
1918 });
1919 body["loaded"] = loaded.into();
1920 if let Some(template) = chat_template {
1921 body["chat_template"] = template.into();
1922 }
1923 if loaded {
1924 let bits = daemon.sched_stats().kv_bits.load(std::sync::atomic::Ordering::Relaxed);
1925 if let Some(label) = crate::scheduler::kv_bits_label(bits) {
1926 body["kv_cache"] = serde_json::json!({ "bits": bits, "dtype": label });
1927 }
1928 }
1929 Json(body).into_response()
1930}
1931
1932fn engine_target() -> &'static str {
1933 if cfg!(target_os = "macos") {
1934 "metal"
1935 } else {
1936 "cuda"
1937 }
1938}
1939
1940async fn slots(State(st): State<AppState>) -> Json<serde_json::Value> {
1941 use std::sync::atomic::Ordering;
1942 let (name, daemon) = st.registry.resolve(None).expect("a default model is always loaded");
1943 let s = daemon.sched_stats();
1944 let active = s.lanes_active.load(Ordering::Relaxed);
1945 let tick = s.tick_in_flight().unwrap_or_default();
1946 let in_flight_ms = if tick.since_ms == 0 {
1947 0
1948 } else {
1949 crate::scheduler::uptime_ms().saturating_sub(tick.since_ms)
1950 };
1951 Json(serde_json::json!([{
1952 "id": 0,
1953 "model": name,
1954 "is_processing": active > 0,
1955 "cache_position": s.pool_blocks_used.load(Ordering::Relaxed),
1956 "reuse_snapshots": 0,
1957 "last_access": unix_now(),
1958 "idle_seconds": 0,
1959 "path": name,
1960 "lanes_active": active,
1961 "kv_pool_used": s.pool_blocks_used.load(Ordering::Relaxed),
1962 "kv_pool_total": s.pool_blocks_total.load(Ordering::Relaxed),
1963 "tick_wall_ms": s.tick_wall_ms_live.load(Ordering::Relaxed),
1964 "tick_in_flight_ms": in_flight_ms,
1965 "tick_plan_prefill_tokens": tick.prefill_tokens,
1966 "tick_plan_prefill_lanes": tick.prefill_lanes,
1967 "tick_plan_decode_lanes": tick.decode_lanes,
1968 "tick_plan_admits": tick.admits,
1969 "tick_plan_retires": tick.retires,
1970 "decode_tok_s": s.decode_rate_live.load(Ordering::Relaxed),
1971 "prefill_tok_s": s.prefill_rate_live.load(Ordering::Relaxed),
1972 "ttft_last_ms": s.ttft_last_ms.load(Ordering::Relaxed),
1973 }]))
1974}
1975
1976#[derive(Deserialize)]
1977struct TokenizeRequest {
1978 #[serde(default)]
1979 text: String,
1980 #[serde(default)]
1981 content: Option<String>,
1982}
1983
1984async fn tokenize(State(st): State<AppState>, body: axum::body::Bytes) -> Response {
1985 let req: TokenizeRequest = match serde_json::from_slice(&body) {
1986 Ok(r) => r,
1987 Err(e) => return error_response(StatusCode::BAD_REQUEST, format!("invalid request: {e}")),
1988 };
1989 let text = req.content.unwrap_or(req.text);
1990 let tokens = st.default_daemon().tokenize(&text);
1991 let count = tokens.len();
1992 (StatusCode::OK, Json(serde_json::json!({"tokens": tokens, "count": count}))).into_response()
1993}
1994
1995async fn chat_completions(
1996 State(st): State<AppState>,
1997 key: Option<axum::Extension<crate::keypolicy::ClientKey>>,
1998 headers: HeaderMap,
1999 body: axum::body::Bytes,
2000) -> Response {
2001 let req: ChatRequest = match serde_json::from_slice(&body) {
2002 Ok(req) => req,
2003 Err(e) => return error_response(StatusCode::BAD_REQUEST, format!("invalid request: {e}")),
2004 };
2005 let keepalive = st.nonstream_keepalive.filter(|_| !req.stream);
2006 crate::keepalive::respond(keepalive, move |hooks| async move {
2007 match crate::http_generation::chat(State(st), key, headers, req, hooks).await {
2008 Ok(generation) => generation_response(generation),
2009 Err(response) => response,
2010 }
2011 })
2012 .await
2013}
2014
2015#[cfg(test)]
2016mod input_modality_tests {
2017 use super::*;
2018
2019 fn caps(modalities: serde_json::Value) -> serde_json::Value {
2020 serde_json::json!({"descriptor_version": 1, "modalities": modalities})
2021 }
2022
2023 #[test]
2024 fn text_is_the_floor_without_a_descriptor() {
2025 assert_eq!(input_modalities(None), vec!["text"]);
2026 }
2027
2028 #[test]
2029 fn a_descriptor_without_modalities_is_text_only() {
2030 let c = serde_json::json!({"descriptor_version": 1});
2031 assert_eq!(input_modalities(Some(&c)), vec!["text"]);
2032 }
2033
2034 #[test]
2035 fn a_vision_bundle_advertises_image() {
2036 let c = caps(serde_json::json!({"image_encode": true}));
2037 assert_eq!(input_modalities(Some(&c)), vec!["text", "image"]);
2038 }
2039
2040 #[test]
2041 fn a_refusal_reason_is_not_a_yes() {
2042 let c = caps(serde_json::json!({
2043 "image_encode": "no vision tower in bundle",
2044 "gemma_audio_encode": "the Conformer tower's kernels are not ported to this backend",
2045 }));
2046 assert_eq!(input_modalities(Some(&c)), vec!["text"]);
2047 }
2048
2049 #[test]
2050 fn a_gemma_audio_bundle_advertises_audio() {
2051 let c = caps(serde_json::json!({"image_encode": true, "gemma_audio_encode": true}));
2052 assert_eq!(input_modalities(Some(&c)), vec!["text", "image", "audio"]);
2053 }
2054
2055 #[test]
2056 fn whisper_never_advertises_audio() {
2057 let c = caps(serde_json::json!({
2058 "image_encode": "no vision tower in bundle",
2059 "gemma_audio_encode": "no audio tower in bundle",
2060 "whisper_transcribe": true,
2061 "whisper_translate": true,
2062 }));
2063 assert_eq!(input_modalities(Some(&c)), vec!["text"]);
2064 }
2065
2066 #[test]
2067 fn n_ctx_is_omitted_rather_than_guessed() {
2068 let v = ModelObject {
2069 id: "m",
2070 created: 0,
2071 n_ctx: None,
2072 owned_by: "superfluid-fleet",
2073 loaded: true,
2074 capabilities: None,
2075 }
2076 .build();
2077 assert!(v["meta"].is_object(), "meta stays present: {v}");
2078 assert!(v["meta"]["n_ctx"].is_null(), "unknown n_ctx is absent: {v}");
2079 assert_eq!(v["owned_by"], "superfluid-fleet");
2080 }
2081}
2082
2083#[cfg(test)]
2084mod browser_guard_tests {
2085 use super::{browser_refusal, HostGuard};
2086 use axum::http::HeaderMap;
2087
2088 fn headers(pairs: &[(&'static str, &str)]) -> HeaderMap {
2089 let mut h = HeaderMap::new();
2090 for (k, v) in pairs {
2091 h.insert(*k, v.parse().unwrap());
2092 }
2093 h
2094 }
2095
2096 fn this_machine() -> HostGuard {
2097 HostGuard::ThisMachine(vec!["box.lan".to_string(), "box.local".to_string()].into())
2098 }
2099
2100 #[test]
2101 fn a_page_from_another_origin_is_refused_and_a_local_one_is_not() {
2102 for origin in ["https://evil.example", "null", "http://192.168.1.5:3000", "http://127.0.0.1.evil.example"] {
2103 assert!(browser_refusal(&headers(&[("origin", origin)]), &HostGuard::Any).is_some(), "{origin}");
2104 }
2105 for origin in ["http://localhost:3000", "http://127.0.0.1:8453", "http://[::1]:8080", "http://app.localhost"] {
2106 let h = headers(&[("origin", origin), ("host", "127.0.0.1:8453")]);
2107 assert!(browser_refusal(&h, &HostGuard::Loopback).is_none(), "{origin}");
2108 }
2109 assert!(browser_refusal(&headers(&[("host", "127.0.0.1:8453")]), &HostGuard::Loopback).is_none(), "curl sends no Origin");
2110 }
2111
2112 #[test]
2113 fn a_loopback_server_answers_only_to_a_loopback_host() {
2114 for host in ["127.0.0.1:8453", "localhost:8453", "localhost", "[::1]:8453", "127.0.0.2"] {
2115 assert!(browser_refusal(&headers(&[("host", host)]), &HostGuard::Loopback).is_none(), "{host}");
2116 }
2117 for host in ["rebound.evil.example:8453", "10.0.0.4:8453", "[2001:db8::1]:8453"] {
2118 assert!(browser_refusal(&headers(&[("host", host)]), &HostGuard::Loopback).is_some(), "{host}");
2119 assert!(browser_refusal(&headers(&[("host", host)]), &HostGuard::Any).is_none(), "{host}: keyed, off loopback");
2120 }
2121 }
2122
2123 #[test]
2124 fn a_keyless_server_off_loopback_answers_only_to_an_ip_or_its_own_name() {
2125 for host in [
2126 "10.0.0.4:8453", "192.168.1.5", "[2001:db8::1]:8453", "127.0.0.1:8453", "localhost:8453",
2127 "app.localhost", "box.lan:8453", "BOX.LOCAL.", "box.local:8453",
2128 ] {
2129 assert!(browser_refusal(&headers(&[("host", host)]), &this_machine()).is_none(), "{host}");
2130 }
2131 for host in ["rebound.evil.example:8453", "box.lan.evil.example", "box", "10.0.0.4.nip.io:8453"] {
2132 let why = browser_refusal(&headers(&[("host", host)]), &this_machine());
2133 assert!(why.as_deref().is_some_and(|w| w.contains("--api-key")), "{host}: {why:?}");
2134 }
2135 }
2136
2137 #[test]
2138 fn this_machines_names_include_its_mdns_name() {
2139 let HostGuard::ThisMachine(names) =
2140 HostGuard::for_listener(&super::ServeConfig::default(), false)
2141 else {
2142 panic!("a keyless server off loopback guards its Host")
2143 };
2144 assert!(names.iter().any(|n| n.ends_with(".local")), "{names:?}");
2145 let keyed = super::ServeConfig { api_key: Some("k".into()), ..Default::default() };
2146 assert!(matches!(HostGuard::for_listener(&keyed, false), HostGuard::Any));
2147 assert!(matches!(HostGuard::for_listener(&keyed, true), HostGuard::Loopback));
2148 }
2149}
2150
2151#[cfg(test)]
2152mod audio_staging_tests {
2153 use super::{audio_extension, stage_audio};
2154 use std::os::unix::fs::PermissionsExt;
2155
2156 #[test]
2157 fn the_client_extension_is_kept_only_when_it_is_plain() {
2158 assert_eq!(audio_extension("clip.mp3"), Some("mp3"));
2159 assert_eq!(audio_extension("clip"), Some("wav"));
2160 assert_eq!(audio_extension("a.b.flac"), Some("flac"));
2161 for bad in ["clip.wav;rm -rf", "clip.", "clip.toolongext", "clip.w\u{e9}v", "x.a b"] {
2162 assert_eq!(audio_extension(bad), None, "{bad}");
2163 }
2164 }
2165
2166 #[test]
2167 fn staged_audio_is_owner_only_and_never_reuses_a_path() {
2168 let dir = std::env::temp_dir().join(format!("superfluid-audio-stage-{}", std::process::id()));
2169 std::fs::create_dir_all(&dir).unwrap();
2170 let a = stage_audio(&dir, "wav", b"RIFF").unwrap();
2171 let b = stage_audio(&dir, "wav", b"RIFF").unwrap();
2172 assert_ne!(a, b);
2173 assert_eq!(std::fs::read(&a).unwrap(), b"RIFF");
2174 let mode = std::fs::metadata(&a).unwrap().permissions().mode() & 0o777;
2175 assert_eq!(mode, 0o600, "{mode:o}");
2176 let name = a.file_name().unwrap().to_string_lossy().into_owned();
2177 assert!(!name.contains(&std::process::id().to_string()), "{name}: no pid in the name");
2178 let _ = std::fs::remove_dir_all(dir);
2179 }
2180}