Skip to main content

batuta/serve/banco/
server.rs

1//! Banco TCP server binding with graceful shutdown.
2
3use super::router::create_banco_router;
4use super::state::BancoState;
5use tokio::net::TcpListener;
6
7/// Start the Banco HTTP server with graceful shutdown on SIGTERM/SIGINT.
8pub async fn start_server(host: &str, port: u16, state: BancoState) -> anyhow::Result<()> {
9    let addr = format!("{host}:{port}");
10    let listener = TcpListener::bind(&addr).await?;
11
12    let model_status = if let Some(info) = state.model.info() {
13        format!("{} ({})", info.model_id, format!("{:?}", info.format).to_lowercase())
14    } else {
15        "none — load via POST /api/v1/models/load or --model flag".to_string()
16    };
17
18    let tokenizer_status = {
19        #[cfg(feature = "aprender")]
20        {
21            if state.model.has_bpe_tokenizer() {
22                "BPE (proper merge rules)"
23            } else if state.model.is_loaded() {
24                "greedy (no tokenizer.json found)"
25            } else {
26                "n/a"
27            }
28        }
29        #[cfg(not(feature = "aprender"))]
30        {
31            "greedy (aprender not enabled)"
32        }
33    };
34
35    eprintln!("┌──────────────────────────────────────────────────┐");
36    eprintln!("│  Banco – Local AI Workbench                      │");
37    eprintln!("├──────────────────────────────────────────────────┤");
38    eprintln!("│  Listening:  {addr:<36}│");
39    eprintln!("│  Privacy:    {:?}", state.privacy_tier);
40    eprintln!("│  Model:      {model_status}");
41    eprintln!("│  Tokenizer:  {tokenizer_status}");
42    eprintln!("│  Telemetry:  disabled");
43    eprintln!("├──────────────────────────────────────────────────┤");
44    eprintln!("│  Browser:    http://{addr}/ (chat UI)");
45    eprintln!("│  Core:       /health /api/v1/models /api/v1/system");
46    eprintln!("│  Chat:       /api/v1/chat/completions (SSE)");
47    eprintln!("│  Completions:/v1/completions (text, OpenAI-compat)");
48    eprintln!("│  Data:       /api/v1/tokenize /detokenize /embeddings");
49    eprintln!("│  Models:     /api/v1/models/load|unload|status|:id");
50    eprintln!("│  Chat cfg:   /api/v1/chat/parameters");
51    eprintln!("│  Convos:     /api/v1/conversations + /search /export /import");
52    eprintln!("│  Presets:    /api/v1/prompts");
53    eprintln!("│  Files:      /api/v1/data/upload /files");
54    eprintln!("│  Recipes:    /api/v1/data/recipes /datasets");
55    eprintln!("│  RAG:        /api/v1/rag/index /status /search");
56    eprintln!("│  Eval:       /api/v1/eval/perplexity /runs");
57    eprintln!("│  Training:   /api/v1/train/start /runs /presets /metrics /export");
58    eprintln!("│  Merge:      /api/v1/models/merge /strategies (TIES/DARE/SLERP)");
59    eprintln!("│  Experiments:/api/v1/experiments /compare");
60    eprintln!("│  Batch:      /api/v1/batch");
61    eprintln!("│  Registry:   /api/v1/models/pull /registry (pacha)");
62    eprintln!("│  Audio:      /api/v1/audio/transcriptions (whisper-apr)");
63    eprintln!("│  MCP:        /api/v1/mcp (Model Context Protocol)");
64    eprintln!("│  Tools:      /api/v1/tools (calculator, code_execution, web_search)");
65    eprintln!("│  Metrics:    /api/v1/metrics (Prometheus)");
66    eprintln!("│  WebSocket:  /api/v1/ws (real-time events)");
67    eprintln!("│  OpenAI:     /v1/models /v1/completions /v1/chat/completions /v1/embeddings");
68    eprintln!("│  Ollama:     /api/generate /api/chat /api/tags /api/show /api/pull /api/delete");
69    eprintln!("├──────────────────────────────────────────────────┤");
70    let sys = state.system_info();
71    eprintln!("│  Data:       {} files, {} conversations", sys.files, sys.conversations);
72    eprintln!(
73        "│  RAG:        {}",
74        if sys.rag_indexed {
75            format!("{} chunks indexed", sys.rag_chunks)
76        } else {
77            "empty".to_string()
78        }
79    );
80    eprintln!("│  Storage:    ~/.banco/");
81    eprintln!("│  Shutdown:   Ctrl+C (graceful drain)");
82    eprintln!("└──────────────────────────────────────────────────┘");
83
84    let app = create_banco_router(state);
85
86    axum::serve(listener, app).with_graceful_shutdown(shutdown_signal()).await?;
87
88    eprintln!("[banco] Graceful shutdown complete");
89    Ok(())
90}
91
92/// Wait for shutdown signal (Ctrl+C / SIGTERM).
93async fn shutdown_signal() {
94    let ctrl_c = async {
95        tokio::signal::ctrl_c().await.expect("failed to install Ctrl+C handler");
96    };
97
98    #[cfg(unix)]
99    let terminate = async {
100        tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
101            .expect("failed to install signal handler")
102            .recv()
103            .await;
104    };
105
106    #[cfg(not(unix))]
107    let terminate = std::future::pending::<()>();
108
109    tokio::select! {
110        () = ctrl_c => eprintln!("\n[banco] Received Ctrl+C, shutting down..."),
111        () = terminate => eprintln!("\n[banco] Received SIGTERM, shutting down..."),
112    }
113}