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, 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/// What the server calls itself: `owned_by` in the model list, and `server`
65/// and `build.version` in /props. A program embedding superfluid may give
66/// its own name and version.
67#[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/// How a request's `model` picks the model that serves it.
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
81pub enum ModelNaming {
82    /// As the OpenAI API has it: every request names its model, exactly.
83    #[default]
84    Exact,
85    /// A request that names no model gets the default one; a name that is no
86    /// model's matches one by case, then as part of its name.
87    Loose,
88}
89
90/// The APIs served beside the OpenAI-compatible routes. `superfluid serve`
91/// serves all of them; a program embedding it may serve fewer.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub struct HttpSurface {
94    /// Anthropic's Messages API, `/v1/messages`.
95    pub anthropic: bool,
96    /// Ollama's API, `/api/*` and `/`.
97    pub ollama: bool,
98}
99
100impl HttpSurface {
101    pub const OPENAI_ONLY: HttpSurface = HttpSurface { anthropic: false, ollama: false };
102
103    /// What the startup line says is served besides the OpenAI API.
104    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/// Which `Host` headers a listener answers to. A DNS-rebound page sends its own domain name as
419/// `Host` and cannot present an API key, so a keyless server off loopback still refuses names
420/// that are not this machine's.
421#[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    // SAFETY: buf is writable for its full length; gethostname NUL-terminates within it or fails.
443    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
468/// Why a request that a web page could have sent is refused, if it is. A browser names the page
469/// in `Origin`; one that is not served from this machine may not drive the API, keyed or not.
470/// The `Host` check is what stops a DNS-rebound page, which sends no `Origin` on a GET.
471pub(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(&registry);
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, &params, 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
1152/// Writes an upload to a fresh owner-only file under `dir`. The name is random and the open is
1153/// exclusive, so a file or symlink planted in a shared temp dir is never written through.
1154fn 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, &params, 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}