Skip to main content

superfluid_daemon/
openai.rs

1//! api-openai.
2
3use 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    /// Where answers recorded under an `Idempotency-Key` are kept across restarts; in memory
44    /// alone when none.
45    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/// What the server calls itself: `owned_by` in the model list, and `server`
69/// and `build.version` in /props. A program embedding superfluid may give
70/// its own name and version.
71#[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/// How a request's `model` picks the model that serves it.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
85pub enum ModelNaming {
86    /// As the OpenAI API has it: every request names its model, exactly.
87    #[default]
88    Exact,
89    /// A request that names no model gets the default one; a name that is no
90    /// model's matches one by case, then as part of its name.
91    Loose,
92}
93
94/// The APIs served beside the OpenAI-compatible routes. `superfluid serve`
95/// serves all of them; a program embedding it may serve fewer.
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub struct HttpSurface {
98    /// Anthropic's Messages API, `/v1/messages`.
99    pub anthropic: bool,
100    /// Ollama's API, `/api/*` and `/`.
101    pub ollama: bool,
102}
103
104impl HttpSurface {
105    pub const OPENAI_ONLY: HttpSurface = HttpSurface { anthropic: false, ollama: false };
106
107    /// What the startup line says is served besides the OpenAI API.
108    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/// Which `Host` headers a listener answers to. A DNS-rebound page sends its own domain name as
423/// `Host` and cannot present an API key, so a keyless server off loopback still refuses names
424/// that are not this machine's.
425#[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    // SAFETY: buf is writable for its full length; gethostname NUL-terminates within it or fails.
447    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
472/// Why a request that a web page could have sent is refused, if it is. A browser names the page
473/// in `Origin`; one that is not served from this machine may not drive the API, keyed or not.
474/// The `Host` check is what stops a DNS-rebound page, which sends no `Origin` on a GET.
475pub(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(&registry);
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, &params, 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
1158/// Writes an upload to a fresh owner-only file under `dir`. The name is random and the open is
1159/// exclusive, so a file or symlink planted in a shared temp dir is never written through.
1160fn 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, &params, 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
1797/// On a fleet head, 503 while no node that is up serves any of its models, so a load balancer
1798/// sends the head nothing it could not place; each model's nodes are counted under `models`.
1799async 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}