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