1use axum::{
2 extract::State,
3 http::{header, HeaderMap, HeaderValue, StatusCode},
4 response::{
5 sse::{Event as SseEvent, KeepAlive, Sse},
6 IntoResponse,
7 },
8 routing::{delete, get, post},
9 Json, Router,
10};
11use base64::Engine as _;
12use mold_core::{
13 GpuInfo, ModelInfoExtended, OutputFormat, ServerStatus, SseCompleteEvent, SseErrorEvent,
14 SseProgressEvent,
15};
16use serde::{Deserialize, Serialize};
17use std::convert::Infallible;
18use tokio_stream::StreamExt as _;
19use utoipa::OpenApi;
20
21use crate::model_manager;
22use crate::state::AppState;
23
24#[derive(Debug, Serialize)]
27pub struct ApiError {
28 pub error: String,
29 pub code: String,
30 #[serde(skip)]
31 status: StatusCode,
32}
33
34impl ApiError {
35 pub fn validation(msg: impl Into<String>) -> Self {
36 Self {
37 error: msg.into(),
38 code: "VALIDATION_ERROR".to_string(),
39 status: StatusCode::UNPROCESSABLE_ENTITY,
40 }
41 }
42
43 pub fn not_found(msg: impl Into<String>) -> Self {
44 Self {
45 error: msg.into(),
46 code: "MODEL_NOT_FOUND".to_string(),
47 status: StatusCode::NOT_FOUND,
48 }
49 }
50
51 pub fn unknown_model(msg: impl Into<String>) -> Self {
52 Self {
53 error: msg.into(),
54 code: "UNKNOWN_MODEL".to_string(),
55 status: StatusCode::BAD_REQUEST,
56 }
57 }
58
59 pub fn inference(msg: impl Into<String>) -> Self {
60 Self {
61 error: msg.into(),
62 code: "INFERENCE_ERROR".to_string(),
63 status: StatusCode::INTERNAL_SERVER_ERROR,
64 }
65 }
66
67 pub fn internal(msg: impl Into<String>) -> Self {
68 Self {
69 error: msg.into(),
70 code: "INTERNAL_ERROR".to_string(),
71 status: StatusCode::INTERNAL_SERVER_ERROR,
72 }
73 }
74}
75
76impl IntoResponse for ApiError {
77 fn into_response(self) -> axum::response::Response {
78 let status = self.status;
79 (status, Json(self)).into_response()
80 }
81}
82
83fn clean_error_message(e: &anyhow::Error) -> String {
87 let full = format!("{e}");
88 let mut lines: Vec<&str> = Vec::new();
91 for line in full.lines() {
92 let trimmed = line.trim_start();
93 if trimmed.starts_with("0:") || trimmed.starts_with("1:") {
94 if trimmed.len() > 3
96 && trimmed
97 .as_bytes()
98 .first()
99 .is_some_and(|b| b.is_ascii_digit())
100 {
101 break;
102 }
103 }
104 if trimmed.len() > 2
106 && trimmed.as_bytes()[0].is_ascii_digit()
107 && trimmed.contains("::")
108 && trimmed.contains("at ")
109 {
110 break;
111 }
112 lines.push(line);
113 }
114 let msg = lines.join("\n").trim().to_string();
115 if msg.is_empty() {
116 format!("{}", e.root_cause())
118 } else {
119 msg
120 }
121}
122
123#[derive(OpenApi)]
124#[openapi(
125 paths(generate, generate_stream, list_models, load_model, pull_model_endpoint, unload_model, server_status, health),
126 components(schemas(
127 mold_core::GenerateRequest,
128 mold_core::GenerateResponse,
129 mold_core::ImageData,
130 mold_core::OutputFormat,
131 mold_core::ModelInfo,
132 mold_core::ServerStatus,
133 mold_core::GpuInfo,
134 mold_core::SseProgressEvent,
135 mold_core::SseCompleteEvent,
136 mold_core::SseErrorEvent,
137 ModelInfoExtended,
138 LoadModelBody,
139 )),
140 tags(
141 (name = "generation", description = "Image generation"),
142 (name = "models", description = "Model management"),
143 (name = "server", description = "Server status and health"),
144 ),
145 info(
146 title = "mold",
147 description = "Local AI image generation server — FLUX, SD3.5, SD1.5, SDXL, Z-Image, Flux.2, Qwen-Image",
148 version = env!("CARGO_PKG_VERSION"),
149 )
150)]
151pub struct ApiDoc;
152
153pub fn create_router(state: AppState) -> Router {
154 Router::new()
157 .route("/api/generate", post(generate))
158 .route("/api/generate/stream", post(generate_stream))
159 .route("/api/models", get(list_models))
160 .route("/api/models/load", post(load_model))
161 .route("/api/models/pull", post(pull_model_endpoint))
162 .route("/api/models/unload", delete(unload_model))
163 .route("/api/status", get(server_status))
164 .route("/health", get(health))
165 .with_state(state)
166 .route("/api/openapi.json", get(openapi_json))
167 .route("/api/docs", get(scalar_docs))
168}
169
170enum SseMessage {
174 Progress(SseProgressEvent),
175 Complete(SseCompleteEvent),
176 Error(SseErrorEvent),
177}
178
179fn progress_to_sse(event: mold_inference::ProgressEvent) -> SseProgressEvent {
181 event.into()
182}
183
184fn sse_message_to_event(msg: SseMessage) -> SseEvent {
185 fn serialize_event<T: Serialize>(event_name: &str, payload: &T) -> SseEvent {
186 match serde_json::to_string(payload) {
187 Ok(data) => SseEvent::default().event(event_name).data(data),
188 Err(err) => SseEvent::default().event("error").data(
189 serde_json::json!({
190 "message": format!("failed to serialize SSE payload: {err}")
191 })
192 .to_string(),
193 ),
194 }
195 }
196
197 match msg {
198 SseMessage::Progress(payload) => serialize_event("progress", &payload),
199 SseMessage::Complete(payload) => serialize_event("complete", &payload),
200 SseMessage::Error(payload) => serialize_event("error", &payload),
201 }
202}
203
204fn take_generated_image(
205 response: &mut mold_core::GenerateResponse,
206) -> Result<mold_core::ImageData, ApiError> {
207 if response.images.is_empty() {
208 return Err(ApiError::inference(
209 "generation error: engine returned no images",
210 ));
211 }
212 Ok(response.images.remove(0))
213}
214
215#[utoipa::path(
218 post,
219 path = "/api/generate",
220 tag = "generation",
221 request_body = mold_core::GenerateRequest,
222 responses(
223 (status = 200, description = "Generated image bytes", content_type = "image/png"),
224 (status = 404, description = "Model not downloaded"),
225 (status = 422, description = "Invalid request parameters"),
226 (status = 500, description = "Inference error"),
227 )
228)]
229async fn generate(
232 State(state): State<AppState>,
233 Json(mut req): Json<mold_core::GenerateRequest>,
234) -> Result<impl IntoResponse, ApiError> {
235 apply_default_metadata_setting(&state, &mut req).await;
236 tracing::info!(
237 model = %req.model,
238 prompt = %req.prompt,
239 width = req.width,
240 height = req.height,
241 steps = req.steps,
242 guidance = req.guidance,
243 seed = ?req.seed,
244 format = %req.output_format,
245 "generate request"
246 );
247
248 if let Err(e) = validate_generate_request(&req) {
250 return Err(ApiError::validation(e));
251 }
252
253 model_manager::ensure_model_ready(&state, &req.model, None).await?;
255
256 let engine = state.engine.clone();
258 let result = tokio::task::spawn_blocking(move || {
259 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
260 let mut guard = engine.blocking_lock();
261 let engine = guard.as_mut().ok_or_else(|| {
262 anyhow::anyhow!("no engine available after model readiness check")
263 })?;
264 engine.clear_on_progress();
265 engine.generate(&req)
266 }))
267 })
268 .await
269 .map_err(|e| {
270 tracing::error!("inference task join error: {e:?}");
271 ApiError::inference("inference task failed")
272 })?;
273
274 let mut response = match result {
275 Ok(Ok(resp)) => resp,
276 Ok(Err(e)) => {
277 tracing::error!("generation error: {e:#}");
278 return Err(ApiError::inference(format!(
279 "generation error: {}",
280 clean_error_message(&e)
281 )));
282 }
283 Err(panic_payload) => {
284 let msg = panic_payload
285 .downcast_ref::<String>()
286 .map(|s| s.as_str())
287 .or_else(|| panic_payload.downcast_ref::<&str>().copied())
288 .unwrap_or("unknown panic");
289 tracing::error!("inference panicked: {msg}");
290 return Err(ApiError::inference(format!("inference panicked: {msg}")));
291 }
292 };
293
294 let img = take_generated_image(&mut response)?;
295 let content_type = match img.format {
296 OutputFormat::Png => HeaderValue::from_static("image/png"),
297 OutputFormat::Jpeg => HeaderValue::from_static("image/jpeg"),
298 };
299 let mut headers = HeaderMap::new();
300 headers.insert(header::CONTENT_TYPE, content_type);
301 headers.insert(
302 "x-mold-seed-used",
303 HeaderValue::from_str(&response.seed_used.to_string())
304 .map_err(|e| ApiError::internal(format!("failed to serialize seed header: {e}")))?,
305 );
306 Ok((headers, img.data))
307}
308
309fn validate_generate_request(req: &mold_core::GenerateRequest) -> Result<(), String> {
310 mold_core::validate_generate_request(req)
311}
312
313async fn apply_default_metadata_setting(state: &AppState, req: &mut mold_core::GenerateRequest) {
314 if req.embed_metadata.is_some() {
315 return;
316 }
317
318 let config = state.config.read().await;
319 req.embed_metadata = Some(config.effective_embed_metadata(None));
320}
321
322#[utoipa::path(
325 post,
326 path = "/api/generate/stream",
327 tag = "generation",
328 request_body = mold_core::GenerateRequest,
329 responses(
330 (status = 200, description = "SSE event stream with progress and result"),
331 (status = 404, description = "Model not downloaded"),
332 (status = 422, description = "Invalid request parameters"),
333 (status = 500, description = "Inference error"),
334 )
335)]
336async fn generate_stream(
337 State(state): State<AppState>,
338 Json(mut req): Json<mold_core::GenerateRequest>,
339) -> Result<Sse<impl futures_core::Stream<Item = Result<SseEvent, Infallible>>>, ApiError> {
340 apply_default_metadata_setting(&state, &mut req).await;
341 tracing::info!(
342 model = %req.model,
343 prompt = %req.prompt,
344 "generate/stream request"
345 );
346
347 if let Err(e) = validate_generate_request(&req) {
349 return Err(ApiError::validation(e));
350 }
351
352 let _ = model_manager::check_model_available(&state, &req.model).await?;
354
355 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<SseMessage>();
357
358 let bg_tx = tx;
360 tokio::spawn(async move {
361 let progress = std::sync::Arc::new({
363 let bg_tx = bg_tx.clone();
364 move |event| {
365 let _ = bg_tx.send(SseMessage::Progress(progress_to_sse(event)));
366 }
367 });
368
369 if let Err(api_err) =
370 model_manager::ensure_model_ready(&state, &req.model, Some(progress)).await
371 {
372 let _ = bg_tx.send(SseMessage::Error(SseErrorEvent {
373 message: api_err.error,
374 }));
375 return;
376 }
377
378 let engine = state.engine.clone();
380 let gen_tx = bg_tx.clone();
381 let gen_req = req.clone();
382 let result = tokio::task::spawn_blocking(move || {
383 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
384 let mut guard = engine.blocking_lock();
385 let e = guard.as_mut().ok_or_else(|| {
386 anyhow::anyhow!("no engine available after model readiness check")
387 })?;
388 let progress_tx = gen_tx.clone();
390 e.set_on_progress(Box::new(move |event| {
391 let _ = progress_tx.send(SseMessage::Progress(progress_to_sse(event)));
392 }));
393 let generate_result = e.generate(&gen_req);
394 e.clear_on_progress();
395 generate_result
396 }))
397 })
398 .await;
399
400 match result {
401 Ok(Ok(Ok(mut response))) => {
402 let img = match take_generated_image(&mut response) {
403 Ok(img) => img,
404 Err(err) => {
405 let _ = bg_tx.send(SseMessage::Error(SseErrorEvent { message: err.error }));
406 return;
407 }
408 };
409 let _ = bg_tx.send(SseMessage::Complete(SseCompleteEvent {
410 image: base64::engine::general_purpose::STANDARD.encode(&img.data),
411 format: img.format,
412 width: img.width,
413 height: img.height,
414 seed_used: response.seed_used,
415 generation_time_ms: response.generation_time_ms,
416 }));
417 }
418 Ok(Ok(Err(e))) => {
419 tracing::error!("generation error: {e:#}");
420 let _ = bg_tx.send(SseMessage::Error(SseErrorEvent {
421 message: format!("generation error: {}", clean_error_message(&e)),
422 }));
423 }
424 Ok(Err(panic_payload)) => {
425 let msg = panic_payload
426 .downcast_ref::<String>()
427 .map(|s| s.as_str())
428 .or_else(|| panic_payload.downcast_ref::<&str>().copied())
429 .unwrap_or("unknown panic");
430 tracing::error!("inference panicked: {msg}");
431 let _ = bg_tx.send(SseMessage::Error(SseErrorEvent {
432 message: format!("inference panicked: {msg}"),
433 }));
434 }
435 Err(join_err) => {
436 tracing::error!("inference task join error: {join_err:?}");
437 let _ = bg_tx.send(SseMessage::Error(SseErrorEvent {
438 message: "inference task failed".to_string(),
439 }));
440 }
441 }
442 });
443
444 let stream = tokio_stream::wrappers::UnboundedReceiverStream::new(rx)
446 .map(|msg| Ok::<_, Infallible>(sse_message_to_event(msg)));
447
448 Ok(Sse::new(stream).keep_alive(
449 KeepAlive::new()
450 .interval(std::time::Duration::from_secs(15))
451 .text("ping"),
452 ))
453}
454
455#[utoipa::path(
458 get,
459 path = "/api/models",
460 tag = "models",
461 responses(
462 (status = 200, description = "List of available models", body = Vec<ModelInfoExtended>),
463 )
464)]
465async fn list_models(State(state): State<AppState>) -> Json<Vec<ModelInfoExtended>> {
466 Json(model_manager::list_models(&state).await)
467}
468
469#[derive(Debug, Deserialize, utoipa::ToSchema)]
472pub struct LoadModelBody {
473 #[schema(example = "flux-schnell:q8")]
474 pub model: String,
475}
476
477#[utoipa::path(
478 post,
479 path = "/api/models/load",
480 tag = "models",
481 request_body = LoadModelBody,
482 responses(
483 (status = 200, description = "Model loaded successfully"),
484 (status = 404, description = "Model not downloaded"),
485 (status = 400, description = "Unknown model"),
486 (status = 500, description = "Failed to load model"),
487 )
488)]
489async fn load_model(
490 State(state): State<AppState>,
491 Json(body): Json<LoadModelBody>,
492) -> Result<impl IntoResponse, ApiError> {
493 model_manager::ensure_model_ready(&state, &body.model, None).await?;
494 tracing::info!(model = %body.model, "model loaded via API");
495 Ok(StatusCode::OK)
496}
497
498#[utoipa::path(
501 post,
502 path = "/api/models/pull",
503 tag = "models",
504 request_body = LoadModelBody,
505 responses(
506 (status = 200, description = "Model pulled (SSE stream or plain text)"),
507 (status = 400, description = "Unknown model"),
508 (status = 500, description = "Download failed"),
509 )
510)]
511async fn pull_model_endpoint(
512 State(state): State<AppState>,
513 headers: HeaderMap,
514 Json(body): Json<LoadModelBody>,
515) -> Result<impl IntoResponse, ApiError> {
516 let wants_sse = headers
517 .get(header::ACCEPT)
518 .and_then(|v| v.to_str().ok())
519 .is_some_and(|v| v.contains("text/event-stream"));
520
521 if !wants_sse {
522 return pull_model_blocking(state, body.model)
524 .await
525 .map(PullResponse::Text);
526 }
527
528 let model = body.model.clone();
530
531 if mold_core::manifest::find_manifest(&mold_core::manifest::resolve_model_name(&model))
533 .is_none()
534 {
535 return Err(ApiError::unknown_model(format!(
536 "unknown model '{model}'. Run 'mold list' to see available models."
537 )));
538 }
539
540 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<SseMessage>();
541
542 tokio::spawn(async move {
543 let progress_tx = tx.clone();
544 let model_for_cb = model.clone();
545 let callback =
546 std::sync::Arc::new(move |event: mold_core::download::DownloadProgressEvent| {
547 let sse_event = match event {
548 mold_core::download::DownloadProgressEvent::FileStart {
549 filename,
550 file_index,
551 total_files,
552 size_bytes,
553 } => SseProgressEvent::DownloadProgress {
554 filename,
555 file_index,
556 total_files,
557 bytes_downloaded: 0,
558 bytes_total: size_bytes,
559 },
560 mold_core::download::DownloadProgressEvent::FileProgress {
561 filename,
562 file_index,
563 bytes_downloaded,
564 bytes_total,
565 } => SseProgressEvent::DownloadProgress {
566 filename,
567 file_index,
568 total_files: 0,
569 bytes_downloaded,
570 bytes_total,
571 },
572 mold_core::download::DownloadProgressEvent::FileDone {
573 filename,
574 file_index,
575 total_files,
576 } => SseProgressEvent::DownloadDone {
577 filename,
578 file_index,
579 total_files,
580 },
581 };
582 let _ = progress_tx.send(SseMessage::Progress(sse_event));
583 });
584
585 match model_manager::pull_model(&state, &model, Some(callback)).await {
586 Ok(model_manager::PullStatus::AlreadyAvailable) => {
587 let _ = tx.send(SseMessage::Progress(SseProgressEvent::PullComplete {
588 model: model_for_cb,
589 }));
590 }
591 Ok(model_manager::PullStatus::Pulled) => {
592 let _ = tx.send(SseMessage::Progress(SseProgressEvent::PullComplete {
593 model: model_for_cb,
594 }));
595 }
596 Err(e) => {
597 let _ = tx.send(SseMessage::Error(SseErrorEvent { message: e.error }));
598 }
599 }
600 });
601
602 let stream = tokio_stream::wrappers::UnboundedReceiverStream::new(rx)
603 .map(|msg| Ok::<_, Infallible>(sse_message_to_event(msg)));
604
605 Ok(PullResponse::Sse(
606 Sse::new(stream)
607 .keep_alive(
608 KeepAlive::new()
609 .interval(std::time::Duration::from_secs(15))
610 .text("ping"),
611 )
612 .into_response(),
613 ))
614}
615
616async fn pull_model_blocking(state: AppState, model: String) -> Result<String, ApiError> {
618 match model_manager::pull_model(&state, &model, None).await? {
619 model_manager::PullStatus::AlreadyAvailable => {
620 Ok(format!("model '{}' already available", model))
621 }
622 model_manager::PullStatus::Pulled => Ok(format!("model '{}' pulled successfully", model)),
623 }
624}
625
626enum PullResponse {
628 Sse(axum::response::Response),
629 Text(String),
630}
631
632impl IntoResponse for PullResponse {
633 fn into_response(self) -> axum::response::Response {
634 match self {
635 PullResponse::Sse(resp) => resp,
636 PullResponse::Text(text) => text.into_response(),
637 }
638 }
639}
640
641#[utoipa::path(
644 delete,
645 path = "/api/models/unload",
646 tag = "models",
647 responses(
648 (status = 200, description = "Model unloaded or no model was loaded", body = String),
649 )
650)]
651async fn unload_model(State(state): State<AppState>) -> Result<impl IntoResponse, ApiError> {
652 Ok((StatusCode::OK, model_manager::unload_model(&state).await))
653}
654
655#[utoipa::path(
658 get,
659 path = "/api/status",
660 tag = "server",
661 responses(
662 (status = 200, description = "Server status", body = ServerStatus),
663 )
664)]
665async fn server_status(State(state): State<AppState>) -> Json<ServerStatus> {
666 let engine = state.engine.lock().await;
667 let models_loaded = match engine.as_ref() {
668 Some(e) if e.is_loaded() => vec![e.model_name().to_string()],
669 _ => vec![],
670 };
671 drop(engine);
672
673 Json(ServerStatus {
674 version: env!("CARGO_PKG_VERSION").to_string(),
675 models_loaded,
676 gpu_info: query_gpu_info(),
677 uptime_secs: state.start_time.elapsed().as_secs(),
678 })
679}
680
681#[utoipa::path(
684 get,
685 path = "/health",
686 tag = "server",
687 responses(
688 (status = 200, description = "Server is healthy"),
689 )
690)]
691async fn health() -> impl IntoResponse {
692 StatusCode::OK
693}
694
695async fn openapi_json() -> impl IntoResponse {
698 Json(ApiDoc::openapi())
699}
700
701async fn scalar_docs() -> impl IntoResponse {
704 (
705 [(header::CONTENT_TYPE, "text/html")],
706 r#"<!DOCTYPE html>
707<html>
708<head>
709 <title>mold API</title>
710 <meta charset="utf-8" />
711 <meta name="viewport" content="width=device-width, initial-scale=1" />
712</head>
713<body>
714 <script id="api-reference" data-url="/api/openapi.json"></script>
715 <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
716</body>
717</html>"#,
718 )
719}
720
721fn query_gpu_info() -> Option<GpuInfo> {
724 let nvidia_smi = if std::path::Path::new("/run/current-system/sw/bin/nvidia-smi").exists() {
725 "/run/current-system/sw/bin/nvidia-smi"
726 } else {
727 "nvidia-smi"
728 };
729
730 let output = std::process::Command::new(nvidia_smi)
731 .args([
732 "--query-gpu=name,memory.total,memory.used",
733 "--format=csv,noheader,nounits",
734 ])
735 .output()
736 .ok()?;
737
738 if !output.status.success() {
739 return None;
740 }
741
742 let text = String::from_utf8(output.stdout).ok()?;
743 let line = text.lines().next()?;
744 let parts: Vec<&str> = line.split(',').map(str::trim).collect();
745 if parts.len() < 3 {
746 return None;
747 }
748
749 Some(GpuInfo {
750 name: parts[0].to_string(),
751 vram_total_mb: parts[1].parse().ok()?,
752 vram_used_mb: parts[2].parse().ok()?,
753 })
754}
755
756#[cfg(test)]
757mod tests {
758 use super::*;
759
760 #[test]
761 fn clean_error_message_strips_backtrace() {
762 let err = anyhow::anyhow!(
763 "DriverError(CUDA_ERROR_OUT_OF_MEMORY, \"out of memory\")\n\
764 \x20 0: candle_core::error::Error::bt\n\
765 \x20 at /home/user/.cargo/git/candle/src/error.rs:264:25\n\
766 \x20 1: <core::result::Result<O,E> as candle_core::cuda_backend::error::WrapErr<O>>::w\n\
767 \x20 at /home/user/.cargo/git/candle/src/cuda_backend/error.rs:60:65"
768 );
769 let msg = clean_error_message(&err);
770 assert_eq!(
771 msg,
772 "DriverError(CUDA_ERROR_OUT_OF_MEMORY, \"out of memory\")"
773 );
774 }
775
776 #[test]
777 fn clean_error_message_preserves_simple_error() {
778 let err = anyhow::anyhow!("model not found: flux-dev:q4");
779 let msg = clean_error_message(&err);
780 assert_eq!(msg, "model not found: flux-dev:q4");
781 }
782
783 #[test]
784 fn clean_error_message_preserves_multiline_without_backtrace() {
785 let err = anyhow::anyhow!("validation failed\nprompt is empty\nsteps must be > 0");
786 let msg = clean_error_message(&err);
787 assert_eq!(msg, "validation failed\nprompt is empty\nsteps must be > 0");
788 }
789
790 #[test]
791 fn clean_error_message_strips_high_numbered_frames() {
792 let err = anyhow::anyhow!(
793 "some error\n\
794 \x20 10: tokio::runtime::task::core::Core<T,S>::poll at /home/user/.cargo/tokio/src/core.rs:375\n\
795 \x20 11: std::panicking::catch_unwind at /nix/store/rust/src/panicking.rs:544"
796 );
797 let msg = clean_error_message(&err);
798 assert_eq!(msg, "some error");
799 }
800
801 #[test]
802 fn clean_error_message_empty_fallback() {
803 let err = anyhow::anyhow!("0: candle_core::error::Error::bt at /some/path.rs:10:5");
805 let msg = clean_error_message(&err);
806 assert!(!msg.is_empty());
808 }
809}