use std::path::PathBuf;
use clap::{Parser, Subcommand};
use modelpipe::{ConnectOptions, ServeOptions, Ticket, TokenPolicy};
mod diagnostics;
mod interrupt;
mod park;
use interrupt::Interrupt;
use park::{park, shut_down};
#[derive(Parser)]
#[command(
name = "modelpipe",
version,
about = "Your model server, from anywhere"
)]
struct Cli {
#[expect(clippy::doc_markdown, reason = "clap help text, not rustdoc")]
#[arg(short, long, global = true, action = clap::ArgAction::Count)]
verbose: u8,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Serve {
#[expect(clippy::doc_markdown, reason = "clap help text, not rustdoc")]
backend_url: String,
#[arg(long)]
insecure_no_auth: bool,
#[expect(clippy::doc_markdown, reason = "clap help text, not rustdoc")]
#[arg(
long,
env = "MODELPIPE_TOKEN",
hide_env_values = true,
conflicts_with = "insecure_no_auth"
)]
token: Option<String>,
#[arg(long, conflicts_with_all = ["insecure_no_auth", "token"])]
token_file: Option<PathBuf>,
#[arg(long)]
allow_private_backend: bool,
#[arg(long)]
relay: Option<String>,
#[arg(long, value_name = "FILE")]
identity: Option<PathBuf>,
#[arg(long)]
no_qr: bool,
},
Connect {
ticket: String,
#[arg(long)]
bind: Option<std::net::SocketAddr>,
},
}
fn token_policy(
token: Option<String>,
token_file: Option<PathBuf>,
insecure: bool,
) -> anyhow::Result<TokenPolicy> {
Ok(match (token, token_file, insecure) {
(Some(t), _, _) if t.trim().is_empty() => {
anyhow::bail!("the bearer token is empty — unset MODELPIPE_TOKEN or pass a value")
}
(Some(t), _, _) => TokenPolicy::Supplied(t),
(None, Some(path), _) => {
let raw = std::fs::read_to_string(&path)
.map_err(|e| anyhow::anyhow!("could not read {}: {e}", path.display()))?;
let trimmed = raw.trim_end_matches(['\n', '\r']).to_owned();
if trimmed.is_empty() {
anyhow::bail!("{} is empty", path.display());
}
TokenPolicy::Supplied(trimmed)
}
(None, None, true) => TokenPolicy::InsecureNoAuth,
(None, None, false) => TokenPolicy::Generate,
})
}
fn qr(ticket: &Ticket) -> Option<String> {
use qrcode::QrCode;
use qrcode::render::unicode;
let code = QrCode::new(ticket.to_string().to_uppercase()).ok()?;
Some(code.render::<unicode::Dense1x2>().quiet_zone(true).build())
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
diagnostics::install(cli.verbose);
let mut interrupt = Interrupt::new()?;
match cli.command {
Command::Serve {
backend_url,
insecure_no_auth,
token,
token_file,
allow_private_backend,
relay,
identity,
no_qr,
} => {
let mut opts = ServeOptions::default();
opts.auth = token_policy(token, token_file, insecure_no_auth)?;
opts.allow_private_backend = allow_private_backend;
opts.relay = relay;
let ephemeral = identity.is_none();
opts.identity = identity;
let mut handle = modelpipe::serve(&backend_url, opts).await?;
let ticket = handle.ticket();
println!("ticket: {ticket}");
match handle.token() {
Some(token) => println!("token: {token}"),
None => eprintln!(
"WARNING: serving open — anyone holding the ticket can use your backend"
),
}
if ephemeral {
eprintln!(
"note: this ticket dies when serve restarts — \
pass --identity <file> to keep it across restarts"
);
}
if !no_qr && let Some(code) = qr(&ticket) {
println!("\n{code}");
}
park(&mut handle, &mut interrupt).await?;
shut_down(handle.shutdown(), &mut interrupt).await;
}
Command::Connect { ticket, bind } => {
let ticket: Ticket = ticket.parse()?;
if let Some(addr) = bind
&& !addr.ip().is_loopback()
{
eprintln!(
"WARNING: binding {addr} exposes the pipe beyond this machine — anyone who can reach that port can reach the backend (with the token)"
);
}
let mut opts = ConnectOptions::default();
opts.bind = bind;
let mut handle = modelpipe::connect(&ticket, opts).await?;
println!("{}", handle.base_url());
park(&mut handle, &mut interrupt).await?;
shut_down(handle.shutdown(), &mut interrupt).await;
}
}
Ok(())
}
#[cfg(test)]
#[path = "main_tests.rs"]
mod main_tests;