use std::net::{SocketAddr, ToSocketAddrs};
use std::path::PathBuf;
use clap::Parser;
use offgrid::{GpuLayers, ModelParams, SessionParams};
use offgrid_server::{AppState, ServerConfig, parse_gpu_layers, router};
#[derive(Parser, Debug)]
#[command(version, about = "OpenAI-compatible local inference server (offgrid)")]
struct Args {
#[arg(short, long, env = "OFFGRID_MODEL")]
model: Option<PathBuf>,
#[arg(long, env = "OFFGRID_MMPROJ")]
mmproj: Option<PathBuf>,
#[arg(long, env = "OFFGRID_EMBEDDING_MODEL")]
embedding_model: Option<PathBuf>,
#[arg(long)]
alias: Option<String>,
#[arg(long)]
embedding_alias: Option<String>,
#[arg(long, default_value = "127.0.0.1", env = "OFFGRID_HOST")]
host: String,
#[arg(short, long, default_value_t = 8080, env = "OFFGRID_PORT")]
port: u16,
#[arg(short = 'c', long, default_value_t = 0)]
ctx: u32,
#[arg(long, default_value_t = 1)]
parallel: usize,
#[arg(long, default_value = "all", value_parser = parse_gpu_layers)]
gpu_layers: GpuLayers,
#[arg(long = "device")]
devices: Vec<String>,
#[arg(long)]
threads: Option<i32>,
#[arg(long, default_value_t = 64)]
max_body_mb: usize,
#[arg(long, env = "OFFGRID_API_KEY", hide_env_values = true)]
api_key: Option<String>,
}
fn main() {
let args = Args::parse();
init_logging();
let config = ServerConfig {
model: args.model,
embedding_model: args.embedding_model,
alias: args.alias,
embedding_alias: args.embedding_alias,
mmproj: args.mmproj,
model_params: ModelParams {
gpu_layers: args.gpu_layers,
devices: (!args.devices.is_empty()).then_some(args.devices),
mmproj: None,
},
session: SessionParams { n_ctx: args.ctx, n_threads: args.threads, ..Default::default() },
parallel: args.parallel,
api_key: args.api_key,
max_body_bytes: args.max_body_mb << 20,
};
let host = args.host.trim_start_matches('[').trim_end_matches(']').to_string();
let pick = |addrs: Vec<SocketAddr>| addrs.iter().find(|a| a.is_ipv4()).or(addrs.first()).copied();
let addr: SocketAddr = match (host.as_str(), args.port).to_socket_addrs().map(|a| pick(a.collect())) {
Ok(Some(a)) => a,
Ok(None) => fail(format!("--host {} resolved to no address", args.host)),
Err(e) => fail(format!("invalid --host/--port: {e}")),
};
if !addr.ip().is_loopback() && config.api_key.is_none() {
log::warn!("listening on {addr} without --api-key: anyone on the network can use this server");
}
for d in offgrid::devices() {
log::info!("device {} ({:?}): {}", d.name, d.kind, d.description);
}
let state = AppState::load(&config).unwrap_or_else(|e| fail(format!("startup failed: {e}")));
let runtime = tokio::runtime::Runtime::new().unwrap_or_else(|e| fail(e.to_string()));
runtime.block_on(async move {
let listener = tokio::net::TcpListener::bind(addr).await.unwrap_or_else(|e| fail(format!("bind {addr}: {e}")));
log::info!("offgrid-server listening on http://{addr}");
let shutdown_state = state.clone();
axum::serve(listener, router(state))
.with_graceful_shutdown(async move {
shutdown_signal().await;
log::info!("shutting down (Ctrl-C again to exit immediately)");
shutdown_state.shutdown();
tokio::spawn(async {
shutdown_signal().await;
log::warn!("second signal: exiting now");
std::process::exit(130);
});
})
.await
.unwrap_or_else(|e| fail(e.to_string()));
});
}
async fn shutdown_signal() {
let ctrl_c = async {
let _ = tokio::signal::ctrl_c().await;
};
#[cfg(unix)]
let term = async {
if let Ok(mut s) = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
s.recv().await;
}
};
#[cfg(not(unix))]
let term = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = term => {},
}
}
fn init_logging() {
struct Logger {
level: log::LevelFilter,
llama: log::LevelFilter,
start: std::time::Instant,
}
impl log::Log for Logger {
fn enabled(&self, m: &log::Metadata) -> bool {
m.level() <= if m.target() == "llama.cpp" { self.llama } else { self.level }
}
fn log(&self, r: &log::Record) {
if self.enabled(r.metadata()) {
let t = self.start.elapsed().as_secs_f64();
eprintln!("[{t:7.2}s {} {}] {}", r.level(), r.target(), r.args());
}
}
fn flush(&self) {}
}
let env = std::env::var("OFFGRID_LOG").ok().and_then(|v| v.parse().ok());
let level = env.unwrap_or(log::LevelFilter::Info);
let llama = env.unwrap_or(log::LevelFilter::Warn);
let logger = Logger { level, llama, start: std::time::Instant::now() };
if log::set_logger(Box::leak(Box::new(logger))).is_ok() {
log::set_max_level(level.max(llama));
}
}
fn fail(msg: String) -> ! {
eprintln!("offgrid-server: {msg}");
std::process::exit(1)
}