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