1#![forbid(unsafe_code)]
2
3use clap::ValueEnum;
4use serde_json::Value;
5use std::future::Future;
6use std::io::{self, IsTerminal};
7use std::pin::Pin;
8use std::sync::{Arc, OnceLock, RwLock};
9
10pub mod terminal_progress;
11
12#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
13pub enum LogFormat {
14 #[default]
15 Pretty,
16 Json,
17}
18
19#[allow(dead_code)]
20#[derive(Clone, Debug, Eq, PartialEq)]
21pub enum RuntimeStatus {
22 NotReady,
23 Starting,
24 Loading,
25 Ready,
26 ShuttingDown,
27 Stopped,
28 Exited,
29 Warning,
30 Error,
31}
32
33impl RuntimeStatus {
34 pub fn as_str(&self) -> &'static str {
35 match self {
36 RuntimeStatus::NotReady => "NOT READY",
37 RuntimeStatus::Starting => "starting",
38 RuntimeStatus::Loading => "loading",
39 RuntimeStatus::Ready => "ready",
40 RuntimeStatus::ShuttingDown => "shutting down",
41 RuntimeStatus::Stopped => "stopped",
42 RuntimeStatus::Exited => "exited",
43 RuntimeStatus::Warning => "warning",
44 RuntimeStatus::Error => "error",
45 }
46 }
47}
48
49#[allow(dead_code)]
50#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51pub enum ConsoleSessionMode {
52 InteractiveDashboard,
53 Fallback,
54 None,
55}
56
57#[allow(dead_code)]
58#[derive(Clone, Debug, Eq, PartialEq)]
59pub struct DashboardProcessRow {
60 pub name: String,
61 pub backend: String,
62 pub status: RuntimeStatus,
63 pub port: u16,
64 pub pid: u32,
65}
66
67#[allow(dead_code)]
68#[derive(Clone, Debug, Eq, PartialEq)]
69pub struct DashboardEndpointRow {
70 pub label: String,
71 pub status: RuntimeStatus,
72 pub url: String,
73 pub port: u16,
74 pub pid: Option<u32>,
75}
76
77#[allow(dead_code)]
78#[derive(Clone, Debug, PartialEq)]
79pub struct DashboardModelRow {
80 pub name: String,
81 pub role: Option<String>,
82 pub status: RuntimeStatus,
83 pub port: Option<u16>,
84 pub device: Option<String>,
85 pub slots: Option<usize>,
86 pub quantization: Option<String>,
87 pub ctx_size: Option<u32>,
88 pub ctx_used_tokens: Option<u64>,
89 pub lanes: Option<Vec<DashboardModelLane>>,
90 pub file_size_gb: Option<f64>,
91}
92
93#[allow(dead_code)]
94#[derive(Clone, Debug, Eq, PartialEq)]
95pub struct DashboardModelLane {
96 pub index: usize,
97 pub active: bool,
98}
99
100#[allow(dead_code)]
101#[derive(Clone, Debug, Eq, PartialEq)]
102pub struct DashboardAcceptedRequestBucket {
103 pub second_offset: u32,
104 pub accepted_count: u64,
105}
106
107#[derive(Clone, Debug, Eq, PartialEq)]
108pub enum ModelProgressStatus {
109 Ensuring,
110 Downloading,
111 Ready,
112}
113
114impl ModelProgressStatus {
115 pub fn as_str(&self) -> &'static str {
116 match self {
117 ModelProgressStatus::Ensuring => "ensuring",
118 ModelProgressStatus::Downloading => "downloading",
119 ModelProgressStatus::Ready => "ready",
120 }
121 }
122}
123
124#[allow(dead_code)]
125#[derive(Clone, Debug, PartialEq)]
126pub struct DashboardSnapshot {
127 pub llama_process_rows: Vec<DashboardProcessRow>,
128 pub webserver_rows: Vec<DashboardEndpointRow>,
129 pub loaded_model_rows: Vec<DashboardModelRow>,
130 pub current_inflight_requests: u64,
131 pub accepted_request_buckets: Vec<DashboardAcceptedRequestBucket>,
132 pub latency_samples_ms: Vec<u64>,
133}
134
135impl Default for DashboardSnapshot {
136 fn default() -> Self {
137 Self {
138 llama_process_rows: Vec::new(),
139 webserver_rows: Vec::new(),
140 loaded_model_rows: Vec::new(),
141 current_inflight_requests: 0,
142 accepted_request_buckets: (0..30)
143 .map(|second_offset| DashboardAcceptedRequestBucket {
144 second_offset,
145 accepted_count: 0,
146 })
147 .collect(),
148 latency_samples_ms: Vec::new(),
149 }
150 }
151}
152
153#[allow(dead_code)]
154#[derive(Clone, Debug, Default, PartialEq)]
155pub struct DashboardLaunchPlan {
156 pub llama_process_rows: Vec<DashboardProcessRow>,
157 pub webserver_rows: Vec<DashboardEndpointRow>,
158 pub loaded_model_rows: Vec<DashboardModelRow>,
159}
160
161#[allow(dead_code)]
162pub type DashboardSnapshotFuture<'a> = Pin<Box<dyn Future<Output = DashboardSnapshot> + Send + 'a>>;
163
164#[allow(dead_code)]
165pub trait DashboardSnapshotProvider: Send + Sync {
166 fn snapshot(&self) -> DashboardSnapshotFuture<'_>;
167}
168
169pub type OutputSinkFuture<'a, T> = Pin<Box<dyn Future<Output = io::Result<T>> + Send + 'a>>;
170
171pub trait OutputSink: Send + Sync {
172 fn emit_event(&self, event: OutputEvent) -> io::Result<()>;
173
174 fn schedule_ready_prompt(&self) -> io::Result<()> {
175 Ok(())
176 }
177
178 fn write_ready_prompt(&self) -> io::Result<()> {
179 Ok(())
180 }
181
182 fn ready_prompt_active(&self) -> bool {
183 false
184 }
185
186 fn flush(&self) -> OutputSinkFuture<'_, ()> {
187 Box::pin(async { Ok(()) })
188 }
189
190 fn mode(&self) -> LogFormat {
191 LogFormat::Pretty
192 }
193
194 fn console_session_mode(&self) -> Option<ConsoleSessionMode> {
195 None
196 }
197
198 fn register_dashboard_snapshot_provider(&self, _provider: Arc<dyn DashboardSnapshotProvider>) {}
199
200 fn enter_tui(&self) -> OutputSinkFuture<'_, ()> {
201 Box::pin(async { Ok(()) })
202 }
203
204 fn exit_tui(&self) -> OutputSinkFuture<'_, ()> {
205 Box::pin(async { Ok(()) })
206 }
207
208 fn dispatch_tui_event(&self, _event: TuiEvent) -> OutputSinkFuture<'_, TuiControlFlow> {
209 Box::pin(async { Ok(TuiControlFlow::Continue) })
210 }
211
212 fn render_tui_if_dirty(&self) -> OutputSinkFuture<'_, bool> {
213 Box::pin(async { Ok(false) })
214 }
215
216 fn force_restore_tui_terminal(&self) -> io::Result<()> {
217 Ok(())
218 }
219}
220
221static OUTPUT_SINK: OnceLock<RwLock<Option<Arc<dyn OutputSink>>>> = OnceLock::new();
222
223fn output_sink_slot() -> &'static RwLock<Option<Arc<dyn OutputSink>>> {
224 OUTPUT_SINK.get_or_init(|| RwLock::new(None))
225}
226
227pub fn set_output_sink(sink: Arc<dyn OutputSink>) {
228 if let Ok(mut slot) = output_sink_slot().write() {
229 *slot = Some(sink);
230 }
231}
232
233pub fn clear_output_sink() {
234 if let Ok(mut slot) = output_sink_slot().write() {
235 *slot = None;
236 }
237}
238
239pub fn output_sink() -> Option<Arc<dyn OutputSink>> {
240 output_sink_slot()
241 .read()
242 .ok()
243 .and_then(|slot| slot.as_ref().cloned())
244}
245
246pub fn emit_event(event: OutputEvent) -> io::Result<()> {
247 match output_sink() {
248 Some(sink) => sink.emit_event(event),
249 None => Ok(()),
250 }
251}
252
253pub async fn flush_output() -> io::Result<()> {
254 match output_sink() {
255 Some(sink) => sink.flush().await,
256 None => Ok(()),
257 }
258}
259
260pub fn schedule_ready_prompt() -> io::Result<()> {
261 match output_sink() {
262 Some(sink) => sink.schedule_ready_prompt(),
263 None => Ok(()),
264 }
265}
266
267pub fn json_mode_enabled() -> bool {
268 output_sink().is_some_and(|sink| matches!(sink.mode(), LogFormat::Json))
269}
270
271pub fn interactive_tui_active() -> bool {
272 output_sink().is_some_and(|sink| {
273 matches!(sink.mode(), LogFormat::Pretty)
274 && matches!(
275 sink.console_session_mode(),
276 Some(ConsoleSessionMode::InteractiveDashboard)
277 )
278 })
279}
280
281pub fn current_console_session_mode() -> ConsoleSessionMode {
282 console_session_mode(
283 std::io::stdin().is_terminal(),
284 std::io::stderr().is_terminal(),
285 )
286}
287
288pub fn console_session_mode(stdin_is_tty: bool, stderr_is_tty: bool) -> ConsoleSessionMode {
289 console_session_mode_for_term(
290 stdin_is_tty,
291 stderr_is_tty,
292 std::env::var("TERM").ok().as_deref(),
293 )
294}
295
296pub fn console_session_mode_for_term(
297 stdin_is_tty: bool,
298 stderr_is_tty: bool,
299 term: Option<&str>,
300) -> ConsoleSessionMode {
301 if stdin_is_tty && stderr_is_tty && terminal_supports_dashboard(term) {
302 ConsoleSessionMode::InteractiveDashboard
303 } else {
304 ConsoleSessionMode::Fallback
305 }
306}
307
308fn terminal_supports_dashboard(term: Option<&str>) -> bool {
309 match term.map(str::trim).filter(|term| !term.is_empty()) {
310 Some(term) => term != "dumb",
311 None => false,
312 }
313}
314
315pub fn sort_dashboard_endpoint_rows(rows: &mut [DashboardEndpointRow]) {
316 rows.sort_by(|left, right| {
317 dashboard_endpoint_sort_bucket(left)
318 .cmp(&dashboard_endpoint_sort_bucket(right))
319 .then_with(|| left.label.cmp(&right.label))
320 });
321}
322
323fn dashboard_endpoint_sort_bucket(row: &DashboardEndpointRow) -> u8 {
324 if row.label.starts_with("Plugin: ") {
325 1
326 } else {
327 0
328 }
329}
330
331#[derive(Clone, Copy, Debug, Eq, PartialEq)]
332pub enum TuiKeyEvent {
333 Tab,
334 BackTab,
335 Backspace,
336 Enter,
337 Escape,
338 Left,
339 Right,
340 Up,
341 Down,
342 PageUp,
343 PageDown,
344 Interrupt,
345 Char(char),
346}
347
348#[derive(Clone, Copy, Debug, Eq, PartialEq)]
349pub enum TuiEvent {
350 Key(TuiKeyEvent),
351 Resize { columns: u16, rows: u16 },
352 MouseDown { column: u16, row: u16 },
353}
354
355#[derive(Clone, Copy, Debug, Eq, PartialEq)]
356pub enum TuiControlFlow {
357 Continue,
358 Quit,
359}
360
361#[derive(Clone, Debug, Eq, PartialEq)]
362pub enum OutputLevel {
363 Debug,
364 Info,
365 Warn,
366 Error,
367 Fatal,
368}
369
370impl OutputLevel {
371 pub fn as_str(&self) -> &'static str {
372 match self {
373 OutputLevel::Debug => "debug",
374 OutputLevel::Info => "info",
375 OutputLevel::Warn => "warn",
376 OutputLevel::Error => "error",
377 OutputLevel::Fatal => "fatal",
378 }
379 }
380}
381
382#[derive(Clone, Debug, Eq, PartialEq)]
383pub enum LlamaInstanceKind {
384 LlamaServer,
385}
386
387impl LlamaInstanceKind {
388 pub fn as_str(&self) -> &'static str {
389 match self {
390 LlamaInstanceKind::LlamaServer => "llama-server",
391 }
392 }
393
394 pub fn sort_key(&self) -> u8 {
395 match self {
396 LlamaInstanceKind::LlamaServer => 0,
397 }
398 }
399}
400
401#[allow(dead_code)]
402#[derive(Clone, Debug, PartialEq)]
403pub enum OutputEvent {
404 Info {
405 message: String,
406 context: Option<String>,
407 },
408 Startup {
409 version: String,
410 message: Option<String>,
411 },
412 LaunchPlan {
413 plan: DashboardLaunchPlan,
414 },
415 NodeIdentity {
416 node_id: String,
417 mesh_id: Option<String>,
418 },
419 InviteToken {
420 token: String,
421 mesh_id: String,
422 mesh_name: Option<String>,
423 },
424 DiscoveryStarting {
425 source: String,
426 },
427 MeshFound {
428 mesh: String,
429 peers: usize,
430 region: Option<String>,
431 },
432 DiscoveryJoined {
433 mesh: String,
434 },
435 DiscoveryFailed {
436 message: String,
437 detail: Option<String>,
438 },
439 WaitingForPeers {
440 detail: Option<String>,
441 },
442 PassiveMode {
443 role: String,
444 status: RuntimeStatus,
445 capacity_gb: Option<f64>,
446 models_on_disk: Option<Vec<String>>,
447 detail: Option<String>,
448 },
449 PeerJoined {
450 peer_id: String,
451 label: Option<String>,
452 },
453 PeerLeft {
454 peer_id: String,
455 reason: Option<String>,
456 },
457 ModelQueued {
458 model: String,
459 },
460 ModelLoading {
461 model: String,
462 source: Option<String>,
463 },
464 ModelLoaded {
465 model: String,
466 bytes: Option<u64>,
467 },
468 ModelUnloading {
469 model: String,
470 },
471 ModelUnloaded {
472 model: String,
473 },
474 HostElected {
475 model: String,
476 host: String,
477 role: Option<String>,
478 capacity_gb: Option<f64>,
479 },
480 RpcServerStarting {
481 port: u16,
482 device: String,
483 log_path: Option<String>,
484 },
485 RpcReady {
486 port: u16,
487 device: String,
488 log_path: Option<String>,
489 },
490 RpcStartupFailed {
491 port: u16,
492 device: String,
493 log_path: Option<String>,
494 detail: String,
495 },
496 LlamaStarting {
497 model: Option<String>,
498 http_port: u16,
499 ctx_size: Option<u32>,
500 log_path: Option<String>,
501 },
502 LlamaReady {
503 model: Option<String>,
504 port: u16,
505 ctx_size: Option<u32>,
506 log_path: Option<String>,
507 },
508 LlamaStartupFailed {
509 model: Option<String>,
510 http_port: u16,
511 ctx_size: Option<u32>,
512 log_path: Option<String>,
513 detail: String,
514 },
515 ModelReady {
516 model: String,
517 internal_port: Option<u16>,
518 role: Option<String>,
519 },
520 MultiModelMode {
521 count: usize,
522 models: Vec<String>,
523 },
524 WebserverStarting {
525 url: String,
526 },
527 WebserverReady {
528 url: String,
529 },
530 ApiStarting {
531 url: String,
532 },
533 ApiReady {
534 url: String,
535 },
536 RuntimeReady {
537 api_url: String,
538 console_url: Option<String>,
539 api_port: u16,
540 console_port: Option<u16>,
541 models_count: Option<usize>,
542 pi_command: Option<String>,
543 goose_command: Option<String>,
544 },
545 ModelDownloadProgress {
546 label: String,
547 file: Option<String>,
548 downloaded_bytes: Option<u64>,
549 total_bytes: Option<u64>,
550 status: ModelProgressStatus,
551 },
552 RequestRouted {
553 model: String,
554 target: String,
555 },
556 Warning {
557 message: String,
558 context: Option<String>,
559 },
560 Error {
561 message: String,
562 context: Option<String>,
563 },
564 Fatal {
565 message: String,
566 context: Option<String>,
567 },
568 ShutdownRequested {
569 signal: &'static str,
570 },
571 Shutdown {
572 reason: Option<String>,
573 },
574 LlamaNativeLog {
575 message: String,
576 category: &'static str,
577 params: Vec<(String, Value)>,
578 },
579}
580
581impl OutputEvent {
582 pub fn event_name(&self) -> &'static str {
583 match self {
584 OutputEvent::Info { .. } => "info",
585 OutputEvent::Startup { .. } => "startup",
586 OutputEvent::LaunchPlan { .. } => "launch_plan",
587 OutputEvent::NodeIdentity { .. } => "node_identity",
588 OutputEvent::InviteToken { .. } => "invite_token",
589 OutputEvent::DiscoveryStarting { .. } => "discovery_starting",
590 OutputEvent::MeshFound { .. } => "mesh_found",
591 OutputEvent::DiscoveryJoined { .. } => "discovery_joined",
592 OutputEvent::DiscoveryFailed { .. } => "discovery_failed",
593 OutputEvent::WaitingForPeers { .. } => "waiting_for_peers",
594 OutputEvent::PassiveMode { .. } => "passive_mode",
595 OutputEvent::PeerJoined { .. } => "peer_joined",
596 OutputEvent::PeerLeft { .. } => "peer_left",
597 OutputEvent::ModelQueued { .. } => "model_queued",
598 OutputEvent::ModelLoading { .. } => "model_loading",
599 OutputEvent::ModelLoaded { .. } => "model_loaded",
600 OutputEvent::ModelUnloading { .. } => "model_unloading",
601 OutputEvent::ModelUnloaded { .. } => "model_unloaded",
602 OutputEvent::HostElected { .. } => "host_elected",
603 OutputEvent::RpcServerStarting { .. } => "rpc_server_starting",
604 OutputEvent::RpcReady { .. } => "rpc_ready",
605 OutputEvent::RpcStartupFailed { .. } => "rpc_startup_failed",
606 OutputEvent::LlamaStarting { .. } => "llama_starting",
607 OutputEvent::LlamaReady { .. } => "llama_ready",
608 OutputEvent::LlamaStartupFailed { .. } => "llama_startup_failed",
609 OutputEvent::ModelReady { .. } => "model_ready",
610 OutputEvent::MultiModelMode { .. } => "multi_model_mode",
611 OutputEvent::WebserverStarting { .. } => "webserver_starting",
612 OutputEvent::WebserverReady { .. } => "webserver_ready",
613 OutputEvent::ApiStarting { .. } => "api_starting",
614 OutputEvent::ApiReady { .. } => "api_ready",
615 OutputEvent::RuntimeReady { .. } => "ready",
616 OutputEvent::ModelDownloadProgress { .. } => "model_download_progress",
617 OutputEvent::RequestRouted { .. } => "request_routed",
618 OutputEvent::Warning { .. } => "warning",
619 OutputEvent::Error { .. } => "error",
620 OutputEvent::Fatal { .. } => "fatal",
621 OutputEvent::ShutdownRequested { signal } => signal,
622 OutputEvent::Shutdown { .. } => "shutdown",
623 OutputEvent::LlamaNativeLog { category, .. } => category,
624 }
625 }
626
627 pub fn level(&self) -> OutputLevel {
628 match self {
629 OutputEvent::RpcStartupFailed { .. } | OutputEvent::LlamaStartupFailed { .. } => {
630 OutputLevel::Error
631 }
632 OutputEvent::LlamaNativeLog { .. } => OutputLevel::Debug,
633 OutputEvent::Warning { .. } => OutputLevel::Warn,
634 OutputEvent::Error { .. } => OutputLevel::Error,
635 OutputEvent::Fatal { .. } => OutputLevel::Fatal,
636 _ => OutputLevel::Info,
637 }
638 }
639
640 pub fn message(&self) -> String {
641 match self {
642 OutputEvent::Info { message, .. } => message.clone(),
643 OutputEvent::Startup { message, .. } => message
644 .clone()
645 .unwrap_or_else(|| "mesh-llm starting".to_string()),
646 OutputEvent::LaunchPlan { plan } => format!(
647 "startup plan ready ({} process(es), {} endpoint(s), {} model(s))",
648 plan.llama_process_rows.len(),
649 plan.webserver_rows.len(),
650 plan.loaded_model_rows.len()
651 ),
652 OutputEvent::NodeIdentity { node_id, mesh_id } => match mesh_id {
653 Some(mesh_id) => format!("node {node_id} joined mesh {mesh_id}"),
654 None => format!("node {node_id} initialized"),
655 },
656 OutputEvent::InviteToken {
657 mesh_id, mesh_name, ..
658 } => {
659 let mesh_label = format_invite_mesh_label(mesh_name.as_deref(), mesh_id);
660 format!("invite token ready for mesh {mesh_label}")
661 }
662 OutputEvent::DiscoveryStarting { source } => format!("discovering mesh via {source}"),
663 OutputEvent::MeshFound { mesh, peers, .. } => {
664 format!("discovered mesh {mesh} ({peers} peer(s))")
665 }
666 OutputEvent::DiscoveryJoined { mesh } => format!("joined mesh {mesh}"),
667 OutputEvent::DiscoveryFailed { message, detail } => match detail {
668 Some(detail) => format!("{message}: {detail}"),
669 None => message.clone(),
670 },
671 OutputEvent::WaitingForPeers { detail } => detail
672 .clone()
673 .unwrap_or_else(|| "waiting for peers".to_string()),
674 OutputEvent::PassiveMode {
675 role,
676 status,
677 capacity_gb,
678 models_on_disk,
679 detail,
680 } => {
681 let mut line = detail
682 .clone()
683 .unwrap_or_else(|| format!("{role} {}", status.as_str()));
684 if let Some(capacity_gb) = capacity_gb {
685 line.push_str(&format!(" ({capacity_gb:.1}GB capacity)"));
686 }
687 if let Some(models_on_disk) = models_on_disk
688 && !models_on_disk.is_empty()
689 {
690 line.push_str(&format!(" models={}", models_on_disk.join(", ")));
691 }
692 line
693 }
694 OutputEvent::PeerJoined { peer_id, .. } => format!("peer {peer_id} joined"),
695 OutputEvent::PeerLeft { peer_id, .. } => format!("peer {peer_id} left"),
696 OutputEvent::ModelQueued { model } => format!("queued model {model}"),
697 OutputEvent::ModelLoading { model, .. } => format!("loading model {model}"),
698 OutputEvent::ModelLoaded { model, .. } => format!("loaded model {model}"),
699 OutputEvent::ModelUnloading { model } => format!("unloading model {model}"),
700 OutputEvent::ModelUnloaded { model } => format!("unloaded model {model}"),
701 OutputEvent::HostElected {
702 model, host, role, ..
703 } => match role {
704 Some(role) => format!("{model} elected {host} as {role}"),
705 None => format!("{model} elected {host} as host"),
706 },
707 OutputEvent::RpcServerStarting { port, log_path, .. } => {
708 let msg = format!("rpc-server starting on port {port}");
709 append_log_path(msg, log_path)
710 }
711 OutputEvent::RpcReady { port, log_path, .. } => {
712 let msg = format!("rpc-server ready on port {port}");
713 append_log_path(msg, log_path)
714 }
715 OutputEvent::RpcStartupFailed {
716 port,
717 detail,
718 log_path,
719 ..
720 } => {
721 let msg = format!("rpc-server failed to start on port {port}: {detail}");
722 append_log_path(msg, log_path)
723 }
724 OutputEvent::LlamaStarting {
725 http_port,
726 log_path,
727 ..
728 } => {
729 let msg = format!("llama-server starting on port {http_port}");
730 append_log_path(msg, log_path)
731 }
732 OutputEvent::LlamaReady { port, log_path, .. } => {
733 let msg = format!("llama-server ready on port {port}");
734 append_log_path(msg, log_path)
735 }
736 OutputEvent::LlamaStartupFailed {
737 model,
738 http_port,
739 detail,
740 log_path,
741 ..
742 } => {
743 let msg = match model {
744 Some(model) => {
745 format!(
746 "llama-server failed to start for {model} on port {http_port}: {detail}"
747 )
748 }
749 None => format!("llama-server failed to start on port {http_port}: {detail}"),
750 };
751 append_log_path(msg, log_path)
752 }
753 OutputEvent::ModelReady {
754 model,
755 internal_port,
756 ..
757 } => match internal_port {
758 Some(port) => format!("model {model} ready on port {port}"),
759 None => format!("model {model} ready"),
760 },
761 OutputEvent::WebserverStarting { url } => format!("web console starting at {url}"),
762 OutputEvent::WebserverReady { url } => format!("web console ready at {url}"),
763 OutputEvent::ApiStarting { url } => format!("api starting at {url}"),
764 OutputEvent::ApiReady { url } => format!("api ready at {url}"),
765 OutputEvent::RuntimeReady { .. } => "mesh-llm runtime ready".to_string(),
766 OutputEvent::ModelDownloadProgress {
767 label,
768 file,
769 downloaded_bytes,
770 total_bytes,
771 status,
772 } => format_model_download_progress_message(
773 label,
774 file.as_deref(),
775 *downloaded_bytes,
776 *total_bytes,
777 status,
778 ),
779 OutputEvent::MultiModelMode { count, models } => {
780 if models.is_empty() {
781 format!("Multi-model mode: {count} model(s)")
782 } else {
783 format!("Multi-model mode: {count} model(s): {}", models.join(", "))
784 }
785 }
786 OutputEvent::RequestRouted { model, target } => {
787 format!("routed request for {model} to {target}")
788 }
789 OutputEvent::Warning { message, .. } => message.clone(),
790 OutputEvent::Error { message, .. } => message.clone(),
791 OutputEvent::Fatal { message, .. } => message.clone(),
792 OutputEvent::ShutdownRequested { signal } => format!("shutdown requested ({signal})"),
793 OutputEvent::Shutdown { reason } => reason
794 .clone()
795 .unwrap_or_else(|| "mesh-llm shutting down".to_string()),
796 OutputEvent::LlamaNativeLog { message, .. } => message.clone(),
797 }
798 }
799}
800
801fn append_log_path(message: String, log_path: &Option<String>) -> String {
802 if let Some(path) = log_path {
803 format!("{message}\n ↳ log={path}")
804 } else {
805 message
806 }
807}
808
809fn format_invite_mesh_label(mesh_name: Option<&str>, mesh_id: &str) -> String {
810 match mesh_name.map(str::trim).filter(|name| !name.is_empty()) {
811 Some(name) => format!("{name} ({mesh_id})"),
812 None => mesh_id.to_string(),
813 }
814}
815
816pub fn format_model_download_progress_message(
817 label: &str,
818 file: Option<&str>,
819 downloaded_bytes: Option<u64>,
820 total_bytes: Option<u64>,
821 status: &ModelProgressStatus,
822) -> String {
823 let target = file.unwrap_or(label);
824 if let Some(package) = label.strip_prefix("layer package ") {
825 return match status {
826 ModelProgressStatus::Ensuring => {
827 format!("ensuring layer package artifact {target} for {package}")
828 }
829 ModelProgressStatus::Downloading => match (downloaded_bytes, total_bytes) {
830 (Some(downloaded), Some(total)) if total > 0 => format!(
831 "downloading layer package artifact {target} for {package} {}/{}",
832 format_display_bytes(downloaded),
833 format_display_bytes(total)
834 ),
835 (Some(downloaded), _) if downloaded > 0 => format!(
836 "downloading layer package artifact {target} for {package} {}",
837 format_display_bytes(downloaded)
838 ),
839 _ => format!("downloading layer package artifact {target} for {package}"),
840 },
841 ModelProgressStatus::Ready => match total_bytes {
842 Some(total) if total > 0 => format!(
843 "layer package artifact {target} ready for {package} ({})",
844 format_display_bytes(total)
845 ),
846 _ => format!("layer package artifact {target} ready for {package}"),
847 },
848 };
849 }
850 match status {
851 ModelProgressStatus::Ensuring => format!("ensuring model {target}"),
852 ModelProgressStatus::Downloading => match (downloaded_bytes, total_bytes) {
853 (Some(downloaded), Some(total)) if total > 0 => format!(
854 "downloading model {target} {}/{}",
855 format_display_bytes(downloaded),
856 format_display_bytes(total)
857 ),
858 (Some(downloaded), _) if downloaded > 0 => {
859 format!(
860 "downloading model {target} {}",
861 format_display_bytes(downloaded)
862 )
863 }
864 _ => format!("downloading model {target}"),
865 },
866 ModelProgressStatus::Ready => match total_bytes {
867 Some(total) if total > 0 => {
868 format!("model {target} ready ({})", format_display_bytes(total))
869 }
870 _ => format!("model {target} ready"),
871 },
872 }
873}
874
875fn format_display_bytes(bytes: u64) -> String {
876 if bytes >= 1_000_000_000 {
877 format!("{:.1}GB", bytes as f64 / 1e9)
878 } else if bytes >= 1_000_000 {
879 format!("{:.0}MB", bytes as f64 / 1e6)
880 } else if bytes >= 1_000 {
881 format!("{:.0}KB", bytes as f64 / 1e3)
882 } else {
883 format!("{bytes}B")
884 }
885}