1use axum::http::{
2 HeaderMap, HeaderName, HeaderValue, Method,
3 header::{
4 ACCEPT, ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS,
5 ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_EXPOSE_HEADERS, ACCESS_CONTROL_REQUEST_METHOD,
6 CONTENT_TYPE, ORIGIN, VARY, X_CONTENT_TYPE_OPTIONS, X_FRAME_OPTIONS,
7 },
8};
9use axum::{
10 Router,
11 body::Bytes,
12 extract::{DefaultBodyLimit, FromRequest, Path, Request, State},
13 http::StatusCode,
14 middleware::{self, Next},
15 response::{IntoResponse, Json, Response},
16 routing::{MethodRouter, get, post},
17};
18use serde::de::{self, Deserializer, IgnoredAny, MapAccess, Visitor};
19use serde::{Deserialize, Serialize};
20use std::fmt;
21use std::sync::Arc;
22use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
23use std::time::{Duration, Instant, SystemTime};
24use tokio::signal;
25use tokio::sync::Semaphore;
26use tracing::{Instrument, debug, error, info, info_span, warn};
27
28#[cfg(feature = "grpc")]
29pub mod grpc;
30mod infer;
31mod v2;
32
33use infer::{Code, f32s_from_le, f32s_to_le};
34
35use crate::access::{Access, ClientRateLimit, Rejection, constant_time_eq, presented_key};
36pub use crate::batching::BatchConfig;
37use crate::error::{ApiError, FurnaceError, ModelError, Result};
38use crate::model::{MAX_BATCH_SIZE, Model, ModelInfo, ModelStats};
39use crate::nerve::Announced;
40use crate::nerve::serve::{Refused, ServedNeuron, Serving};
41#[cfg(feature = "image")]
42use crate::preprocess::{ImageKind, image_input_size, preprocess_image};
43use crate::tensor::{HostTensor, Values};
44use kuebiko::{Histogram, Percentiles};
45
46pub const OCTET_STREAM: &str = "application/octet-stream";
48pub const BATCH_SIZE_HEADER: HeaderName = HeaderName::from_static("x-batch-size");
51pub const OUTPUT_SHAPE_HEADER: HeaderName = HeaderName::from_static("x-output-shape");
53pub const INFERENCE_TIME_HEADER: HeaderName = HeaderName::from_static("x-inference-time-ms");
55pub const REQUEST_ID_HEADER: HeaderName = HeaderName::from_static("x-request-id");
58
59#[derive(Debug, Serialize)]
60pub struct PredictRequest {
61 #[serde(flatten)]
62 pub input_data: PredictInputData,
63}
64
65#[derive(Debug, Deserialize, Serialize)]
66#[serde(untagged)]
67pub enum PredictInputData {
68 Single { input: Vec<f32> },
69 Batch { inputs: Vec<Vec<f32>> },
70}
71
72impl<'de> Deserialize<'de> for PredictRequest {
77 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
78 #[derive(Deserialize)]
79 #[serde(field_identifier, rename_all = "lowercase")]
80 enum Field {
81 Input,
82 Inputs,
83 #[serde(other)]
84 Other,
85 }
86
87 struct RequestVisitor;
88
89 impl<'de> Visitor<'de> for RequestVisitor {
90 type Value = PredictRequest;
91
92 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
93 f.write_str("an object with an `input` or `inputs` field")
94 }
95
96 fn visit_map<A: MapAccess<'de>>(
97 self,
98 mut map: A,
99 ) -> std::result::Result<PredictRequest, A::Error> {
100 let mut input_data = None;
101 while let Some(field) = map.next_key()? {
102 let value = match field {
103 Field::Input => PredictInputData::Single {
104 input: map.next_value()?,
105 },
106 Field::Inputs => PredictInputData::Batch {
107 inputs: map.next_value()?,
108 },
109 Field::Other => {
110 map.next_value::<IgnoredAny>()?;
111 continue;
112 }
113 };
114 if input_data.replace(value).is_some() {
115 return Err(de::Error::custom(
116 "expected exactly one of `input` or `inputs`",
117 ));
118 }
119 }
120 input_data
121 .map(|input_data| PredictRequest { input_data })
122 .ok_or_else(|| de::Error::custom("missing field `input` or `inputs`"))
123 }
124 }
125
126 deserializer.deserialize_map(RequestVisitor)
127 }
128}
129
130struct PredictInput {
132 data: Vec<f32>,
133 batch_size: usize,
134 single: bool,
136}
137
138#[derive(Debug, Serialize, Deserialize)]
139pub struct PredictResponse {
140 #[serde(flatten)]
141 pub output_data: PredictOutputData,
142 pub status: String,
143 pub inference_time_ms: f64,
144 pub timestamp: String,
145 pub batch_size: usize,
146}
147
148#[derive(Debug, Serialize, Deserialize)]
149#[serde(untagged)]
150pub enum PredictOutputData {
151 Single { output: Vec<f32> },
152 Batch { outputs: Vec<Vec<f32>> },
153}
154
155#[derive(Serialize)]
158struct PredictReply<'a> {
159 #[serde(skip_serializing_if = "Option::is_none")]
160 output: Option<&'a [f32]>,
161 #[serde(skip_serializing_if = "Option::is_none")]
162 outputs: Option<Rows<'a>>,
163 status: &'static str,
164 inference_time_ms: f64,
165 timestamp: String,
166 batch_size: usize,
167}
168
169struct Rows<'a> {
171 values: &'a [f32],
172 rows: usize,
173}
174
175impl Serialize for Rows<'_> {
176 fn serialize<S: serde::Serializer>(
177 &self,
178 serializer: S,
179 ) -> std::result::Result<S::Ok, S::Error> {
180 let width = self.values.len() / self.rows.max(1);
181 serializer.collect_seq(self.values.chunks_exact(width.max(1)).take(self.rows))
182 }
183}
184
185#[derive(Debug, Serialize, Deserialize)]
187pub struct MetricsResponse {
188 pub uptime_seconds: u64,
189 pub total_requests: u64,
191 pub total_errors: u64,
193 pub rejected_busy: u64,
195 pub rejected_unauthorized: u64,
197 pub rejected_rate_limited: u64,
199 pub in_flight_inferences: usize,
201 pub max_concurrent_requests: usize,
203 pub total_inferences: u64,
205 pub total_batches: u64,
207 pub average_batch_size: f64,
209 pub max_batch_size: usize,
211 pub average_inference_time_ms: f64,
212 pub last_inference_at: Option<String>,
214 pub request_latency_ms: Percentiles,
216 pub inference_latency_ms: Percentiles,
218 pub ready: bool,
220 pub models: Vec<ModelMetrics>,
222 #[cfg(feature = "llm")]
224 #[serde(default, skip_serializing_if = "Vec::is_empty")]
225 pub llm: Vec<crate::llm::LlmMetrics>,
226 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub nerve: Option<crate::nerve::NerveMetrics>,
230 pub timestamp: String,
231}
232
233#[derive(Debug, Serialize, Deserialize)]
235pub struct ModelMetrics {
236 pub name: String,
237 pub ready: bool,
238 pub inference_count: u64,
239 pub error_count: u64,
240 pub batch_count: u64,
241 pub average_batch_size: f64,
242 pub average_inference_time_ms: f64,
243 pub inference_latency_ms: Percentiles,
244}
245
246#[derive(Debug, Serialize, Deserialize)]
248pub struct ModelsResponse {
249 pub default_model: String,
251 pub models: Vec<ModelSummary>,
252}
253
254#[derive(Debug, Serialize, Deserialize)]
255pub struct ModelSummary {
256 pub name: String,
257 pub ready: bool,
258 pub input_shape: Vec<usize>,
259 pub output_shape: Vec<usize>,
260 pub backend: String,
261 pub reloadable: bool,
263}
264
265#[derive(Debug, Serialize, Deserialize)]
267pub struct ReloadResponse {
268 pub model: String,
269 pub weights: Option<String>,
271 pub reload_ms: f64,
273 pub warm_up_ms: f64,
274 pub timestamp: String,
275}
276
277#[derive(Debug, Serialize, Deserialize)]
279pub struct ReadinessResponse {
280 pub status: String,
282 pub timestamp: String,
283}
284
285#[derive(Debug, Serialize, Deserialize)]
286pub struct HealthResponse {
287 pub status: String,
288 pub model_loaded: bool,
289 pub backend: String,
290 pub uptime_seconds: u64,
291 pub timestamp: String,
292}
293
294#[derive(Debug, Serialize, Deserialize)]
295pub struct ModelInfoResponse {
296 pub model_info: ModelInfo,
297 pub stats: ModelStats,
298 pub timestamp: String,
299}
300
301pub use crate::error::ErrorResponse;
302
303#[derive(Debug, Clone)]
304pub struct ServerConfig {
305 pub host: String,
306 pub port: u16,
307 pub max_concurrent_requests: usize,
309 pub max_body_bytes: usize,
311 pub request_timeout: Option<Duration>,
313 pub batching: BatchConfig,
315 pub grpc_port: Option<u16>,
317 pub admin_token: Option<String>,
320 pub api_keys: Vec<String>,
323 pub rate_limit: Option<ClientRateLimit>,
325 pub tls: Option<TlsSettings>,
327}
328
329#[derive(Debug, Clone, PartialEq)]
331pub struct TlsSettings {
332 pub cert: std::path::PathBuf,
334 pub key: std::path::PathBuf,
336 pub client_ca: Option<std::path::PathBuf>,
338}
339
340#[derive(Debug, Clone, Copy)]
342pub struct ClientAddr(pub std::net::SocketAddr);
343
344impl
345 axum::extract::connect_info::Connected<axum::serve::IncomingStream<'_, tokio::net::TcpListener>>
346 for ClientAddr
347{
348 fn connect_info(stream: axum::serve::IncomingStream<'_, tokio::net::TcpListener>) -> Self {
349 Self(*stream.remote_addr())
350 }
351}
352
353#[cfg(feature = "tls")]
354impl
355 axum::extract::connect_info::Connected<axum::serve::IncomingStream<'_, crate::tls::TlsListener>>
356 for ClientAddr
357{
358 fn connect_info(stream: axum::serve::IncomingStream<'_, crate::tls::TlsListener>) -> Self {
359 Self(*stream.remote_addr())
360 }
361}
362
363impl Default for ServerConfig {
364 fn default() -> Self {
365 Self {
366 host: "127.0.0.1".to_string(),
367 port: 3000,
368 max_concurrent_requests: 100,
369 max_body_bytes: 100 * 1024 * 1024,
371 request_timeout: None,
372 batching: BatchConfig::default(),
373 grpc_port: None,
374 admin_token: None,
375 api_keys: Vec::new(),
376 rate_limit: None,
377 tls: None,
378 }
379 }
380}
381
382pub(crate) type AppState = Arc<ServerState>;
383
384pub(crate) type Reloader = Arc<dyn Fn(Option<&std::path::Path>) -> Result<Model> + Send + Sync>;
387
388pub(crate) struct ServedModel {
390 pub(crate) model: Model,
391 pub(crate) reload: Option<Reloader>,
392}
393
394impl From<Model> for ServedModel {
395 fn from(model: Model) -> Self {
396 Self {
397 model,
398 reload: None,
399 }
400 }
401}
402
403pub struct Server {
417 config: ServerConfig,
418 contents: Contents,
419}
420
421#[derive(Default)]
423struct Contents {
424 models: Vec<ServedModel>,
425 #[cfg(feature = "llm")]
426 llms: Vec<Arc<dyn crate::llm::LlmEngine>>,
427 nerve: Option<crate::nerve::FabricBuilder>,
428}
429
430impl Contents {
431 fn is_empty(&self) -> bool {
432 #[cfg(feature = "llm")]
433 if !self.llms.is_empty() {
434 return false;
435 }
436 if self.nerve.is_some() {
437 return false;
438 }
439 self.models.is_empty()
440 }
441}
442
443impl From<Vec<ServedModel>> for Contents {
444 fn from(models: Vec<ServedModel>) -> Self {
445 Self {
446 models,
447 #[cfg(feature = "llm")]
448 llms: Vec::new(),
449 nerve: None,
450 }
451 }
452}
453
454impl Default for Server {
455 fn default() -> Self {
456 Self::new()
457 }
458}
459
460impl Server {
461 pub fn new() -> Self {
464 Self::with_config(ServerConfig::default())
465 }
466
467 pub fn with_config(config: ServerConfig) -> Self {
468 Self {
469 config,
470 contents: Contents::default(),
471 }
472 }
473
474 pub fn host(mut self, host: &str) -> Self {
475 self.config.host = host.to_string();
476 self
477 }
478
479 pub fn port(mut self, port: u16) -> Self {
480 self.config.port = port;
481 self
482 }
483
484 pub fn max_batch_size(mut self, items: usize) -> Self {
487 self.config.batching.max_batch_size = items;
488 self
489 }
490
491 pub fn request_timeout(mut self, timeout: Duration) -> Self {
494 self.config.request_timeout = Some(timeout);
495 self
496 }
497
498 pub fn configure(mut self, change: impl FnOnce(&mut ServerConfig)) -> Self {
500 change(&mut self.config);
501 self
502 }
503
504 pub fn model(mut self, model: Model) -> Self {
506 self.contents.models.push(model.into());
507 self
508 }
509
510 #[cfg(feature = "llm")]
516 pub fn llm(mut self, engine: impl crate::llm::LlmEngine) -> Self {
517 self.contents.llms.push(Arc::new(engine));
518 self
519 }
520
521 pub fn nerve(mut self, fabric: crate::nerve::FabricBuilder) -> Self {
530 self.contents.nerve = Some(fabric);
531 self
532 }
533
534 pub fn reloadable_model(
537 mut self,
538 model: Model,
539 reload: impl Fn(Option<&std::path::Path>) -> Result<Model> + Send + Sync + 'static,
540 ) -> Self {
541 self.contents.models.push(ServedModel {
542 model,
543 reload: Some(Arc::new(reload)),
544 });
545 self
546 }
547
548 pub fn router(self) -> Result<Router> {
566 check_contents(&self.contents)?;
567 warn_if_limited_by_address(&self.config);
568 Ok(router_with_state(
569 server_state(self.contents, &self.config)?,
570 &self.config,
571 ))
572 }
573
574 pub async fn serve(self) -> Result<()> {
576 serve(self.config, self.contents).await
577 }
578}
579
580fn check_contents(contents: &Contents) -> Result<()> {
582 if contents.is_empty() {
583 return Err(ApiError::ServerStartup(
584 "nothing to serve; add a model with Server::model, an LLM with Server::llm \
585 (feature `llm`) or neurons with Server::nerve"
586 .to_string(),
587 )
588 .into());
589 }
590 Ok(())
591}
592
593struct Loaded {
596 model: Arc<Model>,
597 ready: Arc<AtomicBool>,
599}
600
601impl Loaded {
602 fn start(model: Model) -> Arc<Self> {
604 let loaded = Self::new(model);
605 let model = loaded.model.clone();
606 let ready = loaded.ready.clone();
607 std::thread::spawn(move || {
608 match model.warm_up() {
609 Ok(Some(elapsed)) => info!(
610 "🔥 Warm-up inference of {} took {:.1}ms",
611 model.get_info().name,
612 elapsed.as_secs_f64() * 1e3
613 ),
614 Ok(None) => info!(
615 "No warm-up for {}: an input's shape is only fixed per request",
616 model.get_info().name
617 ),
618 Err(e) => warn!("Warm-up inference failed: {e}"),
619 }
620 ready.store(true, Ordering::Release);
621 });
622 Arc::new(loaded)
623 }
624
625 fn new(model: Model) -> Self {
626 Self {
627 model: Arc::new(model),
628 ready: Arc::new(AtomicBool::new(false)),
629 }
630 }
631
632 fn ready(&self) -> bool {
633 self.ready.load(Ordering::Acquire)
634 }
635}
636
637struct ModelSlot {
640 name: String,
641 current: std::sync::RwLock<Arc<Loaded>>,
642 reload: Option<Reloader>,
643 reloading: tokio::sync::Mutex<()>,
645 inference_latency: Histogram,
647}
648
649impl ModelSlot {
650 fn get(&self) -> Arc<Loaded> {
651 self.current
652 .read()
653 .unwrap_or_else(|e| e.into_inner())
654 .clone()
655 }
656
657 fn replace(&self, loaded: Arc<Loaded>) {
658 *self.current.write().unwrap_or_else(|e| e.into_inner()) = loaded;
659 }
660}
661
662pub(crate) struct ServerState {
663 models: Vec<Arc<ModelSlot>>,
665 start_time: SystemTime,
666 inference_slots: Arc<Semaphore>,
667 max_concurrent_requests: usize,
670 request_timeout: Option<Duration>,
671 max_batch_size: usize,
672 admin_token: Option<String>,
673 access: Access,
674 total_requests: AtomicU64,
675 total_errors: AtomicU64,
676 rejected_busy: AtomicU64,
677 rejected_unauthorized: AtomicU64,
678 rejected_rate_limited: AtomicU64,
679 request_latency: Histogram,
681 inference_latency: Histogram,
683 #[cfg(feature = "llm")]
685 llm: Option<Arc<crate::llm::LlmState>>,
686 serving: Arc<Serving>,
688}
689
690impl ServerState {
691 fn uptime_seconds(&self) -> u64 {
692 self.start_time.elapsed().unwrap_or_default().as_secs()
693 }
694
695 fn default_slot(&self) -> Option<&Arc<ModelSlot>> {
697 self.models.first()
698 }
699
700 fn slot(&self, name: &str) -> Option<&Arc<ModelSlot>> {
701 self.models.iter().find(|slot| slot.name == name)
702 }
703
704 fn ready(&self) -> bool {
706 self.models.iter().all(|slot| slot.get().ready())
707 }
708
709 fn model_names(&self) -> Vec<&str> {
710 self.models.iter().map(|slot| slot.name.as_str()).collect()
711 }
712}
713
714fn warn_if_limited_by_address(config: &ServerConfig) {
717 if config.rate_limit.is_some() && config.api_keys.is_empty() {
718 warn!(
719 "the rate limit tells clients apart by address: serve this router with \
720 into_make_service_with_connect_info::<furnace::api::ClientAddr>(), or every \
721 client shares one budget"
722 );
723 }
724}
725
726fn server_state(contents: Contents, config: &ServerConfig) -> Result<AppState> {
728 let batching = config.batching;
729 let inference_slots = Arc::new(Semaphore::new(config.max_concurrent_requests));
730 let models = contents
731 .models
732 .into_iter()
733 .map(|served| {
734 Arc::new(ModelSlot {
735 name: served.model.get_info().name.clone(),
736 current: std::sync::RwLock::new(Loaded::start(served.model)),
737 reload: served.reload,
738 reloading: tokio::sync::Mutex::new(()),
739 inference_latency: Histogram::new(),
740 })
741 })
742 .collect::<Vec<Arc<ModelSlot>>>();
743 let serving = Serving::build(
744 contents.nerve.unwrap_or_default(),
745 models
746 .iter()
747 .map(|slot| {
748 let model = slot.get().model.clone();
749 let current = slot.clone();
750 ServedNeuron {
751 name: slot.name.clone(),
752 batches: model.is_simple() || model.batches_named(),
753 source: Arc::new(move || current.get().model.clone()),
754 }
755 })
756 .collect(),
757 #[cfg(feature = "llm")]
758 &contents.llms,
759 batching,
760 config.max_concurrent_requests,
761 )?;
762 let serving = Arc::new(serving);
763 #[cfg(feature = "llm")]
764 let llm = (!contents.llms.is_empty()).then(|| {
765 let others = models.iter().map(|slot| slot.name.clone()).collect();
766 Arc::new(crate::llm::LlmState::new(
767 contents.llms,
768 others,
769 inference_slots.clone(),
770 serving.clone(),
771 config.request_timeout,
772 ))
773 });
774 Ok(Arc::new(ServerState {
775 models,
776 start_time: SystemTime::now(),
777 inference_slots,
778 max_concurrent_requests: config.max_concurrent_requests,
779 request_timeout: config.request_timeout,
780 max_batch_size: batching.max_batch_size.max(1),
781 admin_token: config.admin_token.clone(),
782 access: Access::new(&config.api_keys, config.rate_limit),
783 total_requests: AtomicU64::new(0),
784 total_errors: AtomicU64::new(0),
785 rejected_busy: AtomicU64::new(0),
786 rejected_unauthorized: AtomicU64::new(0),
787 rejected_rate_limited: AtomicU64::new(0),
788 request_latency: Histogram::new(),
789 inference_latency: Histogram::new(),
790 #[cfg(feature = "llm")]
791 llm,
792 serving,
793 }))
794}
795
796type MakeRoute = fn(&AppState) -> MethodRouter<AppState>;
798
799fn counted(state: &AppState, route: MethodRouter<AppState>) -> MethodRouter<AppState> {
801 route.layer(middleware::from_fn_with_state(
802 state.clone(),
803 count_requests,
804 ))
805}
806
807const ROUTES: &[(&str, MakeRoute)] = &[
810 ("/healthz", |_| get(health_check)),
811 ("/health", |_| get(health_check)),
812 ("/readyz", |_| get(readiness)),
813 ("/predict", |state| counted(state, post(predict))),
814 ("/models/{model}/predict", |state| {
815 counted(state, post(predict_named))
816 }),
817 ("/model/info", |_| get(model_info)),
818 ("/models", |_| get(list_models)),
819 ("/models/{model}", |_| get(model_info_named)),
820 ("/admin/models/{model}/reload", |_| post(reload_model)),
821 ("/metrics", |_| get(metrics)),
822 ("/v2", |_| get(v2::server_metadata)),
823 ("/v2/health/live", |_| get(v2::live)),
824 ("/v2/health/ready", |_| get(v2::ready)),
825 ("/v2/models/{model}", |_| get(v2::model_metadata)),
826 ("/v2/models/{model}/versions/{version}", |_| {
827 get(v2::model_metadata)
828 }),
829 ("/v2/models/{model}/ready", |_| get(v2::model_ready)),
830 ("/v2/models/{model}/versions/{version}/ready", |_| {
831 get(v2::model_ready)
832 }),
833 ("/v2/models/{model}/infer", |state| {
834 counted(state, post(v2::infer))
835 }),
836 ("/v2/models/{model}/versions/{version}/infer", |state| {
837 counted(state, post(v2::infer))
838 }),
839];
840
841#[cfg(any(test, feature = "llm"))]
843pub(crate) const LLM_PATHS: [&str; 3] = ["/v1/chat/completions", "/v1/completions", "/v1/models"];
844
845#[cfg(test)]
847const NERVE_PATHS: [&str; 2] = ["/nerve", "/nerve/effectors/{name}/events"];
848
849fn router_with_state(state: AppState, config: &ServerConfig) -> Router {
850 let router = ROUTES.iter().fold(Router::new(), |router, (path, make)| {
851 router.route(path, make(&state))
852 });
853 #[cfg(feature = "llm")]
854 let router = match &state.llm {
855 Some(llm) => router.merge(crate::llm::routes(llm.clone())),
856 None => router,
857 };
858 let router = router.merge(crate::nerve::http::routes(state.serving.fabric().clone()));
859 let router = if state.access.is_open() {
861 router
862 } else {
863 router.layer(middleware::from_fn_with_state(state.clone(), guard))
864 };
865 router
866 .layer(DefaultBodyLimit::max(config.max_body_bytes))
867 .layer(middleware::from_fn(edge))
868 .with_state(state)
869}
870
871const PUBLIC_PATHS: &[&str] = &[
873 "/healthz",
874 "/health",
875 "/readyz",
876 "/v2/health/live",
877 "/v2/health/ready",
878];
879
880async fn guard(State(state): State<AppState>, request: Request, next: Next) -> Response {
882 let path = request.uri().path();
883 if PUBLIC_PATHS.contains(&path) {
884 return next.run(request).await;
885 }
886 let verdict = {
887 let header = |name: &str| request.headers().get(name).and_then(|v| v.to_str().ok());
888 let key = presented_key(header("authorization"), header(API_KEY_HEADER));
889 let ip = request
890 .extensions()
891 .get::<axum::extract::ConnectInfo<ClientAddr>>()
892 .map(|info| info.0.0.ip());
893 state.access.admit(key, ip, !path.starts_with("/admin/"))
894 };
895 match verdict {
896 Ok(()) => next.run(request).await,
897 Err(rejection) => rejection_response(&state, &rejection),
898 }
899}
900
901const API_KEY_HEADER: &str = "x-api-key";
903
904fn rejection_response(state: &ServerState, rejection: &Rejection) -> Response {
905 match rejection {
906 Rejection::Unauthorized => {
907 state.rejected_unauthorized.fetch_add(1, Ordering::Relaxed);
908 let mut response = create_error_response(
909 StatusCode::UNAUTHORIZED,
910 Code::Unauthorized,
911 "Missing or unknown API key; send 'Authorization: Bearer <key>' or 'X-API-Key: <key>'",
912 None,
913 );
914 response.headers_mut().insert(
915 axum::http::header::WWW_AUTHENTICATE,
916 HeaderValue::from_static("Bearer"),
917 );
918 response
919 }
920 Rejection::RateLimited(retry) => {
921 state.rejected_rate_limited.fetch_add(1, Ordering::Relaxed);
922 let seconds = retry.as_secs() + u64::from(retry.subsec_nanos() > 0);
923 let mut response = create_error_response(
924 StatusCode::TOO_MANY_REQUESTS,
925 Code::RateLimited,
926 &format!(
927 "Rate limit exceeded; retry in {} ms",
928 retry.as_millis().max(1)
929 ),
930 Some(serde_json::json!({ "retry_after_ms": retry.as_millis() })),
931 );
932 response
933 .headers_mut()
934 .insert(axum::http::header::RETRY_AFTER, HeaderValue::from(seconds));
935 response
936 }
937 }
938}
939
940const EXPOSED_HEADERS: &str = "x-batch-size, x-output-shape, x-inference-time-ms, x-request-id";
942
943async fn edge(request: Request, next: Next) -> Response {
947 let id = request
948 .headers()
949 .get(&REQUEST_ID_HEADER)
950 .filter(|id| {
951 let id = id.as_bytes();
952 !id.is_empty() && id.len() <= 128 && id.iter().all(u8::is_ascii_graphic)
953 })
954 .cloned()
955 .unwrap_or_else(new_request_id);
956 let preflight = request.method() == Method::OPTIONS
957 && request.headers().contains_key(ORIGIN)
958 && request
959 .headers()
960 .contains_key(ACCESS_CONTROL_REQUEST_METHOD);
961 let mut response = if preflight {
962 let mut response = StatusCode::OK.into_response();
963 let headers = response.headers_mut();
964 let any = HeaderValue::from_static("*");
965 headers.insert(ACCESS_CONTROL_ALLOW_METHODS, any.clone());
966 headers.insert(ACCESS_CONTROL_ALLOW_HEADERS, any);
967 headers.insert(
968 VARY,
969 HeaderValue::from_static(
970 "origin, access-control-request-method, access-control-request-headers",
971 ),
972 );
973 response
974 } else {
975 let span = info_span!(
978 "request",
979 id = id.to_str().unwrap_or_default(),
980 method = request.method().as_str(),
981 path = request.uri().path()
982 );
983 next.run(request).instrument(span).await
984 };
985 let headers = response.headers_mut();
986 headers.insert(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*"));
987 headers.insert(
988 ACCESS_CONTROL_EXPOSE_HEADERS,
989 HeaderValue::from_static(EXPOSED_HEADERS),
990 );
991 headers.insert(X_CONTENT_TYPE_OPTIONS, HeaderValue::from_static("nosniff"));
992 headers.insert(X_FRAME_OPTIONS, HeaderValue::from_static("DENY"));
993 headers.insert(REQUEST_ID_HEADER, id);
994 response
995}
996
997fn new_request_id() -> HeaderValue {
1000 static SEED: std::sync::LazyLock<u128> = std::sync::LazyLock::new(|| {
1001 (u128::from(process_random(0)) << 64) | u128::from(process_random(1))
1002 });
1003 static NEXT: AtomicU64 = AtomicU64::new(0);
1004 let bits = *SEED ^ u128::from(NEXT.fetch_add(1, Ordering::Relaxed));
1005 let mut bytes = bits.to_be_bytes();
1006 bytes[6] = (bytes[6] & 0x0f) | 0x40;
1008 bytes[8] = (bytes[8] & 0x3f) | 0x80;
1009 let hex: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
1010 let id = format!(
1011 "{}-{}-{}-{}-{}",
1012 &hex[..8],
1013 &hex[8..12],
1014 &hex[12..16],
1015 &hex[16..20],
1016 &hex[20..]
1017 );
1018 HeaderValue::from_str(&id).expect("a UUID is a valid header value")
1019}
1020
1021pub(crate) fn process_random(stream: u64) -> u64 {
1025 use std::hash::{BuildHasher, RandomState};
1026 static KEYS: std::sync::LazyLock<RandomState> = std::sync::LazyLock::new(RandomState::new);
1027 KEYS.hash_one(stream)
1028}
1029
1030async fn serve(config: ServerConfig, contents: Contents) -> Result<()> {
1031 check_contents(&contents)?;
1032 let state = server_state(contents, &config)?;
1033 #[cfg(not(feature = "llm"))]
1034 let names = state.model_names();
1035 #[cfg(feature = "llm")]
1036 let names: Vec<&str> = state
1037 .model_names()
1038 .into_iter()
1039 .chain(state.llm.iter().flat_map(|llm| llm.names()))
1040 .collect();
1041 info!("📚 Serving {}", names.join(", "));
1042 let app = router_with_state(state.clone(), &config);
1043 info!(
1044 "🚦 Concurrency limit set to {} inferences",
1045 config.max_concurrent_requests
1046 );
1047 if config.batching.enabled() {
1048 info!(
1049 "📦 Dynamic batching: up to {} items per batch, waiting at most {}µs, {} worker(s)",
1050 config.batching.max_batch_size,
1051 config.batching.batch_timeout.as_micros(),
1052 config.batching.workers
1053 );
1054 } else {
1055 info!("📦 Dynamic batching off");
1056 }
1057
1058 #[cfg(feature = "tls")]
1059 let tls = match &config.tls {
1060 Some(settings) => {
1061 let configs = crate::tls::TlsConfigs::load(settings.clone())
1062 .map_err(|e| ApiError::ServerStartup(format!("TLS: {e}")))?;
1063 configs.watch();
1064 info!("🔐 TLS 1.3 with {}", crate::tls::describe(settings));
1065 Some(configs)
1066 }
1067 None => None,
1068 };
1069 #[cfg(not(feature = "tls"))]
1070 if config.tls.is_some() {
1071 return Err(ApiError::ServerStartup(missing_feature(
1072 "TLS (--tls-cert, ServerConfig::tls)",
1073 "tls",
1074 ))
1075 .into());
1076 }
1077
1078 let bind_addr = format!("{}:{}", config.host, config.port);
1079 let listener = tokio::net::TcpListener::bind(&bind_addr)
1080 .await
1081 .map_err(|e| {
1082 ApiError::ServerStartup(bind_error(&bind_addr, &e, "--port / Server::port"))
1083 })?;
1084
1085 let scheme = if config.tls.is_some() {
1086 "https"
1087 } else {
1088 "http"
1089 };
1090 info!("✅ Server running on {scheme}://{bind_addr}");
1091
1092 let (stop, stopped) = tokio::sync::watch::channel(false);
1094 let wait_for_stop = move |mut stopped: tokio::sync::watch::Receiver<bool>| async move {
1095 let _ = stopped.wait_for(|&stop| stop).await;
1096 };
1097 tokio::spawn(async move {
1098 shutdown_signal().await;
1099 let _ = stop.send(true);
1100 });
1101
1102 #[cfg(feature = "grpc")]
1103 let grpc = config.grpc_port.map(|port| {
1104 let addr = format!("{}:{port}", config.host);
1105 let state = state.clone();
1106 let max_message_bytes = config.max_body_bytes;
1107 let shutdown = wait_for_stop(stopped.clone());
1108 #[cfg(feature = "tls")]
1109 let tls = tls.clone();
1110 #[cfg(not(feature = "tls"))]
1111 let tls = None;
1112 tokio::spawn(async move {
1113 let addr = tokio::net::lookup_host(&addr)
1114 .await
1115 .ok()
1116 .and_then(|mut addrs| addrs.next())
1117 .ok_or_else(|| format!("cannot resolve {addr}"))?;
1118 grpc::serve(state, addr, max_message_bytes, tls, shutdown).await
1119 })
1120 });
1121 #[cfg(not(feature = "grpc"))]
1122 if config.grpc_port.is_some() {
1123 return Err(ApiError::ServerStartup(missing_feature(
1124 "gRPC (--grpc-port, ServerConfig::grpc_port)",
1125 "grpc",
1126 ))
1127 .into());
1128 }
1129
1130 if !config.api_keys.is_empty() {
1131 info!(
1132 "🔑 API keys required ({} configured)",
1133 config.api_keys.len()
1134 );
1135 }
1136 if let Some(limit) = config.rate_limit {
1137 info!(
1138 "⏱️ Rate limit: {} requests/s per client, bursts of {}",
1139 limit.per_second, limit.burst
1140 );
1141 }
1142
1143 let make_service = app.into_make_service_with_connect_info::<ClientAddr>();
1144 #[cfg(feature = "tls")]
1145 let served = match tls {
1146 Some(configs) => {
1147 let listener = crate::tls::TlsListener::new(listener, configs)
1148 .map_err(|e| ApiError::ServerStartup(format!("TLS listener: {e}")))?;
1149 axum::serve(listener, make_service)
1150 .with_graceful_shutdown(wait_for_stop(stopped))
1151 .await
1152 }
1153 None => {
1154 axum::serve(listener, make_service)
1155 .with_graceful_shutdown(wait_for_stop(stopped))
1156 .await
1157 }
1158 };
1159 #[cfg(not(feature = "tls"))]
1160 let served = axum::serve(listener, make_service)
1161 .with_graceful_shutdown(wait_for_stop(stopped))
1162 .await;
1163 served.map_err(|e| ApiError::ServerStartup(format!("Server error: {e}")))?;
1164
1165 #[cfg(feature = "grpc")]
1166 if let Some(grpc) = grpc {
1167 match grpc.await {
1168 Ok(Ok(())) => {}
1169 Ok(Err(e)) => return Err(ApiError::ServerStartup(e).into()),
1170 Err(e) => return Err(ApiError::ServerStartup(format!("gRPC task failed: {e}")).into()),
1171 }
1172 }
1173
1174 info!("🛑 Server stopped gracefully");
1175 Ok(())
1176}
1177
1178pub(crate) fn bind_error(addr: &str, e: &std::io::Error, setting: &str) -> String {
1180 if e.kind() == std::io::ErrorKind::AddrInUse {
1181 format!(
1182 "cannot listen on {addr}: the port is already in use; choose another with {setting}, \
1183 or stop the process that uses it"
1184 )
1185 } else {
1186 format!("cannot listen on {addr}: {e}")
1187 }
1188}
1189
1190#[cfg(any(not(feature = "tls"), not(feature = "grpc")))]
1192fn missing_feature(setting: &str, feature: &str) -> String {
1193 format!(
1194 "{setting} needs the `{feature}` feature: rebuild with `--features {feature}` (the \
1195 released binaries and the Docker image have it)"
1196 )
1197}
1198
1199async fn shutdown_signal() {
1202 let ctrl_c = async {
1203 if let Err(e) = signal::ctrl_c().await {
1204 warn!("Cannot listen for Ctrl+C, stop the server another way: {e}");
1205 std::future::pending::<()>().await;
1206 }
1207 };
1208
1209 #[cfg(unix)]
1210 let terminate = async {
1211 match signal::unix::signal(signal::unix::SignalKind::terminate()) {
1212 Ok(mut terminate) => {
1213 terminate.recv().await;
1214 }
1215 Err(e) => {
1216 warn!("Cannot listen for SIGTERM, stop the server another way: {e}");
1217 std::future::pending::<()>().await;
1218 }
1219 }
1220 };
1221
1222 #[cfg(not(unix))]
1223 let terminate = std::future::pending::<()>();
1224
1225 tokio::select! {
1226 _ = ctrl_c => info!("Received Ctrl+C signal"),
1227 _ = terminate => info!("Received SIGTERM signal"),
1228 }
1229}
1230
1231async fn health_check(State(state): State<AppState>) -> Json<HealthResponse> {
1232 Json(HealthResponse {
1233 status: "healthy".to_string(),
1234 model_loaded: true,
1235 backend: state.default_slot().map_or_else(
1236 || "none".to_string(),
1237 |slot| slot.get().model.get_backend_info(),
1238 ),
1239 uptime_seconds: state.uptime_seconds(),
1240 timestamp: crate::time::rfc3339_now(),
1241 })
1242}
1243
1244async fn readiness(State(state): State<AppState>) -> Response {
1247 let ready = state.ready();
1248 let status = if ready {
1249 StatusCode::OK
1250 } else {
1251 StatusCode::SERVICE_UNAVAILABLE
1252 };
1253 (
1254 status,
1255 Json(ReadinessResponse {
1256 status: if ready { "ready" } else { "warming_up" }.to_string(),
1257 timestamp: crate::time::rfc3339_now(),
1258 }),
1259 )
1260 .into_response()
1261}
1262
1263async fn predict(State(state): State<AppState>, request: Request) -> Response {
1264 let Some(slot) = state.default_slot().cloned() else {
1265 return no_tensor_model_response();
1266 };
1267 predict_with(&state, &slot, request).await
1268}
1269
1270fn no_tensor_model_response() -> Response {
1272 create_error_response(
1273 StatusCode::NOT_FOUND,
1274 Code::ModelNotFound,
1275 "This server serves no tensor model; its LLMs, if any, are on /v1/chat/completions",
1276 None,
1277 )
1278}
1279
1280async fn predict_named(
1282 State(state): State<AppState>,
1283 Path(name): Path<String>,
1284 request: Request,
1285) -> Response {
1286 let Some(slot) = state.slot(&name).cloned() else {
1287 return unknown_model_response(&state, &name);
1288 };
1289 predict_with(&state, &slot, request).await
1290}
1291
1292fn unknown_model_response(state: &ServerState, name: &str) -> Response {
1293 create_error_response(
1294 StatusCode::NOT_FOUND,
1295 Code::ModelNotFound,
1296 &format!(
1297 "Unknown model '{name}'; this server serves {}",
1298 state.model_names().join(", ")
1299 ),
1300 Some(serde_json::json!({ "models": state.model_names() })),
1301 )
1302}
1303
1304async fn predict_with(state: &AppState, slot: &ModelSlot, request: Request) -> Response {
1305 let loaded = slot.get();
1307 if !loaded.model.is_simple() {
1308 let info = loaded.model.get_info();
1309 return create_error_response(
1310 StatusCode::BAD_REQUEST,
1311 Code::NamedTensorsRequired,
1312 &format!(
1313 "Model '{}' takes named tensors ({}); send them to POST /v2/models/{}/infer",
1314 info.name,
1315 info.inputs
1316 .iter()
1317 .map(|t| format!("{} {}", t.name, t.datatype))
1318 .collect::<Vec<_>>()
1319 .join(", "),
1320 info.name
1321 ),
1322 None,
1323 );
1324 }
1325 let announced = state.serving.announce(&slot.name);
1328 let format = match RequestFormat::from_headers(request.headers()) {
1330 Some(format) => format,
1331 None => {
1332 return create_error_response(
1333 StatusCode::UNSUPPORTED_MEDIA_TYPE,
1334 Code::InvalidRequest,
1335 &format!("Expected request with `Content-Type: {SUPPORTED_REQUEST_TYPES}`"),
1336 None,
1337 );
1338 }
1339 };
1340 #[cfg(feature = "image")]
1341 if let RequestFormat::Image(_) = format
1342 && let Err(response) = image_size(&loaded.model)
1343 {
1344 return *response;
1345 }
1346 let binary_response = accepts_octet_stream(request.headers());
1347 let batch_header = request.headers().get(BATCH_SIZE_HEADER).cloned();
1349
1350 let body = match Bytes::from_request(request, state).await {
1351 Ok(body) => body,
1352 Err(rejection) => {
1353 warn!("Rejected request body: {}", rejection.body_text());
1354 return create_error_response(
1355 rejection.status(),
1356 Code::InvalidRequest,
1357 &rejection.body_text(),
1358 None,
1359 );
1360 }
1361 };
1362
1363 let decoded = match format {
1364 RequestFormat::Json => decode_json(&loaded.model, &body),
1365 RequestFormat::Binary => decode_binary(&loaded.model, batch_header.as_ref(), &body),
1366 #[cfg(feature = "image")]
1367 RequestFormat::Image(kind) => decode_image(state, &loaded.model, kind, body.clone()).await,
1368 };
1369 drop(body);
1371 let PredictInput {
1372 data,
1373 batch_size,
1374 single,
1375 } = match decoded {
1376 Ok(input) => input,
1377 Err(response) => return *response,
1378 };
1379 let start_time = Instant::now();
1381 let output = match state
1382 .infer_flat(slot, &loaded, announced, data, batch_size)
1383 .await
1384 {
1385 Ok(output) => output,
1386 Err(e) => return e.into_response(),
1387 };
1388 let inference_time_ms = start_time.elapsed().as_secs_f64() * 1000.0;
1389 debug!(
1391 "Prediction completed in {:.2}ms, batch size: {}",
1392 inference_time_ms, batch_size
1393 );
1394 if binary_response {
1395 return binary_output_response(
1396 &output,
1397 batch_size,
1398 &loaded.model.get_info().output_spec.shape,
1399 inference_time_ms,
1400 );
1401 }
1402 Json(PredictReply {
1403 output: single.then_some(&output[..]),
1404 outputs: (!single).then_some(Rows {
1405 values: &output,
1406 rows: batch_size,
1407 }),
1408 status: "success",
1409 inference_time_ms,
1410 timestamp: crate::time::rfc3339_now(),
1411 batch_size,
1412 })
1413 .into_response()
1414}
1415
1416#[cfg(not(feature = "image"))]
1417const SUPPORTED_REQUEST_TYPES: &str = "application/json` or `application/octet-stream";
1418#[cfg(feature = "image")]
1419const SUPPORTED_REQUEST_TYPES: &str =
1420 "application/json`, `application/octet-stream`, `image/jpeg` or `image/png";
1421
1422#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1423enum RequestFormat {
1424 Json,
1425 Binary,
1427 #[cfg(feature = "image")]
1429 Image(ImageKind),
1430}
1431
1432impl RequestFormat {
1433 fn from_headers(headers: &HeaderMap) -> Option<Self> {
1434 let content_type = headers.get(CONTENT_TYPE)?.to_str().ok()?;
1435 let essence = media_type_essence(content_type);
1436 if essence.eq_ignore_ascii_case("application/json")
1437 || essence.to_ascii_lowercase().ends_with("+json")
1438 {
1439 return Some(Self::Json);
1440 }
1441 if essence.eq_ignore_ascii_case(OCTET_STREAM) {
1442 return Some(Self::Binary);
1443 }
1444 #[cfg(feature = "image")]
1445 if let Some(kind) = ImageKind::from_media_type(essence) {
1446 return Some(Self::Image(kind));
1447 }
1448 None
1449 }
1450}
1451
1452fn media_type_essence(value: &str) -> &str {
1454 value.split(';').next().unwrap_or_default().trim()
1455}
1456
1457fn accepts_octet_stream(headers: &HeaderMap) -> bool {
1459 headers
1460 .get_all(ACCEPT)
1461 .iter()
1462 .filter_map(|value| value.to_str().ok())
1463 .flat_map(|value| value.split(','))
1464 .any(|item| media_type_essence(item).eq_ignore_ascii_case(OCTET_STREAM))
1465}
1466
1467fn decode_json(model: &Model, body: &[u8]) -> std::result::Result<PredictInput, Box<Response>> {
1468 let input_data = match crate::json_fast::parse(body) {
1471 Some(crate::json_fast::FastInput::Single(input)) => PredictInputData::Single { input },
1472 Some(crate::json_fast::FastInput::Batch(inputs)) => PredictInputData::Batch { inputs },
1473 None => decode_json_serde(body)?.input_data,
1474 };
1475 match input_data {
1476 PredictInputData::Single { input } => {
1477 validate_inputs(model, std::slice::from_ref(&input))?;
1478 Ok(PredictInput {
1479 data: input,
1480 batch_size: 1,
1481 single: true,
1482 })
1483 }
1484 PredictInputData::Batch { inputs } => {
1485 validate_inputs(model, &inputs)?;
1486 Ok(PredictInput {
1487 batch_size: inputs.len(),
1488 data: inputs.concat(),
1489 single: false,
1490 })
1491 }
1492 }
1493}
1494
1495fn decode_json_serde(body: &[u8]) -> std::result::Result<PredictRequest, Box<Response>> {
1496 serde_json::from_slice(body).map_err(|e| {
1499 let (status, message) = match e.classify() {
1500 serde_json::error::Category::Data => (
1501 StatusCode::UNPROCESSABLE_ENTITY,
1502 format!("Failed to deserialize the JSON body into the target type: {e}"),
1503 ),
1504 _ => (
1505 StatusCode::BAD_REQUEST,
1506 format!("Failed to parse the request body as JSON: {e}"),
1507 ),
1508 };
1509 warn!("Rejected request body: {}", message);
1510 Box::new(create_error_response(
1511 status,
1512 Code::InvalidRequest,
1513 &message,
1514 None,
1515 ))
1516 })
1517}
1518
1519fn decode_binary(
1524 model: &Model,
1525 batch_header: Option<&HeaderValue>,
1526 body: &[u8],
1527) -> std::result::Result<PredictInput, Box<Response>> {
1528 let batch_size = match batch_header {
1529 None => 1,
1530 Some(value) => match value.to_str().ok().and_then(|s| s.trim().parse().ok()) {
1531 Some(n) => n,
1532 None => {
1533 return Err(Box::new(create_error_response(
1534 StatusCode::BAD_REQUEST,
1535 Code::InvalidRequest,
1536 "X-Batch-Size must be a non-negative integer",
1537 None,
1538 )));
1539 }
1540 },
1541 };
1542
1543 check_batch_len(batch_size)?;
1544 if body.is_empty() {
1545 return Err(empty_input_response());
1546 }
1547
1548 let shape = &model.get_info().input_spec.shape;
1549 let input_size: usize = shape.iter().product();
1550 let expected_bytes = batch_size * input_size * size_of::<f32>();
1551 if body.len() != expected_bytes {
1552 return Err(Box::new(create_error_response(
1553 StatusCode::BAD_REQUEST,
1554 Code::InputValidationFailed,
1555 &format!(
1556 "Expected {expected_bytes} bytes ({batch_size} item(s) of shape {shape:?} as \
1557 little-endian f32), got {}",
1558 body.len()
1559 ),
1560 Some(serde_json::json!({
1561 "expected_shape": shape,
1562 "batch_size": batch_size,
1563 "expected_bytes": expected_bytes,
1564 "received_bytes": body.len()
1565 })),
1566 )));
1567 }
1568
1569 Ok(PredictInput {
1571 data: f32s_from_le(body),
1572 batch_size,
1573 single: batch_header.is_none(),
1574 })
1575}
1576
1577#[cfg(feature = "image")]
1579fn image_size(model: &Model) -> std::result::Result<(u32, u32), Box<Response>> {
1580 let shape = &model.get_info().input_spec.shape;
1581 image_input_size(shape).ok_or_else(|| {
1582 Box::new(create_error_response(
1583 StatusCode::UNSUPPORTED_MEDIA_TYPE,
1584 Code::InvalidRequest,
1585 &format!(
1586 "Image input needs a model with input shape [3, H, W]; this model takes \
1587 {shape:?}. Send JSON or `{OCTET_STREAM}` instead"
1588 ),
1589 Some(serde_json::json!({ "expected_shape": shape })),
1590 ))
1591 })
1592}
1593
1594#[cfg(feature = "image")]
1598async fn decode_image(
1599 state: &ServerState,
1600 model: &Model,
1601 kind: ImageKind,
1602 body: Bytes,
1603) -> std::result::Result<PredictInput, Box<Response>> {
1604 let (height, width) = image_size(model)?;
1605 if body.is_empty() {
1606 return Err(empty_input_response());
1607 }
1608
1609 let Ok(permit) = state.inference_slots.clone().try_acquire_owned() else {
1612 return Err(Box::new(infer::InferError::Busy.into_response()));
1613 };
1614 let task = tokio::task::spawn_blocking(move || {
1615 let _permit = permit;
1616 preprocess_image(&body, kind, height, width)
1617 });
1618
1619 match task.await {
1620 Ok(Ok(data)) => Ok(PredictInput {
1621 data,
1622 batch_size: 1,
1623 single: true,
1624 }),
1625 Ok(Err(e)) => {
1626 warn!("Rejected image body: {}", e);
1627 Err(Box::new(create_error_response(
1628 StatusCode::BAD_REQUEST,
1629 Code::InvalidImage,
1630 &format!("Failed to decode the image: {e}"),
1631 None,
1632 )))
1633 }
1634 Err(e) => {
1635 error!("Image decoding task failed: {}", e);
1636 Err(Box::new(create_error_response(
1637 StatusCode::INTERNAL_SERVER_ERROR,
1638 Code::InternalError,
1639 "Image decoding task panicked",
1640 None,
1641 )))
1642 }
1643 }
1644}
1645
1646fn binary_output_response(
1648 output: &[f32],
1649 batch_size: usize,
1650 output_shape: &[usize],
1651 inference_time_ms: f64,
1652) -> Response {
1653 let shape = output_shape
1654 .iter()
1655 .map(usize::to_string)
1656 .collect::<Vec<_>>()
1657 .join(",");
1658 let header = |value: String| {
1659 HeaderValue::from_str(&value).expect("digits and commas are a valid header value")
1660 };
1661 (
1662 StatusCode::OK,
1663 [
1664 (CONTENT_TYPE, HeaderValue::from_static(OCTET_STREAM)),
1665 (BATCH_SIZE_HEADER, HeaderValue::from(batch_size)),
1666 (OUTPUT_SHAPE_HEADER, header(shape)),
1667 (
1668 INFERENCE_TIME_HEADER,
1669 header(format!("{inference_time_ms:.3}")),
1670 ),
1671 ],
1672 f32s_to_le(output),
1673 )
1674 .into_response()
1675}
1676
1677fn empty_input_response() -> Box<Response> {
1678 Box::new(create_error_response(
1679 StatusCode::BAD_REQUEST,
1680 Code::InvalidInput,
1681 "Input cannot be empty",
1682 None,
1683 ))
1684}
1685
1686fn check_batch_len(batch_size: usize) -> std::result::Result<(), Box<Response>> {
1687 if batch_size == 0 {
1688 return Err(empty_input_response());
1689 }
1690 if batch_size > MAX_BATCH_SIZE {
1691 return Err(Box::new(create_error_response(
1692 StatusCode::BAD_REQUEST,
1693 Code::BatchTooLarge,
1694 &format!("Batch size {batch_size} exceeds maximum allowed batch size {MAX_BATCH_SIZE}"),
1695 None,
1696 )));
1697 }
1698 Ok(())
1699}
1700
1701fn validate_inputs(model: &Model, inputs: &[Vec<f32>]) -> std::result::Result<(), Box<Response>> {
1703 check_batch_len(inputs.len())?;
1704 if inputs.iter().any(Vec::is_empty) {
1705 return Err(empty_input_response());
1706 }
1707
1708 for (i, input) in inputs.iter().enumerate() {
1709 if let Err(e) = model.validate_input_shape(input) {
1710 return Err(Box::new(create_error_response(
1711 StatusCode::BAD_REQUEST,
1712 Code::InputValidationFailed,
1713 &e.to_string(),
1714 Some(serde_json::json!({
1715 "expected_shape": model.get_info().input_spec.shape,
1716 "received_size": input.len(),
1717 "batch_item": i
1718 })),
1719 )));
1720 }
1721 }
1722
1723 Ok(())
1724}
1725
1726async fn count_requests(State(state): State<AppState>, request: Request, next: Next) -> Response {
1729 let start = Instant::now();
1730 let response = next.run(request).await;
1731 let status = response.status();
1732 state.count_request(
1733 start.elapsed(),
1734 status.is_client_error() || status.is_server_error(),
1735 response.extensions().get::<infer::Busy>().is_some(),
1736 );
1737 response
1738}
1739
1740async fn metrics(State(state): State<AppState>, headers: HeaderMap) -> Response {
1743 if wants_prometheus(&headers) {
1744 return (
1745 [(
1746 CONTENT_TYPE,
1747 HeaderValue::from_static("text/plain; version=0.0.4; charset=utf-8"),
1748 )],
1749 prometheus_metrics(&state),
1750 )
1751 .into_response();
1752 }
1753 Json(json_metrics(&state)).into_response()
1754}
1755
1756fn wants_prometheus(headers: &HeaderMap) -> bool {
1759 let Some(accept) = headers.get(ACCEPT).and_then(|v| v.to_str().ok()) else {
1760 return false;
1761 };
1762 for part in accept.split(',') {
1763 let media = media_type_essence(part.trim());
1764 if media == "application/json" || media == "*/*" {
1765 return false;
1766 }
1767 if media == "text/plain" || media == "application/openmetrics-text" {
1768 return true;
1769 }
1770 }
1771 false
1772}
1773
1774fn prometheus_metrics(state: &ServerState) -> String {
1775 use kuebiko::{label, render_value};
1776 use std::fmt::Write as _;
1777 let mut out = String::with_capacity(8192);
1778 let _ = writeln!(out, "# HELP furnace_build_info Furnace version");
1779 let _ = writeln!(out, "# TYPE furnace_build_info gauge");
1780 let _ = writeln!(
1781 out,
1782 "furnace_build_info{{version=\"{}\"}} 1",
1783 env!("CARGO_PKG_VERSION")
1784 );
1785 let gauge = "gauge";
1786 let counter = "counter";
1787 let values: [(&str, &str, &str, u64); 10] = [
1788 (
1789 "furnace_uptime_seconds",
1790 gauge,
1791 "Seconds since the server started",
1792 state.uptime_seconds(),
1793 ),
1794 (
1795 "furnace_ready",
1796 gauge,
1797 "1 once every model's warm-up inference has run",
1798 u64::from(state.ready()),
1799 ),
1800 (
1801 "furnace_requests_total",
1802 counter,
1803 "/predict requests received, including rejected ones",
1804 state.total_requests.load(Ordering::Relaxed),
1805 ),
1806 (
1807 "furnace_request_errors_total",
1808 counter,
1809 "/predict requests answered with a 4xx or 5xx status",
1810 state.total_errors.load(Ordering::Relaxed),
1811 ),
1812 (
1813 "furnace_requests_rejected_busy_total",
1814 counter,
1815 "Requests rejected with 503 SERVER_BUSY",
1816 state.rejected_busy.load(Ordering::Relaxed),
1817 ),
1818 (
1819 "furnace_requests_rejected_unauthorized_total",
1820 counter,
1821 "Requests rejected with 401 for a missing or unknown API key",
1822 state.rejected_unauthorized.load(Ordering::Relaxed),
1823 ),
1824 (
1825 "furnace_requests_rejected_rate_limited_total",
1826 counter,
1827 "Requests rejected with 429 by the rate limit",
1828 state.rejected_rate_limited.load(Ordering::Relaxed),
1829 ),
1830 (
1831 "furnace_in_flight_requests",
1832 gauge,
1833 "Requests queued for or running inference",
1834 in_flight(state) as u64,
1835 ),
1836 (
1837 "furnace_max_concurrent_requests",
1838 gauge,
1839 "Most requests queued for or running inference at once",
1840 state.max_concurrent_requests as u64,
1841 ),
1842 (
1843 "furnace_max_batch_size",
1844 gauge,
1845 "Most items per batched forward pass; 1 means batching is off",
1846 state.max_batch_size as u64,
1847 ),
1848 ];
1849 for (name, kind, help, value) in values {
1850 render_value(&mut out, name, kind, help, value);
1851 }
1852 state.request_latency.render(
1853 &mut out,
1854 "furnace_request_duration_seconds",
1855 "Inference request latency from arrival to response",
1856 );
1857 #[cfg(feature = "llm")]
1858 if let Some(llm) = &state.llm {
1859 llm.render_prometheus(&mut out);
1860 }
1861 state.serving.fabric().render_prometheus(&mut out);
1862
1863 let models: Vec<_> = state
1865 .models
1866 .iter()
1867 .map(|slot| {
1868 let loaded = slot.get();
1869 (slot, loaded.model.get_stats(), loaded)
1870 })
1871 .collect();
1872 type Field = fn(&ModelStats, &Loaded) -> String;
1873 let families: [(&str, &str, &str, Field); 7] = [
1874 (
1875 "furnace_model_info",
1876 gauge,
1877 "Served models and their backends",
1878 |_, _| "1".to_string(),
1879 ),
1880 (
1881 "furnace_model_ready",
1882 gauge,
1883 "1 once the model's warm-up inference has run",
1884 |_, l| u8::from(l.ready()).to_string(),
1885 ),
1886 (
1887 "furnace_inferences_total",
1888 counter,
1889 "Requests that reached the model",
1890 |s, _| s.inference_count.to_string(),
1891 ),
1892 (
1893 "furnace_inference_errors_total",
1894 counter,
1895 "Requests the model failed",
1896 |s, _| s.error_count.to_string(),
1897 ),
1898 (
1899 "furnace_batches_total",
1900 counter,
1901 "Forward passes run",
1902 |s, _| s.batch_count.to_string(),
1903 ),
1904 (
1905 "furnace_batch_items_total",
1906 counter,
1907 "Items processed by forward passes",
1908 |s, _| s.batched_items.to_string(),
1909 ),
1910 (
1911 "furnace_model_size_bytes",
1912 gauge,
1913 "Size of the model weights",
1914 |_, l| l.model.get_info().model_size_bytes.to_string(),
1915 ),
1916 ];
1917 for (name, kind, help, field) in families {
1918 let _ = writeln!(out, "# HELP {name} {help}");
1919 let _ = writeln!(out, "# TYPE {name} {kind}");
1920 for (slot, stats, loaded) in &models {
1921 let mut labels = format!("model=\"{}\"", label(&slot.name));
1922 if name == "furnace_model_info" {
1923 let _ = write!(
1924 labels,
1925 ",backend=\"{}\"",
1926 label(&loaded.model.get_info().backend)
1927 );
1928 }
1929 let _ = writeln!(out, "{name}{{{labels}}} {}", field(stats, loaded));
1930 }
1931 }
1932 let name = "furnace_inference_duration_seconds";
1933 let _ = writeln!(
1934 out,
1935 "# HELP {name} Successful inference latency from decoded input to output, queueing included"
1936 );
1937 let _ = writeln!(out, "# TYPE {name} histogram");
1938 for (slot, _, _) in &models {
1939 slot.inference_latency.render_series(
1940 &mut out,
1941 name,
1942 &format!("model=\"{}\"", label(&slot.name)),
1943 );
1944 }
1945 out
1946}
1947
1948fn in_flight(state: &ServerState) -> usize {
1949 state
1950 .max_concurrent_requests
1951 .saturating_sub(state.inference_slots.available_permits())
1952}
1953
1954fn json_metrics(state: &ServerState) -> MetricsResponse {
1955 let models: Vec<ModelMetrics> = state
1956 .models
1957 .iter()
1958 .map(|slot| {
1959 let loaded = slot.get();
1960 let stats = loaded.model.get_stats();
1961 ModelMetrics {
1962 name: slot.name.clone(),
1963 ready: loaded.ready(),
1964 inference_count: stats.inference_count,
1965 error_count: stats.error_count,
1966 batch_count: stats.batch_count,
1967 average_batch_size: average_batch_size(&stats),
1968 average_inference_time_ms: stats.average_inference_time_ms,
1969 inference_latency_ms: slot.inference_latency.percentiles_ms(),
1970 }
1971 })
1972 .collect();
1973 let stats: Vec<ModelStats> = state
1975 .models
1976 .iter()
1977 .map(|slot| slot.get().model.get_stats())
1978 .collect();
1979 let success_count: u64 = stats.iter().map(|s| s.success_count).sum();
1980 let total_inference_time_ms: f64 = stats.iter().map(|s| s.total_inference_time_ms).sum();
1981 let stats = ModelStats {
1982 inference_count: stats.iter().map(|s| s.inference_count).sum(),
1983 total_inference_time_ms,
1984 last_inference_time: stats.iter().filter_map(|s| s.last_inference_time).max(),
1985 average_inference_time_ms: if success_count == 0 {
1986 0.0
1987 } else {
1988 total_inference_time_ms / success_count as f64
1989 },
1990 error_count: stats.iter().map(|s| s.error_count).sum(),
1991 success_count,
1992 min_inference_time_ms: stats
1993 .iter()
1994 .filter_map(|s| s.min_inference_time_ms)
1995 .reduce(f64::min),
1996 max_inference_time_ms: stats
1997 .iter()
1998 .filter_map(|s| s.max_inference_time_ms)
1999 .reduce(f64::max),
2000 batch_count: stats.iter().map(|s| s.batch_count).sum(),
2001 batched_items: stats.iter().map(|s| s.batched_items).sum(),
2002 };
2003 MetricsResponse {
2004 uptime_seconds: state.uptime_seconds(),
2005 total_requests: state.total_requests.load(Ordering::Relaxed),
2006 total_errors: state.total_errors.load(Ordering::Relaxed),
2007 rejected_busy: state.rejected_busy.load(Ordering::Relaxed),
2008 rejected_unauthorized: state.rejected_unauthorized.load(Ordering::Relaxed),
2009 rejected_rate_limited: state.rejected_rate_limited.load(Ordering::Relaxed),
2010 in_flight_inferences: in_flight(state),
2011 max_concurrent_requests: state.max_concurrent_requests,
2012 total_inferences: stats.inference_count,
2013 total_batches: stats.batch_count,
2014 average_batch_size: average_batch_size(&stats),
2015 max_batch_size: state.max_batch_size,
2016 average_inference_time_ms: stats.average_inference_time_ms,
2017 last_inference_at: stats.last_inference_time.map(crate::time::rfc3339_utc),
2018 request_latency_ms: state.request_latency.percentiles_ms(),
2019 inference_latency_ms: state.inference_latency.percentiles_ms(),
2020 ready: state.ready(),
2021 models,
2022 #[cfg(feature = "llm")]
2023 llm: state
2024 .llm
2025 .as_ref()
2026 .map(|llm| llm.json_metrics())
2027 .unwrap_or_default(),
2028 nerve: Some(state.serving.fabric().metrics()),
2029 timestamp: crate::time::rfc3339_now(),
2030 }
2031}
2032
2033fn average_batch_size(stats: &ModelStats) -> f64 {
2034 if stats.batch_count == 0 {
2035 0.0
2036 } else {
2037 stats.batched_items as f64 / stats.batch_count as f64
2038 }
2039}
2040
2041fn model_info_response(slot: &ModelSlot) -> ModelInfoResponse {
2042 let loaded = slot.get();
2043 ModelInfoResponse {
2044 model_info: loaded.model.get_info().clone(),
2045 stats: loaded.model.get_stats(),
2046 timestamp: crate::time::rfc3339_now(),
2047 }
2048}
2049
2050async fn model_info(State(state): State<AppState>) -> Response {
2052 match state.default_slot() {
2053 Some(slot) => Json(model_info_response(slot)).into_response(),
2054 None => no_tensor_model_response(),
2055 }
2056}
2057
2058async fn model_info_named(State(state): State<AppState>, Path(name): Path<String>) -> Response {
2060 match state.slot(&name) {
2061 Some(slot) => Json(model_info_response(slot)).into_response(),
2062 None => unknown_model_response(&state, &name),
2063 }
2064}
2065
2066async fn list_models(State(state): State<AppState>) -> Json<ModelsResponse> {
2068 let models = state
2069 .models
2070 .iter()
2071 .map(|slot| {
2072 let loaded = slot.get();
2073 let info = loaded.model.get_info();
2074 ModelSummary {
2075 name: slot.name.clone(),
2076 ready: loaded.ready(),
2077 input_shape: info.input_spec.shape.clone(),
2078 output_shape: info.output_spec.shape.clone(),
2079 backend: info.backend.clone(),
2080 reloadable: slot.reload.is_some(),
2081 }
2082 })
2083 .collect();
2084 Json(ModelsResponse {
2085 default_model: state
2086 .default_slot()
2087 .map(|slot| slot.name.clone())
2088 .unwrap_or_default(),
2089 models,
2090 })
2091}
2092
2093#[derive(Debug, Default, Deserialize)]
2094struct ReloadRequest {
2095 weights: Option<std::path::PathBuf>,
2097}
2098
2099async fn reload_model(
2102 State(state): State<AppState>,
2103 Path(name): Path<String>,
2104 headers: HeaderMap,
2105 body: Bytes,
2106) -> Response {
2107 let Some(token) = &state.admin_token else {
2108 return create_error_response(
2109 StatusCode::NOT_FOUND,
2110 Code::AdminApiDisabled,
2111 "The admin API is off; start the server with --admin-token to enable it",
2112 None,
2113 );
2114 };
2115 if !authorized(&headers, token) {
2116 let mut response = create_error_response(
2117 StatusCode::UNAUTHORIZED,
2118 Code::Unauthorized,
2119 "Missing or wrong bearer token",
2120 None,
2121 );
2122 response.headers_mut().insert(
2123 axum::http::header::WWW_AUTHENTICATE,
2124 HeaderValue::from_static("Bearer"),
2125 );
2126 return response;
2127 }
2128 let Some(slot) = state.slot(&name).cloned() else {
2129 return unknown_model_response(&state, &name);
2130 };
2131 let Some(reload) = slot.reload.clone() else {
2132 return create_error_response(
2133 StatusCode::CONFLICT,
2134 Code::NotReloadable,
2135 &format!("Model '{name}' cannot be reloaded"),
2136 None,
2137 );
2138 };
2139 let request: ReloadRequest = if body.iter().all(u8::is_ascii_whitespace) {
2140 ReloadRequest::default()
2141 } else {
2142 match serde_json::from_slice(&body) {
2143 Ok(request) => request,
2144 Err(e) => {
2145 return create_error_response(
2146 StatusCode::BAD_REQUEST,
2147 Code::InvalidRequest,
2148 &format!("Expected {{\"weights\": \"<path>\"}} or an empty body: {e}"),
2149 None,
2150 );
2151 }
2152 }
2153 };
2154 let Ok(_reloading) = slot.reloading.try_lock() else {
2155 return create_error_response(
2156 StatusCode::CONFLICT,
2157 Code::ReloadInProgress,
2158 &format!("Model '{name}' is already being reloaded"),
2159 None,
2160 );
2161 };
2162
2163 let previous = slot.get();
2164 let weights = request.weights.clone();
2165 let start = Instant::now();
2166 let task = tokio::task::spawn_blocking(move || -> Result<(Loaded, f64)> {
2167 let mut model = reload(weights.as_deref())?;
2168 let previous_info = previous.model.get_info();
2169 let info = model.get_info();
2170 if info.name != previous_info.name {
2171 return Err(ModelError::InvalidFormat(format!(
2172 "The reloaded model is named '{}', not '{}'",
2173 info.name, previous_info.name
2174 ))
2175 .into());
2176 }
2177 if info.inputs != previous_info.inputs || info.outputs != previous_info.outputs {
2178 let describe = |tensors: &[crate::tensor::TensorInfo]| {
2179 tensors
2180 .iter()
2181 .map(|t| format!("{} {} {:?}", t.name, t.datatype, t.shape))
2182 .collect::<Vec<_>>()
2183 .join(", ")
2184 };
2185 return Err(ModelError::InvalidFormat(format!(
2186 "The reloaded model takes [{}] -> [{}], the served one [{}] -> [{}]",
2187 describe(&info.inputs),
2188 describe(&info.outputs),
2189 describe(&previous_info.inputs),
2190 describe(&previous_info.outputs)
2191 ))
2192 .into());
2193 }
2194 model.adopt_stats(&previous.model);
2195 let loaded = Loaded::new(model);
2196 let warm_up = loaded.model.warm_up()?;
2197 loaded.ready.store(true, Ordering::Release);
2198 Ok((loaded, warm_up.map_or(0.0, |d| d.as_secs_f64() * 1e3)))
2199 });
2200 match task.await {
2201 Ok(Ok((loaded, warm_up_ms))) => {
2202 slot.replace(Arc::new(loaded));
2203 let reload_ms = start.elapsed().as_secs_f64() * 1e3;
2204 info!("🔄 Reloaded model {name} in {reload_ms:.1}ms (warm-up {warm_up_ms:.1}ms)");
2205 Json(ReloadResponse {
2206 model: name,
2207 weights: request.weights.map(|p| p.display().to_string()),
2208 reload_ms,
2209 warm_up_ms,
2210 timestamp: crate::time::rfc3339_now(),
2211 })
2212 .into_response()
2213 }
2214 Ok(Err(e)) => {
2215 warn!("Reloading model {name} failed, still serving the previous one: {e}");
2216 create_error_response(
2217 StatusCode::UNPROCESSABLE_ENTITY,
2218 Code::ReloadFailed,
2219 &format!("Reload failed, still serving the previous model: {e}"),
2220 None,
2221 )
2222 }
2223 Err(e) => create_error_response(
2224 StatusCode::INTERNAL_SERVER_ERROR,
2225 Code::ReloadFailed,
2226 &format!("Reload task failed, still serving the previous model: {e}"),
2227 None,
2228 ),
2229 }
2230}
2231
2232fn authorized(headers: &HeaderMap, token: &str) -> bool {
2234 let Some(given) = headers
2235 .get(axum::http::header::AUTHORIZATION)
2236 .and_then(|v| v.to_str().ok())
2237 .and_then(|v| v.strip_prefix("Bearer "))
2238 else {
2239 return false;
2240 };
2241 constant_time_eq(given.as_bytes(), token.as_bytes())
2242}
2243
2244fn create_error_response(
2246 status_code: StatusCode,
2247 code: Code,
2248 message: &str,
2249 details: Option<serde_json::Value>,
2250) -> Response {
2251 let error_response =
2252 ErrorResponse::new(message.to_string(), code.as_str().to_string(), details);
2253 (status_code, Json(error_response)).into_response()
2254}
2255
2256#[cfg(test)]
2257pub(crate) mod tests {
2258 use super::*;
2259 use crate::model::DummyModel;
2260 use axum::{body::Body, http::Request};
2261 use tower::util::ServiceExt;
2262
2263 pub(super) fn dummy_model() -> Model {
2264 let dummy = DummyModel::new("test_model".to_string(), vec![784], vec![10]);
2265 Model::new(Box::new(dummy), "dummy", 0)
2266 }
2267
2268 pub(crate) fn router(model: Model, config: &ServerConfig) -> Result<Router> {
2270 Server::with_config(config.clone()).model(model).router()
2271 }
2272
2273 fn router_with_models(models: Vec<ServedModel>, config: &ServerConfig) -> Result<Router> {
2275 let mut server = Server::with_config(config.clone());
2276 server.contents = models.into();
2277 server.router()
2278 }
2279
2280 pub(super) fn test_app() -> Router {
2281 router(dummy_model(), &ServerConfig::default()).unwrap()
2282 }
2283
2284 #[cfg(feature = "grpc")]
2285 pub(super) fn test_app_state() -> AppState {
2286 server_state(
2287 vec![ServedModel::from(dummy_model())].into(),
2288 &ServerConfig::default(),
2289 )
2290 .unwrap()
2291 }
2292
2293 pub(super) async fn send(
2294 app: Router,
2295 request: Request<Body>,
2296 ) -> (StatusCode, axum::body::Bytes) {
2297 let response = app.oneshot(request).await.unwrap();
2298 let status = response.status();
2299 let body = axum::body::to_bytes(response.into_body(), usize::MAX)
2300 .await
2301 .unwrap();
2302 (status, body)
2303 }
2304
2305 fn predict_request(body: String) -> Request<Body> {
2306 Request::builder()
2307 .uri("/predict")
2308 .method("POST")
2309 .header("content-type", "application/json")
2310 .body(Body::from(body))
2311 .unwrap()
2312 }
2313
2314 async fn get_metrics(app: Router) -> MetricsResponse {
2315 let request = Request::builder()
2316 .uri("/metrics")
2317 .body(Body::empty())
2318 .unwrap();
2319 let (status, body) = send(app, request).await;
2320 assert_eq!(status, StatusCode::OK);
2321 serde_json::from_slice(&body).unwrap()
2322 }
2323
2324 #[tokio::test]
2325 async fn test_metrics_counts_requests_and_errors() {
2326 let app = test_app();
2327 let initial = get_metrics(app.clone()).await;
2328 assert_eq!(initial.total_requests, 0);
2329 assert_eq!(initial.max_concurrent_requests, 100);
2330 assert!(initial.last_inference_at.is_none());
2331
2332 let ok = serde_json::json!({ "input": vec![0.5; 784] }).to_string();
2333 let (status, _) = send(app.clone(), predict_request(ok)).await;
2334 assert_eq!(status, StatusCode::OK);
2335 let bad = serde_json::json!({ "input": vec![0.5; 3] }).to_string();
2336 let (status, _) = send(app.clone(), predict_request(bad)).await;
2337 assert_eq!(status, StatusCode::BAD_REQUEST);
2338
2339 let m = get_metrics(app).await;
2340 assert_eq!(m.total_requests, 2);
2341 assert_eq!(m.total_errors, 1);
2342 assert_eq!(m.rejected_busy, 0);
2343 assert_eq!(m.total_inferences, 1);
2344 assert_eq!(m.total_batches, 1);
2345 assert_eq!(m.average_batch_size, 1.0);
2346 assert_eq!(m.max_batch_size, 1);
2348 assert_eq!(m.in_flight_inferences, 0);
2349 assert!(m.last_inference_at.is_some());
2350 }
2351
2352 async fn concurrent_predictions(config: &ServerConfig) -> MetricsResponse {
2353 let app = router(dummy_model(), config).unwrap();
2354 let mut tasks = Vec::new();
2355 for i in 0..12 {
2356 let body = if i % 3 == 0 {
2357 serde_json::json!({ "inputs": vec![vec![0.5; 784]; 2] })
2358 } else if i == 4 {
2359 serde_json::json!({ "input": vec![0.5; 3] })
2360 } else {
2361 serde_json::json!({ "input": vec![0.5; 784] })
2362 };
2363 let app = app.clone();
2364 tasks.push(tokio::spawn(async move {
2365 send(app, predict_request(body.to_string())).await
2366 }));
2367 }
2368 for (i, task) in tasks.into_iter().enumerate() {
2369 let (status, body) = task.await.unwrap();
2370 if i == 4 {
2371 assert_eq!(status, StatusCode::BAD_REQUEST);
2372 continue;
2373 }
2374 assert_eq!(status, StatusCode::OK);
2375 let response: PredictResponse = serde_json::from_slice(&body).unwrap();
2376 let expected = if i % 3 == 0 { 2 } else { 1 };
2377 assert_eq!(response.batch_size, expected);
2378 match response.output_data {
2379 PredictOutputData::Batch { outputs } => assert_eq!(outputs.len(), 2),
2380 PredictOutputData::Single { output } => assert_eq!(output.len(), 10),
2381 }
2382 }
2383 get_metrics(app).await
2384 }
2385
2386 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
2387 async fn test_concurrent_predictions_with_and_without_batching() {
2388 let batched = concurrent_predictions(&ServerConfig::default()).await;
2389 let unbatched = concurrent_predictions(&ServerConfig {
2390 batching: BatchConfig {
2391 max_batch_size: 1,
2392 ..BatchConfig::default()
2393 },
2394 ..ServerConfig::default()
2395 })
2396 .await;
2397 for m in [&batched, &unbatched] {
2398 assert_eq!(m.total_requests, 12);
2400 assert_eq!(m.total_errors, 1);
2401 assert_eq!(m.total_inferences, 11);
2402 assert!((1..=11).contains(&m.total_batches), "{m:?}");
2403 assert_eq!(m.in_flight_inferences, 0);
2404 }
2405 assert_eq!(unbatched.total_batches, 11);
2406 assert_eq!(unbatched.average_batch_size, 15.0 / 11.0);
2407 assert_eq!(unbatched.max_batch_size, 1);
2408 }
2409
2410 #[tokio::test]
2411 async fn test_metrics_counts_busy_and_body_limit_rejections() {
2412 let config = ServerConfig {
2413 max_concurrent_requests: 0,
2414 max_body_bytes: 64,
2415 ..ServerConfig::default()
2416 };
2417 let app = router(dummy_model(), &config).unwrap();
2418 let small = serde_json::json!({ "input": [0.5] }).to_string();
2419 let (status, _) = send(app.clone(), predict_request(small)).await;
2420 assert_eq!(status, StatusCode::BAD_REQUEST);
2421 let big = serde_json::json!({ "input": vec![0.5; 784] }).to_string();
2422 let (status, _) = send(app.clone(), predict_request(big)).await;
2423 assert_eq!(status, StatusCode::PAYLOAD_TOO_LARGE);
2424
2425 let m = get_metrics(app).await;
2426 assert_eq!(m.total_requests, 2);
2427 assert_eq!(m.total_errors, 2);
2428 assert_eq!(m.total_inferences, 0);
2429 }
2430
2431 #[tokio::test]
2432 async fn test_health_check_endpoints() {
2433 for path in ["/healthz", "/health"] {
2434 let request = Request::builder().uri(path).body(Body::empty()).unwrap();
2435 let (status, body) = send(test_app(), request).await;
2436 assert_eq!(status, StatusCode::OK);
2437 let health: HealthResponse = serde_json::from_slice(&body).unwrap();
2438 assert_eq!(health.status, "healthy");
2439 assert!(health.model_loaded);
2440 assert_eq!(health.backend, "cpu");
2441 }
2442 }
2443
2444 #[tokio::test]
2445 async fn test_security_headers_present() {
2446 let request = Request::builder()
2447 .uri("/healthz")
2448 .body(Body::empty())
2449 .unwrap();
2450 let response = test_app().oneshot(request).await.unwrap();
2451 assert_eq!(response.headers()[X_CONTENT_TYPE_OPTIONS], "nosniff");
2452 assert_eq!(response.headers()[X_FRAME_OPTIONS], "DENY");
2453 }
2454
2455 #[tokio::test]
2456 async fn test_cors_open_to_any_origin() {
2457 let preflight = Request::builder()
2459 .uri("/predict")
2460 .method("OPTIONS")
2461 .header(ORIGIN, "https://example.com")
2462 .header(ACCESS_CONTROL_REQUEST_METHOD, "POST")
2463 .header("access-control-request-headers", "content-type")
2464 .body(Body::empty())
2465 .unwrap();
2466 let response = test_app().oneshot(preflight).await.unwrap();
2467 assert_eq!(response.status(), StatusCode::OK);
2468 let headers = response.headers();
2469 assert_eq!(headers[ACCESS_CONTROL_ALLOW_ORIGIN], "*");
2470 assert_eq!(headers[ACCESS_CONTROL_ALLOW_METHODS], "*");
2471 assert_eq!(headers[ACCESS_CONTROL_ALLOW_HEADERS], "*");
2472 assert!(headers.contains_key(REQUEST_ID_HEADER));
2473
2474 let request = Request::builder()
2476 .uri("/healthz")
2477 .header(ORIGIN, "https://example.com")
2478 .body(Body::empty())
2479 .unwrap();
2480 let response = test_app().oneshot(request).await.unwrap();
2481 assert_eq!(response.headers()[ACCESS_CONTROL_ALLOW_ORIGIN], "*");
2482 let exposed = response.headers()[ACCESS_CONTROL_EXPOSE_HEADERS]
2483 .to_str()
2484 .unwrap();
2485 for name in [
2486 BATCH_SIZE_HEADER,
2487 OUTPUT_SHAPE_HEADER,
2488 INFERENCE_TIME_HEADER,
2489 REQUEST_ID_HEADER,
2490 ] {
2491 assert!(exposed.contains(name.as_str()), "{exposed}");
2492 }
2493 }
2494
2495 fn assert_matches_schema(spec: &serde_json::Value, name: &str, value: &serde_json::Value) {
2498 let schema = &spec["components"]["schemas"][name];
2499 let properties = schema["properties"]
2500 .as_object()
2501 .unwrap_or_else(|| panic!("no schema {name}"));
2502 let fields = value
2503 .as_object()
2504 .unwrap_or_else(|| panic!("{name}: not an object"));
2505 for field in fields.keys() {
2506 assert!(
2507 properties.contains_key(field),
2508 "{name}: the server sends `{field}`, the spec does not list it"
2509 );
2510 }
2511 for required in schema["required"].as_array().into_iter().flatten() {
2512 let required = required.as_str().unwrap();
2513 assert!(
2514 fields.contains_key(required),
2515 "{name}: the spec requires `{required}`, the server does not send it"
2516 );
2517 }
2518 }
2519
2520 #[tokio::test]
2522 async fn test_openapi_spec_matches_server() {
2523 let spec: serde_json::Value =
2524 serde_json::from_str(include_str!("../docs/openapi.json")).unwrap();
2525 assert_eq!(spec["info"]["version"], env!("CARGO_PKG_VERSION"));
2526 let paths: std::collections::BTreeSet<&str> = spec["paths"]
2527 .as_object()
2528 .unwrap()
2529 .keys()
2530 .map(String::as_str)
2531 .collect();
2532 let served: std::collections::BTreeSet<&str> = ROUTES
2533 .iter()
2534 .map(|(path, _)| *path)
2535 .chain(LLM_PATHS)
2536 .chain(NERVE_PATHS)
2537 .collect();
2538 assert_eq!(paths, served);
2539
2540 let codes: std::collections::BTreeSet<&str> = Code::ALL
2542 .iter()
2543 .map(|code| code.as_str())
2544 .chain(cfg!(not(feature = "image")).then_some("INVALID_IMAGE"))
2545 .collect();
2546 let documented: std::collections::BTreeSet<&str> =
2547 spec["components"]["schemas"]["ErrorResponse"]["properties"]["code"]["enum"]
2548 .as_array()
2549 .expect("the spec lists the codes")
2550 .iter()
2551 .map(|code| code.as_str().unwrap())
2552 .collect();
2553 assert_eq!(documented, codes);
2554 let api_md = include_str!("../docs/API.md");
2555 for code in &codes {
2556 assert!(
2557 api_md.contains(&format!("| `{code}` |")),
2558 "docs/API.md has no table row for {code}"
2559 );
2560 }
2561
2562 let app = test_app();
2563 let get = |path: &'static str| {
2564 let app = app.clone();
2565 let request = Request::builder().uri(path).body(Body::empty()).unwrap();
2566 async move {
2567 let (status, body) = send(app, request).await;
2568 assert_eq!(status, StatusCode::OK, "{path}");
2569 serde_json::from_slice::<serde_json::Value>(&body).unwrap()
2570 }
2571 };
2572 for path in ["/healthz", "/health"] {
2573 assert_matches_schema(&spec, "HealthResponse", &get(path).await);
2574 }
2575
2576 let single = serde_json::json!({ "input": vec![0.5; 784] }).to_string();
2577 let (status, body) = send(app.clone(), predict_request(single)).await;
2578 assert_eq!(status, StatusCode::OK);
2579 let single: serde_json::Value = serde_json::from_slice(&body).unwrap();
2580 assert_matches_schema(&spec, "PredictResponse", &single);
2581 let batch = serde_json::json!({ "inputs": vec![vec![0.5; 784]; 2] }).to_string();
2582 let (status, body) = send(app.clone(), predict_request(batch)).await;
2583 assert_eq!(status, StatusCode::OK);
2584 let batch: serde_json::Value = serde_json::from_slice(&body).unwrap();
2585 assert_matches_schema(&spec, "PredictResponse", &batch);
2586
2587 let bad = serde_json::json!({ "input": vec![0.5; 3] }).to_string();
2588 let (status, body) = send(app.clone(), predict_request(bad)).await;
2589 assert_eq!(status, StatusCode::BAD_REQUEST);
2590 let error: serde_json::Value = serde_json::from_slice(&body).unwrap();
2591 assert_matches_schema(&spec, "ErrorResponse", &error);
2592
2593 let metrics = get("/metrics").await;
2594 assert_matches_schema(&spec, "MetricsResponse", &metrics);
2595 assert_matches_schema(&spec, "Percentiles", &metrics["request_latency_ms"]);
2596 wait_until_ready(&app).await;
2597 assert_matches_schema(&spec, "ReadinessResponse", &get("/readyz").await);
2598
2599 assert_matches_schema(&spec, "V2ServerMetadata", &get("/v2").await);
2600 let model = get("/v2/models/test_model").await;
2601 assert_matches_schema(&spec, "V2ModelMetadata", &model);
2602 assert_matches_schema(&spec, "V2TensorMetadata", &model["inputs"][0]);
2603 let infer = Request::builder()
2604 .uri("/v2/models/test_model/infer")
2605 .method("POST")
2606 .header(CONTENT_TYPE, "application/json")
2607 .body(Body::from(
2608 serde_json::json!({
2609 "id": "a",
2610 "inputs": [{ "name": "input", "shape": [1, 784], "datatype": "FP32", "data": vec![0.5; 784] }],
2611 })
2612 .to_string(),
2613 ))
2614 .unwrap();
2615 let (status, body) = send(app.clone(), infer).await;
2616 assert_eq!(status, StatusCode::OK);
2617 let infer: serde_json::Value = serde_json::from_slice(&body).unwrap();
2618 assert_matches_schema(&spec, "V2InferResponse", &infer);
2619 assert_matches_schema(&spec, "V2OutputTensor", &infer["outputs"][0]);
2620 let info = get("/model/info").await;
2621 assert_matches_schema(&spec, "ModelInfoResponse", &info);
2622 assert_matches_schema(&spec, "ModelInfo", &info["model_info"]);
2623 assert_matches_schema(&spec, "ModelStats", &info["stats"]);
2624 assert_matches_schema(&spec, "TensorSpec", &info["model_info"]["input_spec"]);
2625 assert_matches_schema(&spec, "TensorInfo", &info["model_info"]["inputs"][0]);
2626 assert_matches_schema(&spec, "ModelInfoResponse", &get("/models/test_model").await);
2627 assert_matches_schema(&spec, "ModelMetrics", &metrics["models"][0]);
2628 let models = get("/models").await;
2629 assert_matches_schema(&spec, "ModelsResponse", &models);
2630 assert_matches_schema(&spec, "ModelSummary", &models["models"][0]);
2631
2632 let app = multi_app(Some(TOKEN));
2633 let (status, body) = send(app.clone(), reload_request("a", Some(TOKEN), "")).await;
2634 assert_eq!(status, StatusCode::OK);
2635 let reload: serde_json::Value = serde_json::from_slice(&body).unwrap();
2636 assert_matches_schema(&spec, "ReloadResponse", &reload);
2637 }
2638
2639 const TOKEN: &str = "0123456789abcdef-token";
2640 const API_KEY: &str = "api-key-0123456789";
2641
2642 fn guarded_app(api_keys: &[&str], rate_limit: Option<ClientRateLimit>) -> Router {
2643 let config = ServerConfig {
2644 api_keys: api_keys.iter().map(|k| k.to_string()).collect(),
2645 rate_limit,
2646 admin_token: Some(TOKEN.to_string()),
2647 ..ServerConfig::default()
2648 };
2649 router(dummy_model(), &config).unwrap()
2650 }
2651
2652 fn with_header(mut request: Request<Body>, name: &'static str, value: &str) -> Request<Body> {
2653 request
2654 .headers_mut()
2655 .insert(name, HeaderValue::from_str(value).unwrap());
2656 request
2657 }
2658
2659 fn ok_predict() -> Request<Body> {
2660 predict_request(serde_json::json!({ "input": vec![0.5; 784] }).to_string())
2661 }
2662
2663 #[tokio::test]
2664 async fn test_api_keys() {
2665 let app = guarded_app(&[API_KEY, "another-key-0123456789"], None);
2666 let response = app.clone().oneshot(ok_predict()).await.unwrap();
2667 assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
2668 assert_eq!(response.headers()["www-authenticate"], "Bearer");
2669 let wrong = with_header(ok_predict(), "authorization", "Bearer api-key-012345678");
2670 assert_eq!(send(app.clone(), wrong).await.0, StatusCode::UNAUTHORIZED);
2671
2672 let bearer = with_header(ok_predict(), "authorization", &format!("Bearer {API_KEY}"));
2673 assert_eq!(send(app.clone(), bearer).await.0, StatusCode::OK);
2674 let header = with_header(ok_predict(), "x-api-key", "another-key-0123456789");
2675 assert_eq!(send(app.clone(), header).await.0, StatusCode::OK);
2676
2677 for path in ["/healthz", "/health", "/readyz", "/v2/health/live"] {
2679 let request = Request::builder().uri(path).body(Body::empty()).unwrap();
2680 let status = send(app.clone(), request).await.0;
2681 assert_ne!(status, StatusCode::UNAUTHORIZED, "{path}");
2682 }
2683 for path in [
2684 "/metrics",
2685 "/model/info",
2686 "/models",
2687 "/v2",
2688 "/v2/models/test_model",
2689 ] {
2690 let request = Request::builder().uri(path).body(Body::empty()).unwrap();
2691 assert_eq!(
2692 send(app.clone(), request).await.0,
2693 StatusCode::UNAUTHORIZED,
2694 "{path}"
2695 );
2696 }
2697 let reload = Request::builder()
2699 .uri("/admin/models/test_model/reload")
2700 .method("POST")
2701 .header("authorization", format!("Bearer {TOKEN}"))
2702 .body(Body::empty())
2703 .unwrap();
2704 assert_ne!(send(app.clone(), reload).await.0, StatusCode::UNAUTHORIZED);
2705
2706 let request = Request::builder()
2707 .uri("/metrics")
2708 .header("x-api-key", API_KEY)
2709 .body(Body::empty())
2710 .unwrap();
2711 let (_, body) = send(app, request).await;
2712 let metrics: MetricsResponse = serde_json::from_slice(&body).unwrap();
2713 assert_eq!(metrics.rejected_unauthorized, 7);
2714 }
2715
2716 #[tokio::test]
2717 async fn test_rate_limit() {
2718 let limit = ClientRateLimit {
2719 per_second: 0.001,
2720 burst: 2,
2721 };
2722 let app = guarded_app(&[], Some(limit));
2723 for _ in 0..2 {
2724 assert_eq!(send(app.clone(), ok_predict()).await.0, StatusCode::OK);
2725 }
2726 let response = app.clone().oneshot(ok_predict()).await.unwrap();
2727 assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
2728 let retry: u64 = response.headers()["retry-after"]
2730 .to_str()
2731 .unwrap()
2732 .parse()
2733 .unwrap();
2734 assert!((990..=1000).contains(&retry), "{retry}");
2735 let body = axum::body::to_bytes(response.into_body(), usize::MAX)
2736 .await
2737 .unwrap();
2738 let error: serde_json::Value = serde_json::from_slice(&body).unwrap();
2739 assert_eq!(error["code"], "RATE_LIMITED");
2740 let request = Request::builder()
2742 .uri("/healthz")
2743 .body(Body::empty())
2744 .unwrap();
2745 assert_eq!(send(app.clone(), request).await.0, StatusCode::OK);
2746
2747 let app = guarded_app(&[API_KEY, "another-key-0123456789"], Some(limit));
2749 let auth = |key: &str| with_header(ok_predict(), "x-api-key", key);
2750 for _ in 0..2 {
2751 assert_eq!(send(app.clone(), auth(API_KEY)).await.0, StatusCode::OK);
2752 }
2753 assert_eq!(
2754 send(app.clone(), auth(API_KEY)).await.0,
2755 StatusCode::TOO_MANY_REQUESTS
2756 );
2757 assert_eq!(
2758 send(app.clone(), auth("another-key-0123456789")).await.0,
2759 StatusCode::OK
2760 );
2761 }
2762
2763 fn named_model(name: &str) -> Model {
2764 let dummy = DummyModel::new(name.to_string(), vec![784], vec![10]);
2765 Model::new(Box::new(dummy), "dummy", 0)
2766 }
2767
2768 fn multi_app(admin_token: Option<&str>) -> Router {
2771 let models = ["a", "b"]
2772 .map(|name| {
2773 let name = name.to_string();
2774 ServedModel {
2775 model: named_model(&name),
2776 reload: Some(Arc::new(
2777 move |weights: Option<&std::path::Path>| match weights
2778 .and_then(|w| w.to_str())
2779 {
2780 Some("bad") => {
2781 Err(ModelError::InvalidFormat("bad weights".to_string()).into())
2782 }
2783 Some("renamed") => Ok(named_model("other")),
2784 _ => Ok(named_model(&name)),
2785 },
2786 )),
2787 }
2788 })
2789 .into();
2790 let config = ServerConfig {
2791 admin_token: admin_token.map(str::to_string),
2792 ..ServerConfig::default()
2793 };
2794 router_with_models(models, &config).unwrap()
2795 }
2796
2797 fn reload_request(model: &str, token: Option<&str>, body: &str) -> Request<Body> {
2798 let mut request = Request::builder()
2799 .uri(format!("/admin/models/{model}/reload"))
2800 .method("POST")
2801 .header(CONTENT_TYPE, "application/json");
2802 if let Some(token) = token {
2803 request = request.header("authorization", format!("Bearer {token}"));
2804 }
2805 request.body(Body::from(body.to_string())).unwrap()
2806 }
2807
2808 fn named_predict_request(model: &str) -> Request<Body> {
2809 Request::builder()
2810 .uri(format!("/models/{model}/predict"))
2811 .method("POST")
2812 .header(CONTENT_TYPE, "application/json")
2813 .body(Body::from(
2814 serde_json::json!({ "input": vec![0.5; 784] }).to_string(),
2815 ))
2816 .unwrap()
2817 }
2818
2819 async fn get_json(app: &Router, path: &str) -> (StatusCode, serde_json::Value) {
2820 let request = Request::builder().uri(path).body(Body::empty()).unwrap();
2821 let (status, body) = send(app.clone(), request).await;
2822 (status, serde_json::from_slice(&body).unwrap_or_default())
2823 }
2824
2825 #[tokio::test]
2826 async fn test_several_models_side_by_side() {
2827 let app = multi_app(None);
2828 let ok = serde_json::json!({ "input": vec![0.5; 784] }).to_string();
2829 let (status, _) = send(app.clone(), predict_request(ok)).await;
2831 assert_eq!(status, StatusCode::OK);
2832 for _ in 0..2 {
2833 let (status, _) = send(app.clone(), named_predict_request("b")).await;
2834 assert_eq!(status, StatusCode::OK);
2835 }
2836 let (status, body) = send(app.clone(), named_predict_request("c")).await;
2837 assert_eq!(status, StatusCode::NOT_FOUND);
2838 let error: serde_json::Value = serde_json::from_slice(&body).unwrap();
2839 assert_eq!(error["code"], "MODEL_NOT_FOUND");
2840
2841 let (status, models) = get_json(&app, "/models").await;
2842 assert_eq!(status, StatusCode::OK);
2843 assert_eq!(models["default_model"], "a");
2844 assert_eq!(models["models"].as_array().unwrap().len(), 2);
2845 assert_eq!(models["models"][1]["name"], "b");
2846 assert_eq!(models["models"][1]["reloadable"], true);
2847 let (status, info) = get_json(&app, "/models/b").await;
2848 assert_eq!(status, StatusCode::OK);
2849 assert_eq!(info["model_info"]["name"], "b");
2850 assert_eq!(info["stats"]["inference_count"], 2);
2851 let (status, _) = get_json(&app, "/models/c").await;
2852 assert_eq!(status, StatusCode::NOT_FOUND);
2853
2854 let infer = |model: &str| {
2856 Request::builder()
2857 .uri(format!("/v2/models/{model}/infer"))
2858 .method("POST")
2859 .header(CONTENT_TYPE, "application/json")
2860 .body(Body::from(
2861 serde_json::json!({
2862 "inputs": [{ "name": "input", "shape": [1, 784], "datatype": "FP32", "data": vec![0.5; 784] }],
2863 })
2864 .to_string(),
2865 ))
2866 .unwrap()
2867 };
2868 let (status, body) = send(app.clone(), infer("b")).await;
2869 assert_eq!(status, StatusCode::OK);
2870 let response: serde_json::Value = serde_json::from_slice(&body).unwrap();
2871 assert_eq!(response["model_name"], "b");
2872 let (status, body) = send(app.clone(), infer("c")).await;
2873 assert_eq!(status, StatusCode::NOT_FOUND);
2874 let error = String::from_utf8_lossy(&body);
2875 assert!(error.contains("a, b"), "{error}");
2876
2877 let metrics = get_metrics(app.clone()).await;
2878 assert_eq!(metrics.total_inferences, 4);
2879 let per_model: Vec<_> = metrics
2880 .models
2881 .iter()
2882 .map(|m| (m.name.as_str(), m.inference_count))
2883 .collect();
2884 assert_eq!(per_model, [("a", 1), ("b", 3)]);
2885
2886 let request = Request::builder()
2887 .uri("/metrics")
2888 .header(ACCEPT, "text/plain")
2889 .body(Body::empty())
2890 .unwrap();
2891 let (_, body) = send(app.clone(), request).await;
2892 let text = String::from_utf8(body.to_vec()).unwrap();
2893 assert!(
2894 text.contains("furnace_inferences_total{model=\"b\"} 3\n"),
2895 "{text}"
2896 );
2897 assert!(
2898 text.contains("furnace_inference_duration_seconds_count{model=\"a\"} 1\n"),
2899 "{text}"
2900 );
2901 assert!(
2902 text.contains("furnace_model_info{model=\"a\",backend="),
2903 "{text}"
2904 );
2905 assert_eq!(text.matches("# TYPE furnace_inferences_total ").count(), 1);
2907 assert_eq!(
2908 text.matches("# TYPE furnace_inference_duration_seconds ")
2909 .count(),
2910 1
2911 );
2912 }
2913
2914 #[tokio::test]
2915 async fn test_reload_needs_the_admin_token() {
2916 let (status, body) = send(multi_app(None), reload_request("a", Some(TOKEN), "")).await;
2917 assert_eq!(status, StatusCode::NOT_FOUND);
2918 assert!(String::from_utf8_lossy(&body).contains("ADMIN_API_DISABLED"));
2919
2920 let app = multi_app(Some(TOKEN));
2921 for token in [None, Some("wrong"), Some("0123456789abcdef-tokeN")] {
2922 let response = app
2923 .clone()
2924 .oneshot(reload_request("a", token, ""))
2925 .await
2926 .unwrap();
2927 assert_eq!(response.status(), StatusCode::UNAUTHORIZED, "{token:?}");
2928 assert_eq!(response.headers()["www-authenticate"], "Bearer");
2929 }
2930 let (status, _) = send(app.clone(), reload_request("c", Some(TOKEN), "")).await;
2931 assert_eq!(status, StatusCode::NOT_FOUND);
2932 }
2933
2934 #[tokio::test]
2935 async fn a_model_without_a_reloader_is_a_409() {
2936 let config = ServerConfig {
2937 admin_token: Some(TOKEN.to_string()),
2938 ..ServerConfig::default()
2939 };
2940 let app = router(dummy_model(), &config).unwrap();
2941 let (status, body) = send(app, reload_request("test_model", Some(TOKEN), "")).await;
2942 assert_eq!(status, StatusCode::CONFLICT);
2943 let error: ErrorResponse = serde_json::from_slice(&body).unwrap();
2944 assert_eq!(error.code, "NOT_RELOADABLE");
2945 }
2946
2947 #[tokio::test]
2948 async fn a_taken_port_says_what_to_do() {
2949 let taken = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
2950 let port = taken.local_addr().unwrap().port();
2951 let refused = Server::new()
2952 .port(port)
2953 .model(dummy_model())
2954 .serve()
2955 .await
2956 .unwrap_err()
2957 .to_string();
2958 assert!(refused.contains("already in use"), "{refused}");
2959 assert!(refused.contains("Server::port"), "{refused}");
2960 }
2961
2962 #[test]
2963 fn the_shortcuts_set_the_config() {
2964 let server = Server::new()
2965 .max_batch_size(8)
2966 .request_timeout(Duration::from_millis(250));
2967 assert_eq!(server.config.batching.max_batch_size, 8);
2968 assert_eq!(
2969 server.config.request_timeout,
2970 Some(Duration::from_millis(250))
2971 );
2972 }
2973
2974 #[tokio::test]
2975 async fn an_empty_server_is_an_error() {
2976 assert!(Server::new().router().is_err());
2977 assert!(router_with_models(Vec::new(), &ServerConfig::default()).is_err());
2978 let refused = Server::new().serve().await.unwrap_err().to_string();
2980 assert!(refused.contains("Server::model"), "{refused}");
2981 assert!(refused.contains("Server::llm"), "{refused}");
2982 assert!(refused.contains("Server::nerve"), "{refused}");
2983 }
2984
2985 #[tokio::test]
2986 async fn test_reload_swaps_the_model_and_keeps_its_stats() {
2987 let app = multi_app(Some(TOKEN));
2988 let (status, _) = send(app.clone(), named_predict_request("b")).await;
2989 assert_eq!(status, StatusCode::OK);
2990
2991 let (status, body) = send(app.clone(), reload_request("b", Some(TOKEN), "")).await;
2992 assert_eq!(status, StatusCode::OK, "{}", String::from_utf8_lossy(&body));
2993 let reload: ReloadResponse = serde_json::from_slice(&body).unwrap();
2994 assert_eq!(reload.model, "b");
2995 assert!(reload.reload_ms >= reload.warm_up_ms);
2996
2997 let (_, models) = get_json(&app, "/models").await;
2999 assert_eq!(models["models"][1]["ready"], true);
3000 let (status, _) = send(app.clone(), named_predict_request("b")).await;
3001 assert_eq!(status, StatusCode::OK);
3002 let (_, info) = get_json(&app, "/models/b").await;
3003 assert_eq!(info["stats"]["inference_count"], 2);
3004
3005 for (body, code) in [
3007 (r#"{"weights": "bad"}"#, StatusCode::UNPROCESSABLE_ENTITY),
3008 (
3009 r#"{"weights": "renamed"}"#,
3010 StatusCode::UNPROCESSABLE_ENTITY,
3011 ),
3012 ("not json", StatusCode::BAD_REQUEST),
3013 ] {
3014 let (status, _) = send(app.clone(), reload_request("b", Some(TOKEN), body)).await;
3015 assert_eq!(status, code, "{body}");
3016 let (status, _) = send(app.clone(), named_predict_request("b")).await;
3017 assert_eq!(status, StatusCode::OK);
3018 }
3019 }
3020
3021 #[tokio::test]
3022 async fn test_requests_during_a_reload_succeed() {
3023 let app = multi_app(Some(TOKEN));
3024 let requests: Vec<_> = (0..64)
3025 .map(|_| tokio::spawn(send(app.clone(), named_predict_request("a"))))
3026 .collect();
3027 let reloads: Vec<_> = (0..4)
3028 .map(|_| tokio::spawn(send(app.clone(), reload_request("a", Some(TOKEN), ""))))
3029 .collect();
3030 for request in requests {
3031 assert_eq!(request.await.unwrap().0, StatusCode::OK);
3032 }
3033 let mut reloaded = 0;
3035 for reload in reloads {
3036 match reload.await.unwrap().0 {
3037 StatusCode::OK => reloaded += 1,
3038 status => assert_eq!(status, StatusCode::CONFLICT),
3039 }
3040 }
3041 assert!(reloaded >= 1);
3042 let metrics = get_metrics(app).await;
3043 assert_eq!(metrics.models[0].inference_count, 64);
3044 }
3045
3046 async fn wait_until_ready(app: &Router) {
3048 let deadline = Instant::now() + Duration::from_secs(10);
3049 loop {
3050 let request = Request::builder()
3051 .uri("/readyz")
3052 .body(Body::empty())
3053 .unwrap();
3054 let (status, _) = send(app.clone(), request).await;
3055 if status == StatusCode::OK {
3056 return;
3057 }
3058 assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE);
3059 assert!(Instant::now() < deadline, "never became ready");
3060 tokio::time::sleep(Duration::from_millis(5)).await;
3061 }
3062 }
3063
3064 #[tokio::test]
3065 async fn test_readiness_after_warm_up() {
3066 let app = test_app();
3067 wait_until_ready(&app).await;
3068 let metrics = get_metrics(app).await;
3070 assert_eq!(metrics.total_inferences, 0);
3071 assert!(metrics.ready);
3072 }
3073
3074 #[tokio::test]
3075 async fn test_metrics_latency_percentiles_and_prometheus_format() {
3076 let app = test_app();
3077 let ok = serde_json::json!({ "input": vec![0.5; 784] }).to_string();
3078 for _ in 0..3 {
3079 let (status, _) = send(app.clone(), predict_request(ok.clone())).await;
3080 assert_eq!(status, StatusCode::OK);
3081 }
3082 let m = get_metrics(app.clone()).await;
3083 assert!(m.request_latency_ms.p50.is_some());
3084 assert!(m.inference_latency_ms.p99.is_some());
3085
3086 let request = Request::builder()
3088 .uri("/metrics")
3089 .header(
3090 ACCEPT,
3091 "application/openmetrics-text;version=1.0.0;q=0.5,text/plain;version=0.0.4;q=0.4,*/*;q=0.1",
3092 )
3093 .body(Body::empty())
3094 .unwrap();
3095 let response = app.clone().oneshot(request).await.unwrap();
3096 assert_eq!(response.status(), StatusCode::OK);
3097 assert!(
3098 response.headers()[CONTENT_TYPE]
3099 .to_str()
3100 .unwrap()
3101 .starts_with("text/plain")
3102 );
3103 let body = axum::body::to_bytes(response.into_body(), usize::MAX)
3104 .await
3105 .unwrap();
3106 let text = String::from_utf8(body.to_vec()).unwrap();
3107 assert!(text.contains("furnace_requests_total 3\n"), "{text}");
3108 assert!(
3109 text.contains("furnace_inferences_total{model=\"test_model\"} 3\n"),
3110 "{text}"
3111 );
3112 assert!(
3113 text.contains("furnace_request_duration_seconds_count 3\n"),
3114 "{text}"
3115 );
3116 assert!(text.contains("furnace_build_info{version="), "{text}");
3117
3118 let request = Request::builder()
3120 .uri("/metrics")
3121 .header(ACCEPT, "*/*")
3122 .body(Body::empty())
3123 .unwrap();
3124 let response = app.oneshot(request).await.unwrap();
3125 assert!(
3126 response.headers()[CONTENT_TYPE]
3127 .to_str()
3128 .unwrap()
3129 .starts_with("application/json")
3130 );
3131 }
3132
3133 #[tokio::test]
3134 async fn test_request_id_echoed_or_generated() {
3135 let with_id = |id: &str| {
3136 Request::builder()
3137 .uri("/healthz")
3138 .header(REQUEST_ID_HEADER, id)
3139 .body(Body::empty())
3140 .unwrap()
3141 };
3142 let response = test_app().oneshot(with_id("trace-42")).await.unwrap();
3144 assert_eq!(response.headers()[REQUEST_ID_HEADER], "trace-42");
3145
3146 let plain = Request::builder()
3148 .uri("/healthz")
3149 .body(Body::empty())
3150 .unwrap();
3151 for request in [plain, with_id(&"x".repeat(129)), with_id("a b")] {
3152 let response = test_app().oneshot(request).await.unwrap();
3153 let id = response.headers()[REQUEST_ID_HEADER].to_str().unwrap();
3154 let groups: Vec<&str> = id.split('-').collect();
3156 let lengths: Vec<usize> = groups.iter().map(|g| g.len()).collect();
3157 assert_eq!(lengths, [8, 4, 4, 4, 12], "{id}");
3158 assert!(
3159 id.chars()
3160 .all(|c| c == '-' || c.is_ascii_digit() || ('a'..='f').contains(&c)),
3161 "{id}"
3162 );
3163 assert!(groups[2].starts_with('4'), "{id}");
3164 assert!(groups[3].starts_with(['8', '9', 'a', 'b']), "{id}");
3165 }
3166
3167 let ids: std::collections::HashSet<_> = (0..1000).map(|_| new_request_id()).collect();
3169 assert_eq!(ids.len(), 1000);
3170
3171 let request = Request::builder()
3173 .uri("/predict")
3174 .method("POST")
3175 .header("content-type", "application/json")
3176 .header(REQUEST_ID_HEADER, "bad-1")
3177 .body(Body::from("{"))
3178 .unwrap();
3179 let response = test_app().oneshot(request).await.unwrap();
3180 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
3181 assert_eq!(response.headers()[REQUEST_ID_HEADER], "bad-1");
3182 }
3183
3184 #[tokio::test]
3185 async fn test_model_info_endpoint() {
3186 let request = Request::builder()
3187 .uri("/model/info")
3188 .body(Body::empty())
3189 .unwrap();
3190 let (status, body) = send(test_app(), request).await;
3191 assert_eq!(status, StatusCode::OK);
3192 let info: ModelInfoResponse = serde_json::from_slice(&body).unwrap();
3193 assert_eq!(info.model_info.input_spec.shape, vec![784]);
3194 assert_eq!(info.model_info.output_spec.shape, vec![10]);
3195 assert_eq!(info.model_info.model_type, "dummy");
3196 }
3197
3198 #[tokio::test]
3199 async fn test_predict_single() {
3200 let body = serde_json::json!({ "input": vec![0.5; 784] }).to_string();
3201 let (status, body) = send(test_app(), predict_request(body)).await;
3202 assert_eq!(status, StatusCode::OK);
3203 let response: PredictResponse = serde_json::from_slice(&body).unwrap();
3204 assert_eq!(response.status, "success");
3205 assert_eq!(response.batch_size, 1);
3206 match response.output_data {
3207 PredictOutputData::Single { output } => assert_eq!(output.len(), 10),
3208 _ => panic!("Expected single output"),
3209 }
3210 }
3211
3212 #[tokio::test]
3213 async fn test_predict_batch() {
3214 let body = serde_json::json!({ "inputs": vec![vec![0.5; 784]; 3] }).to_string();
3215 let (status, body) = send(test_app(), predict_request(body)).await;
3216 assert_eq!(status, StatusCode::OK);
3217 let response: PredictResponse = serde_json::from_slice(&body).unwrap();
3218 assert_eq!(response.batch_size, 3);
3219 match response.output_data {
3220 PredictOutputData::Batch { outputs } => assert_eq!(outputs.len(), 3),
3221 _ => panic!("Expected batch output"),
3222 }
3223 }
3224
3225 #[tokio::test]
3226 async fn test_predict_accepts_body_larger_than_axum_default() {
3227 let dummy = DummyModel::new("big".to_string(), vec![3, 224, 224], vec![1000]);
3229 let app = router(
3230 Model::new(Box::new(dummy), "dummy", 0),
3231 &ServerConfig::default(),
3232 )
3233 .unwrap();
3234 let body =
3235 serde_json::json!({ "inputs": vec![vec![0.123_456_7f32; 150_528]; 2] }).to_string();
3236 assert!(body.len() > 2 * 1024 * 1024);
3237 let (status, _) = send(app, predict_request(body)).await;
3238 assert_eq!(status, StatusCode::OK);
3239 }
3240
3241 #[tokio::test]
3242 async fn test_predict_body_limit_enforced() {
3243 let config = ServerConfig {
3244 max_body_bytes: 1024,
3245 ..ServerConfig::default()
3246 };
3247 let app = router(dummy_model(), &config).unwrap();
3248 let body = serde_json::json!({ "input": vec![0.5; 784] }).to_string();
3249 let (status, _) = send(app, predict_request(body)).await;
3250 assert_eq!(status, StatusCode::PAYLOAD_TOO_LARGE);
3251 }
3252
3253 #[derive(Debug)]
3256 struct Misbehaving {
3257 dummy: DummyModel,
3258 panics: bool,
3259 delay: Duration,
3260 }
3261
3262 impl crate::TensorModel for Misbehaving {
3263 fn name(&self) -> &str {
3264 self.dummy.name()
3265 }
3266 fn backend(&self) -> &str {
3267 self.dummy.backend()
3268 }
3269 fn inputs(&self) -> Vec<crate::IoSpec> {
3270 self.dummy.inputs()
3271 }
3272 fn outputs(&self) -> Vec<crate::IoSpec> {
3273 self.dummy.outputs()
3274 }
3275 fn run(&self, inputs: Vec<HostTensor>) -> Result<Vec<HostTensor>> {
3276 let zeros = matches!(&inputs[0].values, Values::F32(v) if v.iter().all(|x| *x == 0.0));
3277 if !zeros {
3278 assert!(!self.panics, "the model broke");
3279 std::thread::sleep(self.delay);
3280 }
3281 self.dummy.run(inputs)
3282 }
3283 }
3284
3285 fn misbehaving_app(panics: bool, delay: Duration, config: ServerConfig) -> Router {
3286 let dummy = DummyModel::new("test_model".to_string(), vec![784], vec![10]);
3287 let model = Model::new(
3288 Box::new(Misbehaving {
3289 dummy,
3290 panics,
3291 delay,
3292 }),
3293 "dummy",
3294 0,
3295 );
3296 router(model, &config).unwrap()
3297 }
3298
3299 #[derive(Debug, Default)]
3301 struct Gate {
3302 open: std::sync::Mutex<bool>,
3303 opened: std::sync::Condvar,
3304 }
3305
3306 impl Gate {
3307 fn open(&self) {
3308 *self.open.lock().unwrap() = true;
3309 self.opened.notify_all();
3310 }
3311
3312 fn wait(&self) {
3313 let mut open = self.open.lock().unwrap();
3314 while !*open {
3315 open = self.opened.wait(open).unwrap();
3316 }
3317 }
3318 }
3319
3320 #[derive(Debug)]
3324 struct Gated {
3325 dummy: DummyModel,
3326 warm_up: Arc<Gate>,
3327 inference: Arc<Gate>,
3328 waiting: Arc<AtomicU64>,
3329 }
3330
3331 impl crate::TensorModel for Gated {
3332 fn name(&self) -> &str {
3333 self.dummy.name()
3334 }
3335 fn backend(&self) -> &str {
3336 self.dummy.backend()
3337 }
3338 fn inputs(&self) -> Vec<crate::IoSpec> {
3339 self.dummy.inputs()
3340 }
3341 fn outputs(&self) -> Vec<crate::IoSpec> {
3342 self.dummy.outputs()
3343 }
3344 fn run(&self, inputs: Vec<HostTensor>) -> Result<Vec<HostTensor>> {
3345 let zeros = matches!(&inputs[0].values, Values::F32(v) if v.iter().all(|x| *x == 0.0));
3346 if zeros {
3347 self.warm_up.wait();
3348 } else {
3349 self.waiting.fetch_add(1, Ordering::SeqCst);
3350 self.inference.wait();
3351 }
3352 self.dummy.run(inputs)
3353 }
3354 }
3355
3356 struct GatedServer {
3357 state: AppState,
3358 app: Router,
3359 warm_up: Arc<Gate>,
3360 inference: Arc<Gate>,
3361 waiting: Arc<AtomicU64>,
3362 }
3363
3364 fn gated_server(config: ServerConfig) -> GatedServer {
3365 let warm_up = Arc::new(Gate::default());
3366 let inference = Arc::new(Gate::default());
3367 let waiting = Arc::new(AtomicU64::new(0));
3368 let model = Model::new(
3369 Box::new(Gated {
3370 dummy: DummyModel::new("test_model".to_string(), vec![784], vec![10]),
3371 warm_up: warm_up.clone(),
3372 inference: inference.clone(),
3373 waiting: waiting.clone(),
3374 }),
3375 "dummy",
3376 0,
3377 );
3378 let state = server_state(vec![ServedModel::from(model)].into(), &config).unwrap();
3379 GatedServer {
3380 app: router_with_state(state.clone(), &config),
3381 state,
3382 warm_up,
3383 inference,
3384 waiting,
3385 }
3386 }
3387
3388 async fn status_of(app: &Router, path: &str) -> StatusCode {
3389 let request = Request::builder().uri(path).body(Body::empty()).unwrap();
3390 send(app.clone(), request).await.0
3391 }
3392
3393 #[tokio::test]
3395 async fn readiness_and_metadata_follow_the_warm_up_on_every_protocol() {
3396 let server = gated_server(ServerConfig::default());
3397 let app = &server.app;
3398 assert!(!server.state.ready());
3399 let probes = [
3400 "/readyz",
3401 "/v2/health/ready",
3402 "/v2/models/test_model/ready",
3403 "/v2/models/test_model/versions/1/ready",
3404 ];
3405 for path in probes {
3406 assert_eq!(
3407 status_of(app, path).await,
3408 StatusCode::SERVICE_UNAVAILABLE,
3409 "{path}"
3410 );
3411 }
3412 assert_eq!(status_of(app, "/v2/health/live").await, StatusCode::OK);
3413 assert_eq!(
3414 status_of(app, "/v2/models/test_model/versions/2/ready").await,
3415 StatusCode::NOT_FOUND
3416 );
3417 #[cfg(feature = "grpc")]
3418 let mut grpc = grpc::tests::client_for(server.state.clone()).await;
3419 #[cfg(feature = "grpc")]
3420 {
3421 use grpc::pb;
3422 let ready = grpc.server_ready(pb::ServerReadyRequest {}).await.unwrap();
3423 assert!(!ready.into_inner().ready);
3424 let model = pb::ModelReadyRequest {
3425 name: "test_model".to_string(),
3426 version: String::new(),
3427 };
3428 assert!(!grpc.model_ready(model).await.unwrap().into_inner().ready);
3429 let metadata = grpc
3430 .server_metadata(pb::ServerMetadataRequest {})
3431 .await
3432 .unwrap()
3433 .into_inner();
3434 assert_eq!(metadata.name, "furnace");
3435 assert_eq!(metadata.version, env!("CARGO_PKG_VERSION"));
3436 }
3437
3438 server.warm_up.open();
3439 wait_until_ready(app).await;
3440 for path in probes {
3441 assert_eq!(status_of(app, path).await, StatusCode::OK, "{path}");
3442 }
3443 let (status, body) = get_json(app, "/v2").await;
3444 assert_eq!(status, StatusCode::OK);
3445 assert_eq!(body["name"], "furnace");
3446 assert_eq!(body["version"], env!("CARGO_PKG_VERSION"));
3447 let (status, body) = get_json(app, "/v2/models/test_model/versions/1").await;
3448 assert_eq!(status, StatusCode::OK);
3449 assert_eq!(body["inputs"][0]["shape"], serde_json::json!([-1, 784]));
3450 let (status, body) = get_json(app, "/v2/models/other").await;
3451 assert_eq!(status, StatusCode::NOT_FOUND);
3452 assert_eq!(body["code"], "MODEL_NOT_FOUND");
3453 #[cfg(feature = "grpc")]
3454 {
3455 use grpc::pb;
3456 let ready = grpc.server_ready(pb::ServerReadyRequest {}).await.unwrap();
3457 assert!(ready.into_inner().ready);
3458 let model = pb::ModelReadyRequest {
3459 name: "test_model".to_string(),
3460 version: "1".to_string(),
3461 };
3462 assert!(grpc.model_ready(model).await.unwrap().into_inner().ready);
3463 let unknown = pb::ModelReadyRequest {
3464 name: "other".to_string(),
3465 version: String::new(),
3466 };
3467 let status = grpc.model_ready(unknown).await.unwrap_err();
3468 assert_eq!(status.code(), tonic::Code::NotFound);
3469 }
3470 }
3471
3472 #[tokio::test]
3475 async fn a_saturated_server_refuses_on_every_protocol() {
3476 let server = gated_server(ServerConfig {
3477 max_concurrent_requests: 1,
3478 ..ServerConfig::default()
3479 });
3480 server.warm_up.open();
3481 let app = server.app.clone();
3482 let held = tokio::spawn(async move {
3483 let body = serde_json::json!({ "input": vec![0.5; 784] }).to_string();
3484 send(app, predict_request(body)).await.0
3485 });
3486 let deadline = Instant::now() + Duration::from_secs(10);
3487 while server.waiting.load(Ordering::SeqCst) == 0 {
3488 assert!(
3489 Instant::now() < deadline,
3490 "the first request never reached the model"
3491 );
3492 tokio::time::sleep(Duration::from_millis(1)).await;
3493 }
3494
3495 let body = serde_json::json!({ "input": vec![0.5; 784] }).to_string();
3496 let (status, body) = send(server.app.clone(), predict_request(body)).await;
3497 assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE);
3498 let error: ErrorResponse = serde_json::from_slice(&body).unwrap();
3499 assert_eq!(error.code, "SERVER_BUSY");
3500 let (status, body) = send(server.app.clone(), v2_request(0.5)).await;
3501 assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE);
3502 let error: ErrorResponse = serde_json::from_slice(&body).unwrap();
3503 assert_eq!(error.code, "SERVER_BUSY");
3504 let busy = 2 + u64::from(cfg!(feature = "grpc"));
3506 #[cfg(feature = "grpc")]
3507 {
3508 let mut grpc = grpc::tests::client_for(server.state.clone()).await;
3509 let status = grpc
3510 .model_infer(grpc::tests::request(
3511 vec![1, 784],
3512 Some(vec![0.5; 784]),
3513 None,
3514 ))
3515 .await
3516 .unwrap_err();
3517 assert_eq!(status.code(), tonic::Code::ResourceExhausted);
3518 }
3519
3520 server.inference.open();
3521 assert_eq!(held.await.unwrap(), StatusCode::OK);
3522 let m = get_metrics(server.app.clone()).await;
3523 assert_eq!(m.rejected_busy, busy);
3524 assert_eq!(m.total_requests, busy + 1);
3525 assert_eq!(m.total_errors, busy);
3526 assert_eq!(m.in_flight_inferences, 0);
3527 }
3528
3529 fn v2_request(value: f32) -> Request<Body> {
3530 Request::builder()
3531 .uri("/v2/models/test_model/infer")
3532 .method("POST")
3533 .header("content-type", "application/json")
3534 .body(Body::from(
3535 serde_json::json!({
3536 "inputs": [{ "name": "input", "shape": [1, 784], "datatype": "FP32", "data": vec![value; 784] }],
3537 })
3538 .to_string(),
3539 ))
3540 .unwrap()
3541 }
3542
3543 #[tokio::test]
3544 async fn a_model_past_the_request_timeout_is_a_504() {
3545 let config = ServerConfig {
3546 request_timeout: Some(Duration::from_millis(50)),
3547 ..ServerConfig::default()
3548 };
3549 let app = misbehaving_app(false, Duration::from_millis(500), config);
3550 let body = serde_json::json!({ "input": vec![0.5; 784] }).to_string();
3551 let (status, body) = send(app.clone(), predict_request(body)).await;
3552 assert_eq!(status, StatusCode::GATEWAY_TIMEOUT);
3553 assert!(String::from_utf8_lossy(&body).contains("INFERENCE_TIMEOUT"));
3554 let (status, body) = send(app, v2_request(0.5)).await;
3555 assert_eq!(status, StatusCode::GATEWAY_TIMEOUT);
3556 let error: ErrorResponse = serde_json::from_slice(&body).unwrap();
3558 assert_eq!(error.code, "INFERENCE_TIMEOUT");
3559 }
3560
3561 #[tokio::test]
3562 async fn a_panicking_model_is_a_500_and_the_server_goes_on() {
3563 let batching = ServerConfig {
3564 batching: crate::batching::BatchConfig {
3565 max_batch_size: 8,
3566 ..crate::batching::BatchConfig::default()
3567 },
3568 ..ServerConfig::default()
3569 };
3570 for config in [ServerConfig::default(), batching] {
3571 let app = misbehaving_app(true, Duration::ZERO, config);
3572 let body = serde_json::json!({ "input": vec![0.5; 784] }).to_string();
3573 let (status, body) = send(app.clone(), predict_request(body)).await;
3574 assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
3575 assert!(String::from_utf8_lossy(&body).contains("INFERENCE_FAILED"));
3576 let (status, _) = send(app.clone(), v2_request(0.5)).await;
3577 assert_eq!(status, StatusCode::INTERNAL_SERVER_ERROR);
3578 let body = serde_json::json!({ "input": vec![0.0; 784] }).to_string();
3580 let (status, _) = send(app, predict_request(body)).await;
3581 assert_eq!(status, StatusCode::OK);
3582 }
3583 }
3584
3585 #[tokio::test]
3586 async fn test_predict_invalid_shape() {
3587 let body = serde_json::json!({ "input": vec![0.5; 100] }).to_string();
3588 let (status, body) = send(test_app(), predict_request(body)).await;
3589 assert_eq!(status, StatusCode::BAD_REQUEST);
3590 let error: ErrorResponse = serde_json::from_slice(&body).unwrap();
3591 assert_eq!(error.code, "INPUT_VALIDATION_FAILED");
3592 }
3593
3594 #[tokio::test]
3595 async fn test_predict_empty_input() {
3596 let body = serde_json::json!({ "input": [] }).to_string();
3597 let (status, body) = send(test_app(), predict_request(body)).await;
3598 assert_eq!(status, StatusCode::BAD_REQUEST);
3599 let error: ErrorResponse = serde_json::from_slice(&body).unwrap();
3600 assert_eq!(error.code, "INVALID_INPUT");
3601 }
3602
3603 #[tokio::test]
3604 async fn test_predict_malformed_json() {
3605 let (status, body) = send(test_app(), predict_request("{not json".to_string())).await;
3606 assert_eq!(status, StatusCode::BAD_REQUEST);
3607 let error: ErrorResponse = serde_json::from_slice(&body).unwrap();
3608 assert_eq!(error.code, "INVALID_REQUEST");
3609 }
3610
3611 #[tokio::test]
3612 async fn test_predict_rejects_when_saturated() {
3613 let config = ServerConfig {
3614 max_concurrent_requests: 0,
3615 ..ServerConfig::default()
3616 };
3617 let app = router(dummy_model(), &config).unwrap();
3618 let body = serde_json::json!({ "input": vec![0.5; 784] }).to_string();
3619 let (status, body) = send(app, predict_request(body)).await;
3620 assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE);
3621 let error: ErrorResponse = serde_json::from_slice(&body).unwrap();
3622 assert_eq!(error.code, "SERVER_BUSY");
3623 }
3624 fn binary_request(values: &[f32], batch_size: Option<&str>) -> Request<Body> {
3625 let body: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
3626 let mut builder = Request::builder()
3627 .uri("/predict")
3628 .method("POST")
3629 .header("content-type", OCTET_STREAM);
3630 if let Some(n) = batch_size {
3631 builder = builder.header(BATCH_SIZE_HEADER, n);
3632 }
3633 builder.body(Body::from(body)).unwrap()
3634 }
3635
3636 async fn error_code(app: Router, request: Request<Body>) -> (StatusCode, String) {
3637 let (status, body) = send(app, request).await;
3638 let error: ErrorResponse = serde_json::from_slice(&body).unwrap();
3639 (status, error.code)
3640 }
3641
3642 #[test]
3643 fn test_predict_request_deserialize() {
3644 let single: PredictRequest =
3645 serde_json::from_str(r#"{"input": [1, 2.5, -3e-2], "extra": {"a": [1]}}"#).unwrap();
3646 match &single.input_data {
3647 PredictInputData::Single { input } => assert_eq!(input, &vec![1.0, 2.5, -0.03]),
3648 _ => panic!("Expected single input"),
3649 }
3650
3651 let batch: PredictRequest = serde_json::from_str(r#"{"inputs": [[1], [2, 3]]}"#).unwrap();
3652 match batch.input_data {
3653 PredictInputData::Batch { inputs } => {
3654 assert_eq!(inputs, vec![vec![1.0], vec![2.0, 3.0]])
3655 }
3656 _ => panic!("Expected batch input"),
3657 }
3658
3659 for invalid in [
3660 r#"{}"#,
3661 r#"{"input": [1], "inputs": [[1]]}"#,
3662 r#"{"input": [1], "input": [2]}"#,
3663 r#"{"input": "nope"}"#,
3664 r#"[1, 2]"#,
3665 ] {
3666 assert!(
3667 serde_json::from_str::<PredictRequest>(invalid).is_err(),
3668 "{invalid}"
3669 );
3670 }
3671
3672 let round_trip = serde_json::to_value(&single).unwrap();
3674 assert_eq!(round_trip["input"], serde_json::json!([1.0, 2.5, -0.03f32]));
3675 }
3676
3677 #[tokio::test]
3678 async fn test_predict_json_errors() {
3679 let body = serde_json::json!({ "data": [1, 2] }).to_string();
3681 let (status, code) = error_code(test_app(), predict_request(body)).await;
3682 assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY);
3683 assert_eq!(code, "INVALID_REQUEST");
3684
3685 let request = Request::builder()
3687 .uri("/predict")
3688 .method("POST")
3689 .header("content-type", "application/json; charset=utf-8")
3690 .body(Body::from(
3691 serde_json::json!({ "input": vec![0.5; 784] }).to_string(),
3692 ))
3693 .unwrap();
3694 let (status, _) = send(test_app(), request).await;
3695 assert_eq!(status, StatusCode::OK);
3696 }
3697
3698 #[tokio::test]
3699 async fn test_predict_unsupported_content_type() {
3700 for content_type in [Some("text/plain"), None] {
3701 let mut builder = Request::builder().uri("/predict").method("POST");
3702 if let Some(content_type) = content_type {
3703 builder = builder.header("content-type", content_type);
3704 }
3705 let request = builder.body(Body::from("{}")).unwrap();
3706 let (status, code) = error_code(test_app(), request).await;
3707 assert_eq!(status, StatusCode::UNSUPPORTED_MEDIA_TYPE);
3708 assert_eq!(code, "INVALID_REQUEST");
3709 }
3710 }
3711
3712 #[tokio::test]
3713 async fn test_predict_binary_single() {
3714 let (status, body) = send(test_app(), binary_request(&[0.5; 784], None)).await;
3715 assert_eq!(status, StatusCode::OK);
3716 let response: PredictResponse = serde_json::from_slice(&body).unwrap();
3717 assert_eq!(response.batch_size, 1);
3718 match response.output_data {
3719 PredictOutputData::Single { output } => assert_eq!(output.len(), 10),
3720 _ => panic!("Expected single output"),
3721 }
3722 }
3723
3724 #[tokio::test]
3725 async fn test_predict_binary_batch() {
3726 let request = binary_request(&[0.5; 784 * 3], Some("3"));
3727 let (status, body) = send(test_app(), request).await;
3728 assert_eq!(status, StatusCode::OK);
3729 let response: PredictResponse = serde_json::from_slice(&body).unwrap();
3730 assert_eq!(response.batch_size, 3);
3731 match response.output_data {
3732 PredictOutputData::Batch { outputs } => {
3733 assert_eq!(outputs.len(), 3);
3734 assert!(outputs.iter().all(|o| o.len() == 10));
3735 }
3736 _ => panic!("Expected batch output"),
3737 }
3738
3739 let (status, body) = send(test_app(), binary_request(&[0.5; 784], Some("1"))).await;
3741 assert_eq!(status, StatusCode::OK);
3742 let response: PredictResponse = serde_json::from_slice(&body).unwrap();
3743 assert!(matches!(
3744 response.output_data,
3745 PredictOutputData::Batch { .. }
3746 ));
3747 }
3748
3749 #[tokio::test]
3750 async fn test_predict_binary_response() {
3751 let mut request = binary_request(&[0.5; 784 * 2], Some("2"));
3752 request
3753 .headers_mut()
3754 .insert(ACCEPT, HeaderValue::from_static(OCTET_STREAM));
3755 let response = test_app().oneshot(request).await.unwrap();
3756 assert_eq!(response.status(), StatusCode::OK);
3757 let headers = response.headers().clone();
3758 assert_eq!(headers[CONTENT_TYPE], OCTET_STREAM);
3759 assert_eq!(headers[BATCH_SIZE_HEADER], "2");
3760 assert_eq!(headers[OUTPUT_SHAPE_HEADER], "10");
3761 let time: f64 = headers[INFERENCE_TIME_HEADER]
3762 .to_str()
3763 .unwrap()
3764 .parse()
3765 .unwrap();
3766 assert!(time >= 0.0);
3767
3768 let body = axum::body::to_bytes(response.into_body(), usize::MAX)
3769 .await
3770 .unwrap();
3771 assert_eq!(body.len(), 2 * 10 * 4);
3772 let (values, _) = body.as_chunks::<4>();
3773 assert!(values.iter().all(|v| f32::from_le_bytes(*v) == 0.5));
3774
3775 let request = Request::builder()
3777 .uri("/predict")
3778 .method("POST")
3779 .header("content-type", "application/json")
3780 .header("accept", "text/html, application/octet-stream;q=0.9")
3781 .body(Body::from(
3782 serde_json::json!({ "input": vec![0.5; 784] }).to_string(),
3783 ))
3784 .unwrap();
3785 let response = test_app().oneshot(request).await.unwrap();
3786 assert_eq!(response.headers()[CONTENT_TYPE], OCTET_STREAM);
3787 assert_eq!(response.headers()[BATCH_SIZE_HEADER], "1");
3788 }
3789
3790 #[tokio::test]
3791 async fn test_predict_binary_errors() {
3792 let cases = [
3793 (
3795 binary_request(&[0.5; 100], None),
3796 StatusCode::BAD_REQUEST,
3797 "INPUT_VALIDATION_FAILED",
3798 ),
3799 (
3801 binary_request(&[0.5; 784 * 2], Some("3")),
3802 StatusCode::BAD_REQUEST,
3803 "INPUT_VALIDATION_FAILED",
3804 ),
3805 (
3806 binary_request(&[0.5; 784], Some("abc")),
3807 StatusCode::BAD_REQUEST,
3808 "INVALID_REQUEST",
3809 ),
3810 (
3811 binary_request(&[0.5; 784], Some("0")),
3812 StatusCode::BAD_REQUEST,
3813 "INVALID_INPUT",
3814 ),
3815 (
3816 binary_request(&[], None),
3817 StatusCode::BAD_REQUEST,
3818 "INVALID_INPUT",
3819 ),
3820 (
3821 binary_request(&[0.5; 784], Some("1001")),
3822 StatusCode::BAD_REQUEST,
3823 "BATCH_TOO_LARGE",
3824 ),
3825 ];
3826 for (request, expected_status, expected_code) in cases {
3827 let (status, code) = error_code(test_app(), request).await;
3828 assert_eq!((status, code.as_str()), (expected_status, expected_code));
3829 }
3830
3831 let request = Request::builder()
3833 .uri("/predict")
3834 .method("POST")
3835 .header("content-type", OCTET_STREAM)
3836 .body(Body::from(vec![0u8; 784 * 4 + 1]))
3837 .unwrap();
3838 let (status, code) = error_code(test_app(), request).await;
3839 assert_eq!(
3840 (status, code.as_str()),
3841 (StatusCode::BAD_REQUEST, "INPUT_VALIDATION_FAILED")
3842 );
3843
3844 let mut values = vec![0.5; 784];
3846 values[7] = f32::INFINITY;
3847 let (status, code) = error_code(test_app(), binary_request(&values, None)).await;
3848 assert_eq!(
3849 (status, code.as_str()),
3850 (StatusCode::BAD_REQUEST, "INVALID_INPUT_VALUE")
3851 );
3852 }
3853
3854 #[tokio::test]
3855 async fn test_predict_binary_body_limit_enforced() {
3856 let config = ServerConfig {
3857 max_body_bytes: 1024,
3858 ..ServerConfig::default()
3859 };
3860 let app = router(dummy_model(), &config).unwrap();
3861 let (status, code) = error_code(app, binary_request(&[0.5; 784], None)).await;
3862 assert_eq!(status, StatusCode::PAYLOAD_TOO_LARGE);
3863 assert_eq!(code, "INVALID_REQUEST");
3864 }
3865
3866 fn image_request(content_type: &str, body: Vec<u8>) -> Request<Body> {
3867 Request::builder()
3868 .uri("/predict")
3869 .method("POST")
3870 .header("content-type", content_type)
3871 .body(Body::from(body))
3872 .unwrap()
3873 }
3874
3875 #[cfg(not(feature = "image"))]
3876 #[tokio::test]
3877 async fn test_predict_image_needs_feature() {
3878 let request = image_request("image/png", vec![0x89, b'P', b'N', b'G']);
3879 let (status, code) = error_code(test_app(), request).await;
3880 assert_eq!(status, StatusCode::UNSUPPORTED_MEDIA_TYPE);
3881 assert_eq!(code, "INVALID_REQUEST");
3882 }
3883
3884 #[cfg(feature = "image")]
3885 mod image_input {
3886 use super::*;
3887 use image::{ImageEncoder, RgbImage, codecs::png::PngEncoder};
3888
3889 fn image_model_app(config: &ServerConfig) -> Router {
3890 let dummy = DummyModel::new("rgb".to_string(), vec![3, 16, 16], vec![10]);
3891 router(Model::new(Box::new(dummy), "dummy", 0), config).unwrap()
3892 }
3893
3894 fn png(width: u32, height: u32) -> Vec<u8> {
3895 let image = RgbImage::from_fn(width, height, |x, y| {
3896 image::Rgb([(x * 7) as u8, (y * 5) as u8, 128])
3897 });
3898 let mut bytes = Vec::new();
3899 PngEncoder::new(&mut bytes)
3900 .write_image(
3901 image.as_raw(),
3902 width,
3903 height,
3904 image::ExtendedColorType::Rgb8,
3905 )
3906 .unwrap();
3907 bytes
3908 }
3909
3910 #[tokio::test]
3911 async fn test_predict_png() {
3912 let app = image_model_app(&ServerConfig::default());
3913 let request = image_request("image/png", png(40, 30));
3914 let (status, body) = send(app, request).await;
3915 assert_eq!(status, StatusCode::OK);
3916 let response: serde_json::Value = serde_json::from_slice(&body).unwrap();
3917 assert_eq!(response["batch_size"], 1);
3918 assert_eq!(response["output"].as_array().unwrap().len(), 10);
3919 }
3920
3921 #[tokio::test]
3922 async fn test_predict_image_wrong_model_shape() {
3923 let (status, body) = send(test_app(), image_request("image/png", png(8, 8))).await;
3925 assert_eq!(status, StatusCode::UNSUPPORTED_MEDIA_TYPE);
3926 let error: ErrorResponse = serde_json::from_slice(&body).unwrap();
3927 assert_eq!(error.code, "INVALID_REQUEST");
3928 assert_eq!(
3929 error.details.unwrap()["expected_shape"],
3930 serde_json::json!([784])
3931 );
3932 }
3933
3934 #[tokio::test]
3935 async fn test_predict_invalid_image() {
3936 let app = image_model_app(&ServerConfig::default());
3937 for (content_type, body) in [
3938 ("image/jpeg", b"not a jpeg".to_vec()),
3939 ("image/jpeg", png(8, 8)),
3941 ] {
3942 let request = image_request(content_type, body);
3943 let (status, code) = error_code(app.clone(), request).await;
3944 assert_eq!(status, StatusCode::BAD_REQUEST);
3945 assert_eq!(code, "INVALID_IMAGE");
3946 }
3947
3948 let (status, code) = error_code(app, image_request("image/png", Vec::new())).await;
3949 assert_eq!(status, StatusCode::BAD_REQUEST);
3950 assert_eq!(code, "INVALID_INPUT");
3951 }
3952
3953 #[tokio::test]
3954 async fn test_predict_image_body_limit_enforced() {
3955 let config = ServerConfig {
3956 max_body_bytes: 1024,
3957 ..ServerConfig::default()
3958 };
3959 let app = image_model_app(&config);
3960 let body = png(256, 256);
3961 assert!(body.len() > 1024);
3962 let (status, code) = error_code(app, image_request("image/png", body)).await;
3963 assert_eq!(status, StatusCode::PAYLOAD_TOO_LARGE);
3964 assert_eq!(code, "INVALID_REQUEST");
3965 }
3966 }
3967}