use std::path::Path;
use std::io::IsTerminal as _;
use clap::Subcommand;
use newt_core::dgx::{DgxConfig, DgxNode, EndpointKind};
use newt_core::retry::{with_backoff, RetryPolicy};
use newt_core::router::Classification;
use newt_core::{Config, Router, Tier};
use newt_inference::local::LocalVllmBackend;
use crate::dgx_registry::{self, InferenceTool, ModelVariant};
use crate::dgx_status;
use crate::dgx_vllm;
mod ssh;
use ssh::{detect_node_mem, detect_node_mem_available, RealSsh, RealSshCapture, SshExec};
#[cfg(test)]
use ssh::{node_mem_probe, MEM_AVAILABLE_AWK, MEM_TOTAL_AWK};
#[derive(Subcommand, Debug)]
pub enum DgxCmd {
Setup {
#[arg(long)]
host: Option<String>,
#[arg(long, default_value = "dgx")]
name: String,
#[arg(long)]
model: Option<String>,
#[arg(long)]
template: bool,
#[arg(long, short = 'y')]
yes: bool,
},
Route {
task: String,
},
Status {
#[arg(long)]
node: Option<String>,
#[arg(long)]
json: bool,
},
Deploy {
#[arg(long, conflicts_with = "model")]
strongest: bool,
#[arg(required_unless_present = "strongest")]
model: Option<String>,
#[arg(long)]
node: Option<String>,
},
Clear {
#[arg(long)]
node: Option<String>,
#[arg(long)]
restart_ollama: bool,
},
Models,
Doctor,
Use {
model: String,
},
Warm {
model: Option<String>,
#[arg(long, default_value = "30m")]
keep_alive: String,
#[arg(long)]
evict_vllm: bool,
},
Pull {
model: String,
#[arg(long)]
node: Option<String>,
#[arg(long)]
name: Option<String>,
#[arg(long)]
force: bool,
#[arg(long)]
dry_run: bool,
},
Rm {
model: String,
},
Ps,
Vllm {
#[command(subcommand)]
cmd: VllmCmd,
},
Card {
#[command(subcommand)]
cmd: CardCmd,
},
Gpu,
Switch {
engine: String,
model: String,
#[arg(long)]
node: Option<String>,
#[arg(long)]
dry_run: bool,
},
Adopt {
#[arg(long, conflicts_with = "enforce")]
adopt: bool,
#[arg(long, conflicts_with = "adopt")]
enforce: bool,
#[arg(long)]
node: Option<String>,
},
}
#[derive(clap::Args, Debug)]
pub struct VllmPlanArgs {
#[arg(long)]
pub served_name: Option<String>,
#[arg(long)]
pub dtype: Option<String>,
#[arg(long, default_value_t = 1)]
pub tensor_parallel: u8,
#[arg(long)]
pub max_model_len: Option<u32>,
#[arg(long, default_value_t = 0.90)]
pub gpu_mem_util: f64,
#[arg(long, default_value_t = 8000)]
pub port: u16,
#[arg(long)]
pub docker: bool,
#[arg(long)]
pub extra: Vec<String>,
}
#[derive(Subcommand, Debug)]
pub enum VllmCmd {
Up {
model: String,
#[arg(long)]
node: Option<String>,
#[command(flatten)]
plan: VllmPlanArgs,
#[arg(long)]
force: bool,
#[arg(long)]
evict_ollama: bool,
#[arg(long)]
dry_run: bool,
},
Down {
served_name: Option<String>,
#[arg(long)]
node: Option<String>,
#[arg(long)]
dry_run: bool,
},
Ps,
Config {
model: String,
#[command(flatten)]
plan: VllmPlanArgs,
},
Logs {
served_name: Option<String>,
#[arg(long)]
node: Option<String>,
#[arg(long, default_value_t = 50)]
lines: u32,
#[arg(long)]
dry_run: bool,
},
}
#[derive(Subcommand, Debug)]
pub enum CardCmd {
List {
#[arg(long)]
json: bool,
},
Show {
name: String,
#[arg(long)]
json: bool,
},
Validate {
path: std::path::PathBuf,
},
Setup {
name: String,
#[arg(long)]
model: Option<String>,
#[arg(long)]
node: Option<String>,
#[arg(long)]
backend: Option<String>,
#[arg(long)]
dry_run: bool,
#[arg(long)]
force: bool,
},
Pick {
#[arg(long)]
model: Option<String>,
#[arg(long)]
node: Option<String>,
#[arg(long)]
backend: Option<String>,
#[arg(long)]
dry_run: bool,
#[arg(long)]
force: bool,
},
}
pub async fn run(cmd: DgxCmd, config_path: Option<&Path>) -> anyhow::Result<()> {
match cmd {
DgxCmd::Setup {
host,
name,
model,
template,
yes,
} => setup(
config_path,
host.as_deref(),
&name,
model.as_deref(),
template,
yes,
),
DgxCmd::Route { task } => route(&task, config_path),
DgxCmd::Status { node, json } => status(config_path, node.as_deref(), json).await,
DgxCmd::Deploy {
strongest,
model,
node,
} => deploy(config_path, strongest, model.as_deref(), node.as_deref()).await,
DgxCmd::Clear {
node,
restart_ollama,
} => clear(config_path, node.as_deref(), restart_ollama).await,
DgxCmd::Models => models(config_path).await,
DgxCmd::Doctor => doctor(config_path).await,
DgxCmd::Use { model } => use_model(config_path, &model),
DgxCmd::Warm {
model,
keep_alive,
evict_vllm,
} => warm(config_path, model, &keep_alive, evict_vllm).await,
DgxCmd::Pull {
model,
node,
name,
force,
dry_run,
} => {
pull(
config_path,
&model,
node.as_deref(),
name.as_deref(),
force,
dry_run,
)
.await
}
DgxCmd::Rm { model } => rm(config_path, &model).await,
DgxCmd::Ps => ps(config_path).await,
DgxCmd::Vllm { cmd } => match cmd {
VllmCmd::Up {
model,
node,
plan,
force,
evict_ollama,
dry_run,
} => {
vllm_up(
config_path,
&model,
node.as_deref(),
&plan,
force,
evict_ollama,
dry_run,
)
.await
}
VllmCmd::Down {
served_name,
node,
dry_run,
} => {
vllm_down(
config_path,
served_name.as_deref(),
node.as_deref(),
dry_run,
)
.await
}
VllmCmd::Ps => vllm_ps(config_path).await,
VllmCmd::Config { model, plan } => vllm_config(&model, &plan),
VllmCmd::Logs {
served_name,
node,
lines,
dry_run,
} => {
vllm_logs(
config_path,
served_name.as_deref(),
node.as_deref(),
lines,
dry_run,
)
.await
}
},
DgxCmd::Card { cmd } => crate::dgx_card::run(cmd, config_path).await,
DgxCmd::Gpu => gpu(config_path).await,
DgxCmd::Switch {
engine,
model,
node,
dry_run,
} => switch(config_path, &engine, &model, node.as_deref(), dry_run).await,
DgxCmd::Adopt {
adopt,
enforce,
node,
} => adopt_cmd(config_path, adopt, enforce, node.as_deref()).await,
}
}
fn setup(
config_path: Option<&Path>,
host: Option<&str>,
name: &str,
model: Option<&str>,
template: bool,
yes: bool,
) -> anyhow::Result<()> {
if template {
let tmpl = DgxConfig::home_template();
let text = toml::to_string_pretty(&tmpl)
.map_err(|e| anyhow::anyhow!("TOML serialisation failed: {e}"))?;
println!("# [dgx] reference template");
println!("# Copy into ~/.newt/config.toml under [dgx]\n");
print!("{text}");
return Ok(());
}
let Some(host) = host else {
eprintln!("Usage: newt dgx setup --host <hostname-or-ip> [--model <model>] [--yes]");
eprintln!(" newt dgx setup --template");
eprintln!("\nExamples:");
eprintln!(" newt dgx setup --host REDACTED-IP --model qwen2.5-coder:32b --yes");
eprintln!(" newt dgx setup --host REDACTED-HOST --name home");
return Ok(());
};
let node = DgxNode {
name: name.to_string(),
ollama: Some(format!("http://{host}:11434")),
vllm: Some(format!("http://{host}:8000")),
ssh_host: Some(host.to_string()),
..Default::default()
};
let dgx = DgxConfig {
active_node: Some(name.to_string()),
active_endpoint: EndpointKind::Ollama,
active_model: model.map(str::to_string),
nodes: vec![node],
formations: vec![],
};
let text = toml::to_string_pretty(&dgx)
.map_err(|e| anyhow::anyhow!("TOML serialisation failed: {e}"))?;
let save_path = config_path
.map(std::path::PathBuf::from)
.or_else(newt_core::Config::user_config_path)
.ok_or_else(|| anyhow::anyhow!("cannot determine config file path (HOME unset?)"))?;
println!("# [dgx] config to write to {}:\n", save_path.display());
print!("{text}");
if !yes {
let window = newt_core::tty::Terminal::suspend_for_prompt(
newt_core::tty::TerminalTaker::PlainCliConfirm,
);
if !newt_core::interaction_terminal::confirmed_on_terminal(
&window,
&newt_core::interaction_form::confirm("Write?", "", "yes, write it", "no, stop"),
false,
) {
window.notice("Aborted.")?;
return Ok(());
}
}
let mut config = match config_path {
Some(p) if p.exists() => crate::migration_notices::read(|report| Config::load(p, report))
.map_err(anyhow::Error::from)?,
Some(_) => Config::default(),
None => crate::migration_notices::read(|report| Config::resolve(report))
.map_err(anyhow::Error::from)?,
};
config.dgx = Some(dgx);
config.save(&save_path)?;
println!("Saved → {}", save_path.display());
Ok(())
}
fn route(task: &str, config_path: Option<&Path>) -> anyhow::Result<()> {
let config = load_config(config_path)?;
let classification = Router::new().classify_detailed(task);
let rec = recommend(config.dgx.as_ref(), &classification);
println!(
" Complexity: {} (confidence {:.2})",
tier_label(rec.tier),
rec.confidence
);
match &rec.formation {
Some(name) => println!(" Formation: {name}"),
None => println!(" Formation: (none configured — set NEWT_DGX_HOST=<host>)"),
}
match &rec.model {
Some(model) => println!(" Model: {model}"),
None => println!(" Model: (unset — set [dgx].active_model or NEWT_DGX_MODEL)"),
}
println!(" Endpoint: {}", rec.endpoint);
println!(" Why: {}", rec.why);
Ok(())
}
struct Recommendation {
tier: Tier,
confidence: f64,
formation: Option<String>,
model: Option<String>,
endpoint: EndpointKind,
why: String,
}
fn recommend(dgx: Option<&DgxConfig>, c: &Classification) -> Recommendation {
let why = c
.reasons
.first()
.cloned()
.unwrap_or_else(|| "no routing signal".to_string());
if let Some(formation) = dgx.and_then(|d| d.formation(preferred_formation(c.tier))) {
return Recommendation {
tier: c.tier,
confidence: c.confidence,
formation: Some(formation.name.clone()),
model: Some(formation.model.clone()),
endpoint: formation.endpoint,
why,
};
}
Recommendation {
tier: c.tier,
confidence: c.confidence,
formation: None,
model: dgx.and_then(|d| d.active_model.clone()),
endpoint: default_endpoint_for(c.tier),
why,
}
}
fn preferred_formation(tier: Tier) -> &'static str {
match tier {
Tier::Review => "review",
Tier::Complex => "coding",
Tier::Standard => "standard",
Tier::Fast => "fast",
}
}
fn default_endpoint_for(tier: Tier) -> EndpointKind {
match tier {
Tier::Review | Tier::Complex => EndpointKind::InCluster,
Tier::Standard => EndpointKind::OllamaLb,
Tier::Fast => EndpointKind::Ollama,
}
}
fn tier_label(tier: Tier) -> &'static str {
match tier {
Tier::Fast => "fast",
Tier::Standard => "standard",
Tier::Complex => "complex",
Tier::Review => "review",
}
}
async fn models(config_path: Option<&Path>) -> anyhow::Result<()> {
let dgx = dgx_config(config_path)?;
let kind = dgx.active_endpoint;
let base = dgx.resolve_endpoint()?;
println!("Models on {kind} endpoint ({base}):");
let names = if kind.is_openai_compatible() {
LocalVllmBackend::new(base.as_str(), "")
.list_models()
.await?
.into_iter()
.map(|m| m.id)
.collect::<Vec<_>>()
} else {
fetch_ollama_models(&http_client(), &base).await?
};
if names.is_empty() {
println!(" (none)");
}
for name in &names {
println!(" {name}");
}
Ok(())
}
async fn status(config_path: Option<&Path>, node: Option<&str>, json: bool) -> anyhow::Result<()> {
let mut dgx = dgx_config(config_path)?;
if let Some(n) = node {
dgx.active_node = Some(n.to_string());
}
if json {
let host = dgx.ssh_host()?;
let user = dgx.ssh_user();
let budget = dgx_status::gather_budget(&RealSshCapture, &user, &host, None)?;
println!(
"{}",
serde_json::to_string_pretty(&dgx_status::budget_json(&budget))?
);
return Ok(());
}
let kind = dgx.active_endpoint;
let base = dgx.resolve_endpoint()?;
let client = http_client();
let health_path = if kind.is_openai_compatible() {
"/v1/models"
} else {
"/api/tags"
};
println!("DGX status — {kind} endpoint ({base})");
println!(" Health: {}", probe(&client, &base, health_path).await);
if kind.is_openai_compatible() {
println!(" Running: (vLLM exposes no running-models endpoint)");
} else {
match fetch_ollama_running(&client, &base).await {
Ok(names) if names.is_empty() => println!(" Running: (no models loaded)"),
Ok(names) => println!(" Running: {}", names.join(", ")),
Err(e) => println!(" Running: (unavailable: {e})"),
}
}
match dgx.ssh_host() {
Ok(host) => {
let user = dgx.ssh_user();
match dgx_status::gather_budget(&RealSshCapture, &user, &host, None) {
Ok(budget) => print!("{}", dgx_status::render_budget_human(&host, &budget)),
Err(e) => println!(" Memory: (unavailable over SSH: {e})"),
}
}
Err(_) => {
println!(" Memory: (no ssh_host configured — set [dgx].nodes[].ssh_host)");
}
}
Ok(())
}
async fn doctor(config_path: Option<&Path>) -> anyhow::Result<()> {
let dgx = dgx_config(config_path)?;
let client = http_client();
println!("newt dgx doctor — probing configured endpoints\n");
let mut any = false;
for kind in EndpointKind::ALL {
let label = kind.as_str();
match dgx.resolve_endpoint_for(kind) {
Ok(base) => {
any = true;
let path = if kind.is_openai_compatible() {
"/v1/models"
} else {
"/api/tags"
};
let health = probe(&client, &base, path).await;
println!(" {label:<10} {base} — {health}");
}
Err(_) => println!(" {label:<10} (not set)"),
}
}
if !any {
println!("\n No DGX endpoints configured. Set:");
println!(" NEWT_DGX_HOST=<host> (synthesizes ollama + vllm URLs from a bare hostname)");
println!(" NEWT_DGX_OLLAMA_URL=<url> (direct URL, e.g. https://REDACTED-HOST)");
}
println!("\n DNS note: some mesh routers intercept certain local TLDs (e.g. .lan), so a");
println!(
" custom DNS suffix may resolve from some hosts but not others; prefer an IP. Inside"
);
println!(
" k3s use the in_cluster proxy (http://ollama-proxy.inference.svc.cluster.local:11434)."
);
Ok(())
}
fn dgx_config(config_path: Option<&Path>) -> anyhow::Result<DgxConfig> {
Ok(load_config(config_path)?.dgx.unwrap_or_default())
}
fn load_config(config_path: Option<&Path>) -> anyhow::Result<Config> {
let config = match config_path {
Some(p) => crate::migration_notices::read(|report| Config::load(p, report))?,
None => crate::migration_notices::read(|report| Config::resolve(report))?,
};
Ok(config)
}
fn http_client() -> reqwest::Client {
reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(5))
.build()
.expect("build reqwest client")
}
async fn fetch_ollama_models(client: &reqwest::Client, base: &str) -> anyhow::Result<Vec<String>> {
fetch_names(client, base, "/api/tags").await
}
async fn fetch_ollama_running(client: &reqwest::Client, base: &str) -> anyhow::Result<Vec<String>> {
Ok(
newt_core::backend_probe::fetch_ollama_ps(client, base, None)
.await?
.into_iter()
.map(|m| m.name)
.collect(),
)
}
async fn fetch_names(
client: &reqwest::Client,
base: &str,
path: &str,
) -> anyhow::Result<Vec<String>> {
let url = format!("{}{path}", base.trim_end_matches('/'));
let resp = client
.get(&url)
.send()
.await
.map_err(|e| anyhow::anyhow!("request failed: {e}"))?;
if !resp.status().is_success() {
anyhow::bail!("HTTP {}", resp.status());
}
let json: serde_json::Value = resp.json().await?;
Ok(extract_names(&json["models"]))
}
fn extract_names(models: &serde_json::Value) -> Vec<String> {
models
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|m| m["name"].as_str().map(str::to_string))
.collect()
})
.unwrap_or_default()
}
async fn probe(client: &reqwest::Client, base: &str, path: &str) -> String {
let url = format!("{}{path}", base.trim_end_matches('/'));
match client.get(&url).send().await {
Ok(resp) if resp.status().is_success() => "OK".to_string(),
Ok(resp) => format!("HTTP {}", resp.status()),
Err(e) => format!("unreachable: {e}"),
}
}
fn use_model(config_path: Option<&Path>, model: &str) -> anyhow::Result<()> {
let mut config = load_config(config_path)?;
let dgx = config.dgx.get_or_insert_with(Default::default);
dgx.active_model = Some(model.to_string());
let save_path = config_path
.map(std::path::PathBuf::from)
.or_else(newt_core::Config::user_config_path)
.ok_or_else(|| anyhow::anyhow!("cannot determine config file path"))?;
config.save(&save_path)?;
println!("Active model set to {model}");
println!("Saved → {}", save_path.display());
Ok(())
}
fn warm_body(model: &str, keep_alive: &str) -> serde_json::Value {
serde_json::json!({ "model": model, "keep_alive": keep_alive, "stream": false })
}
async fn warm_model(
client: &reqwest::Client,
base: &str,
model: &str,
keep_alive: &str,
) -> anyhow::Result<Option<f64>> {
let url = format!("{}/api/generate", base.trim_end_matches('/'));
let resp = client
.post(&url)
.json(&warm_body(model, keep_alive))
.send()
.await
.map_err(|e| anyhow::anyhow!("request failed: {e}"))?;
if !resp.status().is_success() {
anyhow::bail!("HTTP {}", resp.status());
}
let json: serde_json::Value = resp.json().await?;
Ok(json["load_duration"].as_u64().map(|ns| ns as f64 / 1e9))
}
fn resolve_warm_model(
model: Option<String>,
evict_vllm: bool,
dgx: &DgxConfig,
) -> anyhow::Result<String> {
resolve_warm_model_with(model, evict_vllm, dgx, &|k| std::env::var(k).ok())
}
fn resolve_warm_model_with(
model: Option<String>,
evict_vllm: bool,
dgx: &DgxConfig,
get_env: &dyn Fn(&str) -> Option<String>,
) -> anyhow::Result<String> {
if let Some(m) = model {
return Ok(m);
}
if evict_vllm {
anyhow::bail!(
"--evict-vllm needs an explicit Ollama model to warm (the active model {:?} \
is the vLLM served name, now stopped) — e.g. \
`dgx warm --evict-vllm qwen2.5-coder:32b`",
dgx.active_model.as_deref().unwrap_or("<none>")
);
}
Ok(dgx.resolve_active_model_with(get_env)?)
}
async fn warm(
config_path: Option<&Path>,
model: Option<String>,
keep_alive: &str,
evict_vllm: bool,
) -> anyhow::Result<()> {
let dgx = dgx_config(config_path)?;
let (kind, base) = if evict_vllm {
evict_vllm_server(&RealSsh, &dgx).await?;
(
EndpointKind::Ollama,
dgx.resolve_endpoint_for(EndpointKind::Ollama)?,
)
} else {
let kind = dgx.active_endpoint;
if kind.is_openai_compatible() {
anyhow::bail!(
"`newt dgx warm` targets Ollama endpoints; the active endpoint is vLLM \
(vLLM keeps its served model resident already). Pass --evict-vllm to stop \
the vLLM server and warm an Ollama model instead."
);
}
(kind, dgx.resolve_endpoint()?)
};
let model = resolve_warm_model(model, evict_vllm, &dgx)?;
println!("Warming {model} on {kind} endpoint ({base}) — keep_alive={keep_alive}");
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(600))
.build()
.expect("build reqwest client");
match warm_model(&client, &base, &model, keep_alive).await? {
Some(secs) => println!(" loaded in {secs:.1}s — now resident"),
None => println!(" ready (already resident)"),
}
Ok(())
}
use crate::dgx_pull::{self, fit_check, FitVerdict, GgufFile, ModelRef, PullPlan};
async fn fetch_hf_siblings(
client: &reqwest::Client,
hf_base: &str,
org: &str,
repo: &str,
) -> anyhow::Result<Vec<GgufFile>> {
let url = format!(
"{}/api/models/{org}/{repo}?blobs=true",
hf_base.trim_end_matches('/')
);
let resp = client
.get(&url)
.send()
.await
.map_err(|e| anyhow::anyhow!("HF API request failed: {e}"))?;
if !resp.status().is_success() {
anyhow::bail!("HF API returned HTTP {}", resp.status());
}
let json: serde_json::Value = resp.json().await?;
Ok(dgx_pull::parse_gguf_siblings(&json))
}
async fn pull(
config_path: Option<&Path>,
model: &str,
node: Option<&str>,
name: Option<&str>,
force: bool,
dry_run: bool,
) -> anyhow::Result<()> {
let mut dgx = dgx_config(config_path)?;
if let Some(node) = node {
dgx.active_node = Some(node.to_string());
}
let user = dgx.ssh_user();
let host = dgx.ssh_host()?;
let has_token = std::env::var("HF_TOKEN").is_ok_and(|t| !t.trim().is_empty());
match ModelRef::parse(model) {
ModelRef::Ollama { name: tag } => {
execute_hf_plan(
&RealSsh,
&user,
&host,
&PullPlan::OllamaNative { tag },
has_token,
dry_run,
)
}
ModelRef::Hf { org, repo, quant } => {
let client = http_client_long();
let all = fetch_hf_siblings(&client, &hf_api_base(), &org, &repo).await?;
let matched: Vec<GgufFile> = all
.into_iter()
.filter(|f| dgx_pull::file_matches_quant(&f.path, &quant))
.collect();
let plan = dgx_pull::plan_hf(&org, &repo, &quant, &matched, name)
.map_err(|e| anyhow::anyhow!(e))?;
let model_bytes = dgx_pull::total_bytes(&matched);
let mem = if dry_run {
None
} else {
detect_node_mem(&user, &host, None)
};
report_fit(fit_check(model_bytes, mem), force)?;
execute_hf_plan(&RealSsh, &user, &host, &plan, has_token, dry_run)
}
}
}
fn hf_api_base() -> String {
std::env::var("NEWT_HF_API_BASE").unwrap_or_else(|_| "https://huggingface.co".to_string())
}
fn http_client_long() -> reqwest::Client {
reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(60))
.build()
.expect("build reqwest client")
}
fn report_fit(verdict: FitVerdict, force: bool) -> anyhow::Result<()> {
let refuse = verdict.should_refuse();
match &verdict {
FitVerdict::Fits {
model_bytes,
mem_bytes,
} => println!(
" Fit: model {:.1} GB fits in node memory {:.1} GB",
dgx_pull::bytes_to_gib(*model_bytes),
dgx_pull::bytes_to_gib(*mem_bytes)
),
FitVerdict::Undetectable { model_bytes } => eprintln!(
" WARNING: could not detect node memory; model is {:.1} GB. Proceeding best-effort.",
dgx_pull::bytes_to_gib(*model_bytes)
),
FitVerdict::Exceeds {
model_bytes,
mem_bytes,
} => eprintln!(
" WARNING: model {:.1} GB exceeds node memory {:.1} GB — \
will be unrunnable / heavy disk paging.",
dgx_pull::bytes_to_gib(*model_bytes),
dgx_pull::bytes_to_gib(*mem_bytes)
),
}
if refuse && !force {
anyhow::bail!("refusing to pull a model larger than node memory; pass --force to override");
}
if refuse {
eprintln!(" --force given: proceeding anyway.");
}
Ok(())
}
fn execute_hf_plan(
ssh: &dyn SshExec,
user: &str,
host: &str,
plan: &PullPlan,
has_token: bool,
dry_run: bool,
) -> anyhow::Result<()> {
match plan {
PullPlan::OllamaNative { tag } => {
let command = dgx_pull::ollama_native_remote_command(tag);
run_or_dryrun(
ssh,
user,
host,
None,
&command,
dry_run,
&format!("PullPlan: OllamaNative {{ tag: {tag:?} }}"),
)
}
PullPlan::SingleFileHf { org, repo, quant } => {
let command = dgx_pull::single_file_remote_command(org, repo, quant);
run_or_dryrun(
ssh,
user,
host,
None,
&command,
dry_run,
"PullPlan: SingleFileHf (ollama pull hf.co/...)",
)
}
PullPlan::ShardedHf {
org,
repo,
quant,
parts,
modelfile,
name,
} => {
let script =
dgx_pull::sharded_remote_script(org, repo, parts, modelfile, name, has_token);
let summary = format!(
"PullPlan: ShardedHf {{ quant: {quant:?}, parts: {}, name: {name:?} }}",
parts.len()
);
run_or_dryrun(ssh, user, host, None, &script, dry_run, &summary)
}
}
}
fn run_or_dryrun(
ssh: &dyn SshExec,
user: &str,
host: &str,
port: Option<u16>,
command: &str,
dry_run: bool,
summary: &str,
) -> anyhow::Result<()> {
if dry_run {
println!("{summary}");
let argv = dgx_pull::ssh_argv(user, host, port, command);
println!("Would run: {}", argv.join(" "));
println!("--- remote command ---");
println!("{command}");
return Ok(());
}
println!("{summary}");
eprintln!("→ ssh {user}@{host}: executing pull (output below)");
ssh.run(user, host, port, command)
}
async fn rm(config_path: Option<&Path>, model: &str) -> anyhow::Result<()> {
let dgx = dgx_config(config_path)?;
if dgx.active_endpoint.is_openai_compatible() {
anyhow::bail!("`newt dgx rm` targets Ollama endpoints; the active endpoint is vLLM");
}
let base = dgx.resolve_endpoint()?;
delete_ollama_model(&http_client(), &base, model).await?;
println!("Deleted {model}");
Ok(())
}
async fn delete_ollama_model(
client: &reqwest::Client,
base: &str,
model: &str,
) -> anyhow::Result<()> {
let url = format!("{}/api/delete", base.trim_end_matches('/'));
let resp = client
.delete(&url)
.json(&serde_json::json!({ "model": model }))
.send()
.await
.map_err(|e| anyhow::anyhow!("request failed: {e}"))?;
if !resp.status().is_success() {
anyhow::bail!("HTTP {}", resp.status());
}
Ok(())
}
async fn ps(config_path: Option<&Path>) -> anyhow::Result<()> {
let dgx = dgx_config(config_path)?;
if dgx.active_endpoint.is_openai_compatible() {
anyhow::bail!("`newt dgx ps` targets Ollama endpoints; the active endpoint is vLLM");
}
let base = dgx.resolve_endpoint()?;
let loaded = fetch_ollama_ps(&http_client(), &base).await?;
println!("Loaded models on {} ({base}):", dgx.active_endpoint);
if loaded.is_empty() {
println!(" (none)");
}
for (name, size) in &loaded {
match size {
Some(bytes) => println!(" {name} ({:.1} GB)", dgx_pull::bytes_to_gib(*bytes)),
None => println!(" {name}"),
}
}
Ok(())
}
async fn fetch_ollama_ps(
client: &reqwest::Client,
base: &str,
) -> anyhow::Result<Vec<(String, Option<u64>)>> {
Ok(
newt_core::backend_probe::fetch_ollama_ps(client, base, None)
.await?
.into_iter()
.map(|m| (m.name, m.size_bytes))
.collect(),
)
}
async fn fetch_hf_model_json(
client: &reqwest::Client,
hf_base: &str,
org: &str,
repo: &str,
) -> anyhow::Result<serde_json::Value> {
let url = format!(
"{}/api/models/{org}/{repo}?blobs=true",
hf_base.trim_end_matches('/')
);
let resp = client
.get(&url)
.send()
.await
.map_err(|e| anyhow::anyhow!("HF API request failed: {e}"))?;
if !resp.status().is_success() {
anyhow::bail!("HF API returned HTTP {}", resp.status());
}
Ok(resp.json().await?)
}
async fn fetch_vllm_weight_bytes(model: &str) -> Option<u64> {
let (org, repo) = dgx_vllm::hf_repo_parts(model)?;
let client = http_client_long();
let json = fetch_hf_model_json(&client, &hf_api_base(), &org, &repo)
.await
.ok()?;
let bytes = dgx_vllm::sum_weight_bytes(&json);
(bytes > 0).then_some(bytes)
}
fn ensure_executable_runtime(runtime: dgx_vllm::VllmRuntime) -> anyhow::Result<()> {
if matches!(runtime, dgx_vllm::VllmRuntime::Docker) {
anyhow::bail!(
"--docker is preview-only here: `dgx vllm config --docker` prints the docker \
argv, but native `vllm serve` is the only launcher `up` executes in this step. \
Omit --docker to launch (docker execution is a follow-up)."
);
}
Ok(())
}
fn build_plan_from_args(model: &str, a: &VllmPlanArgs) -> anyhow::Result<dgx_vllm::VllmPlan> {
let dtype = match a.dtype.as_deref() {
Some(s) => Some(dgx_vllm::Dtype::parse(s).ok_or_else(|| {
anyhow::anyhow!(
"unknown --dtype {s:?}; expected one of: auto, nvfp4, fp8, bf16, awq, gptq"
)
})?),
None => None,
};
let runtime = if a.docker {
dgx_vllm::VllmRuntime::Docker
} else {
dgx_vllm::VllmRuntime::Native
};
Ok(dgx_vllm::resolve_plan(dgx_vllm::PlanInputs {
model,
served_name: a.served_name.as_deref(),
dtype,
tensor_parallel: a.tensor_parallel,
max_model_len: a.max_model_len,
gpu_mem_util: a.gpu_mem_util,
port: a.port,
runtime,
extra: a.extra.clone(),
}))
}
fn execute_vllm_plan(
ssh: &dyn SshExec,
user: &str,
host: &str,
plan: &dgx_vllm::VllmPlan,
dry_run: bool,
) -> anyhow::Result<()> {
let script = dgx_vllm::vllm_remote_script(plan);
let summary = format!(
"VllmPlan: serve {:?} as {:?} ({:?}, tp={}, max_len={}, util={:.2}, port={})",
plan.model,
plan.served_name,
plan.dtype,
plan.tensor_parallel,
plan.max_model_len,
plan.gpu_mem_util(),
plan.port,
);
run_or_dryrun(ssh, user, host, None, &script, dry_run, &summary)
}
async fn poll_vllm_ready(endpoint: &str, policy: &RetryPolicy) -> anyhow::Result<()> {
let backend = LocalVllmBackend::new(endpoint, "");
with_backoff(policy, || async { backend.list_models().await.map(|_| ()) })
.await
.map(|_| ())
.map_err(|e| anyhow::anyhow!("vLLM did not become ready at {endpoint}: {e}"))
}
#[must_use]
fn apply_vllm_persist(
config: &mut Config,
node_name: Option<&str>,
endpoint_url: &str,
served_name: &str,
) -> bool {
let dgx = config.dgx.get_or_insert_with(Default::default);
dgx.active_endpoint = EndpointKind::Vllm;
dgx.active_model = Some(served_name.to_string());
let target = node_name
.map(str::to_string)
.or_else(|| dgx.active_node.clone());
if let Some(name) = target {
if let Some(node) = dgx.nodes.iter_mut().find(|n| n.name == name) {
node.vllm = Some(endpoint_url.to_string());
return true;
}
}
false
}
fn persist_vllm_endpoint(
config_path: Option<&Path>,
node_name: Option<&str>,
endpoint_url: &str,
served_name: &str,
) -> anyhow::Result<()> {
let mut config = load_config(config_path)?;
let node_recorded = apply_vllm_persist(&mut config, node_name, endpoint_url, served_name);
let save_path = config_path
.map(std::path::PathBuf::from)
.or_else(newt_core::Config::user_config_path)
.ok_or_else(|| anyhow::anyhow!("cannot determine config file path"))?;
config.save(&save_path)?;
println!("vLLM endpoint {endpoint_url} active; model {served_name}");
println!("Saved → {}", save_path.display());
if !node_recorded {
eprintln!(
" WARNING: active endpoint set to vLLM, but no matching node was found to \
record {endpoint_url} on — configure the node (`dgx setup` / `dgx node`) so \
the URL resolves without env fallback."
);
}
Ok(())
}
fn resolve_served_name(dgx: &DgxConfig, served_name: Option<&str>) -> anyhow::Result<String> {
match served_name {
Some(s) => Ok(s.to_string()),
None => Ok(dgx.resolve_active_model()?),
}
}
pub(crate) async fn vllm_up(
config_path: Option<&Path>,
model: &str,
node: Option<&str>,
plan_args: &VllmPlanArgs,
force: bool,
evict_ollama: bool,
dry_run: bool,
) -> anyhow::Result<()> {
let mut dgx = dgx_config(config_path)?;
if let Some(n) = node {
dgx.active_node = Some(n.to_string());
}
let user = dgx.ssh_user();
let host = dgx.ssh_host()?;
let plan = build_plan_from_args(model, plan_args)?;
ensure_executable_runtime(plan.runtime)?;
if !dry_run {
if evict_ollama {
evict_ollama_models(&dgx).await?;
}
let mem = detect_node_mem_available(&user, &host, None);
match fetch_vllm_weight_bytes(model).await {
Some(weight) => report_fit(
dgx_vllm::vllm_fit_check(weight, mem, plan_args.gpu_mem_util),
force,
)?,
None => eprintln!(
" WARNING: could not size weights for {model:?} (local path, HF lookup \
failed, or no sized .safetensors/.bin) — skipping the fit pre-flight; the \
model may exceed node memory."
),
}
}
execute_vllm_plan(&RealSsh, &user, &host, &plan, dry_run)?;
if !dry_run {
let endpoint = format!("http://{host}:{}", plan.port);
println!("Waiting for vLLM at {endpoint}/v1/models (cold load can take minutes) …");
poll_vllm_ready(&endpoint, &RetryPolicy::for_local_inference()).await?;
persist_vllm_endpoint(config_path, node, &endpoint, &plan.served_name)?;
}
Ok(())
}
async fn vllm_down(
config_path: Option<&Path>,
served_name: Option<&str>,
node: Option<&str>,
dry_run: bool,
) -> anyhow::Result<()> {
let mut dgx = dgx_config(config_path)?;
if let Some(n) = node {
dgx.active_node = Some(n.to_string());
}
let user = dgx.ssh_user();
let host = dgx.ssh_host()?;
let served = resolve_served_name(&dgx, served_name)?;
let command = dgx_vllm::vllm_stop_command(&served);
run_or_dryrun(
&RealSsh,
&user,
&host,
None,
&command,
dry_run,
&format!("vLLM down: {served:?}"),
)
}
async fn vllm_logs(
config_path: Option<&Path>,
served_name: Option<&str>,
node: Option<&str>,
lines: u32,
dry_run: bool,
) -> anyhow::Result<()> {
let mut dgx = dgx_config(config_path)?;
if let Some(n) = node {
dgx.active_node = Some(n.to_string());
}
let user = dgx.ssh_user();
let host = dgx.ssh_host()?;
let served = resolve_served_name(&dgx, served_name)?;
let command = dgx_vllm::vllm_logs_command(&served, lines);
run_or_dryrun(
&RealSsh,
&user,
&host,
None,
&command,
dry_run,
&format!("vLLM logs: {served:?} (tail -f)"),
)
}
fn vllm_config(model: &str, plan_args: &VllmPlanArgs) -> anyhow::Result<()> {
let plan = build_plan_from_args(model, plan_args)?;
let argv = if matches!(plan.runtime, dgx_vllm::VllmRuntime::Docker) {
dgx_vllm::vllm_docker_argv(&plan)
} else {
dgx_vllm::render_vllm_argv(&plan)
};
println!("Resolved vLLM launch plan for {model:?}:");
println!(" served-model-name: {}", plan.served_name);
println!(
" dtype={:?} tensor-parallel={} max-model-len={} gpu-mem-util={:.2} port={}",
plan.dtype,
plan.tensor_parallel,
plan.max_model_len,
plan.gpu_mem_util(),
plan.port,
);
println!(" argv: {}", argv.join(" "));
Ok(())
}
async fn vllm_ps(config_path: Option<&Path>) -> anyhow::Result<()> {
let dgx = dgx_config(config_path)?;
let base = dgx.resolve_endpoint_for(EndpointKind::Vllm)?;
let models = fetch_vllm_models(&base).await?;
println!("vLLM models on {base}:");
if models.is_empty() {
println!(" (none)");
}
for m in &models {
println!(" {m}");
}
Ok(())
}
async fn fetch_vllm_models(base: &str) -> anyhow::Result<Vec<String>> {
let backend = LocalVllmBackend::new(base, "");
Ok(backend
.list_models()
.await?
.into_iter()
.map(|m| m.id)
.collect())
}
struct Residency {
ollama: Vec<(String, Option<u64>)>,
vllm: Vec<String>,
mem_available: Option<u64>,
}
impl Residency {
fn is_contended(&self) -> bool {
!self.ollama.is_empty() && !self.vllm.is_empty()
}
}
fn ollama_evict_targets(res: &Residency) -> Vec<String> {
res.ollama.iter().map(|(name, _)| name.clone()).collect()
}
fn ollama_unload_body(model: &str) -> serde_json::Value {
serde_json::json!({ "model": model, "keep_alive": 0 })
}
fn render_residency(res: &Residency) -> String {
let mut s = String::new();
match res.mem_available {
Some(b) => s.push_str(&format!(
" MemAvailable: {:.1} GB\n",
dgx_pull::bytes_to_gib(b)
)),
None => s.push_str(" MemAvailable: (undetected)\n"),
}
s.push_str(" Ollama resident:\n");
if res.ollama.is_empty() {
s.push_str(" (none)\n");
}
for (name, size) in &res.ollama {
match size {
Some(b) => s.push_str(&format!(
" {name} ({:.1} GB)\n",
dgx_pull::bytes_to_gib(*b)
)),
None => s.push_str(&format!(" {name}\n")),
}
}
s.push_str(" vLLM served:\n");
if res.vllm.is_empty() {
s.push_str(" (none)\n");
}
for m in &res.vllm {
s.push_str(&format!(" {m}\n"));
}
if res.is_contended() {
s.push_str(
" ⚠ both engines are resident and share one unified pool — \
use `dgx vllm up --evict-ollama` to free it before a large vLLM serve.\n",
);
}
s
}
async fn gpu(config_path: Option<&Path>) -> anyhow::Result<()> {
let dgx = dgx_config(config_path)?;
let client = http_client();
let ollama = match dgx.resolve_endpoint_for(EndpointKind::Ollama) {
Ok(base) => fetch_ollama_ps(&client, &base).await.unwrap_or_default(),
Err(_) => Vec::new(),
};
let vllm = match dgx.resolve_endpoint_for(EndpointKind::Vllm) {
Ok(base) => fetch_vllm_models(&base).await.unwrap_or_default(),
Err(_) => Vec::new(),
};
let mem_available = match dgx.ssh_host() {
Ok(host) => detect_node_mem_available(&dgx.ssh_user(), &host, None),
Err(_) => None,
};
let res = Residency {
ollama,
vllm,
mem_available,
};
println!("GPU residency:");
print!("{}", render_residency(&res));
Ok(())
}
async fn unload_ollama_model(
client: &reqwest::Client,
base: &str,
model: &str,
) -> anyhow::Result<()> {
let url = format!("{}/api/generate", base.trim_end_matches('/'));
let resp = client
.post(&url)
.json(&ollama_unload_body(model))
.send()
.await
.map_err(|e| anyhow::anyhow!("request failed: {e}"))?;
if !resp.status().is_success() {
anyhow::bail!("HTTP {}", resp.status());
}
Ok(())
}
async fn evict_ollama_models(dgx: &DgxConfig) -> anyhow::Result<()> {
let base = match dgx.resolve_endpoint_for(EndpointKind::Ollama) {
Ok(b) => b,
Err(_) => {
eprintln!(" --evict-ollama: no Ollama endpoint configured; nothing to evict");
return Ok(());
}
};
let client = http_client();
let resident = fetch_ollama_ps(&client, &base).await.unwrap_or_default();
let targets = ollama_evict_targets(&Residency {
ollama: resident,
vllm: Vec::new(),
mem_available: None,
});
if targets.is_empty() {
println!(" --evict-ollama: no resident Ollama models to free");
return Ok(());
}
for m in &targets {
match unload_ollama_model(&client, &base, m).await {
Ok(()) => println!(" evicted Ollama model {m}"),
Err(e) => eprintln!(" WARNING: failed to evict Ollama model {m}: {e}"),
}
}
Ok(())
}
async fn evict_vllm_server(ssh: &dyn SshExec, dgx: &DgxConfig) -> anyhow::Result<()> {
let base = match dgx.resolve_endpoint_for(EndpointKind::Vllm) {
Ok(b) => b,
Err(_) => {
eprintln!(" --evict-vllm: no vLLM endpoint configured; nothing to evict");
return Ok(());
}
};
let served = fetch_vllm_models(&base).await.unwrap_or_default();
if served.is_empty() {
println!(" --evict-vllm: no vLLM server running");
return Ok(());
}
let user = dgx.ssh_user();
let host = match dgx.ssh_host() {
Ok(h) => h,
Err(e) => {
eprintln!(" --evict-vllm: cannot resolve SSH host ({e}); skipping eviction");
return Ok(());
}
};
for id in &served {
let cmd = dgx_vllm::vllm_stop_command(id);
match ssh.run(&user, &host, None, &cmd) {
Ok(()) => println!(" stopped vLLM server {id}"),
Err(e) => eprintln!(" WARNING: failed to stop vLLM server {id}: {e}"),
}
}
Ok(())
}
fn serve_port(tool: InferenceTool) -> u16 {
match tool {
InferenceTool::Ollama => 11434,
InferenceTool::Vllm | InferenceTool::LlamaCpp => 8000,
}
}
fn select_deploy_variant(
strongest: bool,
model: Option<&str>,
available_gib: f64,
) -> anyhow::Result<&'static ModelVariant> {
if strongest {
return dgx_registry::strongest_model(available_gib, 1).ok_or_else(|| {
anyhow::anyhow!(
"no registry model fits the available budget ({available_gib:.1} GiB, before \
the 15% headroom) — free memory (`newt dgx clear`) or see `newt dgx status`"
)
});
}
let slug = model.ok_or_else(|| {
anyhow::anyhow!("specify a model slug (e.g. ornith-35b-fp16) or pass --strongest")
})?;
dgx_registry::find_variant(slug).ok_or_else(|| {
anyhow::anyhow!(
"unknown model {slug:?}; expected a registry slug like ornith-35b-fp16 \
(the `~/ornith-{{size}}-{{format}}.sh` convention)"
)
})
}
#[derive(Debug, PartialEq, Eq)]
enum DeployRoute {
Script {
path: String,
slug: String,
port: u16,
},
VllmFallback {
model: String,
port: u16,
},
}
fn deploy_route(v: &ModelVariant, script_present: bool) -> DeployRoute {
let port = serve_port(v.tool);
if script_present {
DeployRoute::Script {
path: dgx_registry::script_name(v),
slug: dgx_registry::variant_slug(v),
port,
}
} else {
DeployRoute::VllmFallback {
model: v.name.to_string(),
port,
}
}
}
fn script_probe_command(script_path: &str) -> String {
format!("test -f {script_path} && echo FOUND || echo MISSING")
}
fn script_is_present(probe_stdout: &str) -> bool {
probe_stdout.lines().any(|l| l.trim() == "FOUND")
}
fn deploy_state_dir() -> &'static str {
"$HOME/.newt/dgx/deploy"
}
fn deploy_launch_command(script_path: &str, slug: &str) -> String {
let dir = deploy_state_dir();
format!(
"set -eu\n\
mkdir -p \"{dir}\"\n\
nohup {script_path} > \"{dir}/{slug}.log\" 2>&1 &\n\
echo $! > \"{dir}/{slug}.pid\"\n"
)
}
fn restart_ollama_command() -> &'static str {
"systemctl --user restart ollama 2>/dev/null || systemctl restart ollama"
}
fn restart_ollama_service(ssh: &dyn SshExec, user: &str, host: &str) {
match ssh.run(user, host, None, restart_ollama_command()) {
Ok(()) => println!(" restarted ollama service"),
Err(e) => eprintln!(" WARNING: failed to restart ollama: {e}"),
}
}
fn freed_gib(before: &dgx_status::MemBudget, after: &dgx_status::MemBudget) -> f64 {
let freed = after.available_bytes.saturating_sub(before.available_bytes);
dgx_pull::bytes_to_gib(freed)
}
async fn deploy(
config_path: Option<&Path>,
strongest: bool,
model: Option<&str>,
node: Option<&str>,
) -> anyhow::Result<()> {
let mut dgx = dgx_config(config_path)?;
if let Some(n) = node {
dgx.active_node = Some(n.to_string());
}
let user = dgx.ssh_user();
let host = dgx.ssh_host()?;
let variant = if strongest {
let budget = dgx_status::gather_budget(&RealSshCapture, &user, &host, None)?;
let available_gib = dgx_pull::bytes_to_gib(budget.available_bytes);
println!("Available budget: {available_gib:.1} GiB");
select_deploy_variant(true, None, available_gib)?
} else {
select_deploy_variant(false, model, 0.0)?
};
println!(
"Selected {} {} (~{:.0} GiB, {})",
variant.name,
variant.format,
variant.gib,
variant.tool.as_str()
);
println!("Clearing competing workloads…");
evict_vllm_server(&RealSsh, &dgx).await?;
evict_ollama_models(&dgx).await?;
use crate::dgx_status::SshCapture as _;
let script_path = dgx_registry::script_name(variant);
let probe = RealSshCapture.capture(&user, &host, None, &script_probe_command(&script_path))?;
let route = deploy_route(variant, script_is_present(&probe));
let port = match &route {
DeployRoute::Script { path, slug, port } => {
println!("Mode A: invoking convention script {path}");
RealSsh.run(&user, &host, None, &deploy_launch_command(path, slug))?;
*port
}
DeployRoute::VllmFallback { model, port } => {
println!(
"Mode B: no convention script {script_path} on node — \
generating a vLLM launch for {model}"
);
let plan = build_plan_from_args(model, &default_vllm_plan_args())?;
execute_vllm_plan(&RealSsh, &user, &host, &plan, false)?;
*port
}
};
let endpoint = format!("http://{host}:{port}");
println!("Waiting for {endpoint}/v1/models (cold load can take minutes)…");
poll_vllm_ready(&endpoint, &RetryPolicy::for_local_inference()).await?;
println!("Serving at {endpoint}");
Ok(())
}
async fn clear(
config_path: Option<&Path>,
node: Option<&str>,
restart_ollama: bool,
) -> anyhow::Result<()> {
let mut dgx = dgx_config(config_path)?;
if let Some(n) = node {
dgx.active_node = Some(n.to_string());
}
let user = dgx.ssh_user();
let host = dgx.ssh_host()?;
let before = dgx_status::gather_budget(&RealSshCapture, &user, &host, None).ok();
println!("Clearing inference workloads on {host}…");
evict_vllm_server(&RealSsh, &dgx).await?;
evict_ollama_models(&dgx).await?;
if restart_ollama {
restart_ollama_service(&RealSsh, &user, &host);
}
let after = dgx_status::gather_budget(&RealSshCapture, &user, &host, None).ok();
match (before, after) {
(Some(b), Some(a)) => {
print!("{}", dgx_status::render_budget_human(&host, &a));
println!(" Freed: {:.1} GiB", freed_gib(&b, &a));
}
(None, Some(a)) => print!("{}", dgx_status::render_budget_human(&host, &a)),
_ => println!(" (memory budget unavailable over SSH)"),
}
Ok(())
}
fn parse_switch_engine(engine: &str) -> anyhow::Result<EndpointKind> {
match engine.to_ascii_lowercase().as_str() {
"ollama" => Ok(EndpointKind::Ollama),
"vllm" => Ok(EndpointKind::Vllm),
other => anyhow::bail!("`dgx switch` targets `ollama` or `vllm`, not {other:?}"),
}
}
fn default_vllm_plan_args() -> VllmPlanArgs {
VllmPlanArgs {
served_name: None,
dtype: None,
tensor_parallel: 1,
max_model_len: None,
gpu_mem_util: 0.90,
port: 8000,
docker: false,
extra: Vec::new(),
}
}
fn ollama_has_model(installed: &[String], model: &str) -> bool {
installed.iter().any(|m| m == model)
}
fn apply_active(config: &mut Config, kind: EndpointKind, model: &str) {
let dgx = config.dgx.get_or_insert_with(Default::default);
dgx.active_endpoint = kind;
dgx.active_model = Some(model.to_string());
}
fn persist_active(
config_path: Option<&Path>,
kind: EndpointKind,
model: &str,
) -> anyhow::Result<()> {
let mut config = load_config(config_path)?;
apply_active(&mut config, kind, model);
let save_path = config_path
.map(std::path::PathBuf::from)
.or_else(newt_core::Config::user_config_path)
.ok_or_else(|| anyhow::anyhow!("cannot determine config file path"))?;
config.save(&save_path)?;
println!("Active endpoint {kind}; model {model}");
println!("Saved → {}", save_path.display());
Ok(())
}
async fn switch(
config_path: Option<&Path>,
engine: &str,
model: &str,
node: Option<&str>,
dry_run: bool,
) -> anyhow::Result<()> {
match parse_switch_engine(engine)? {
EndpointKind::Vllm => {
println!("Switching → vLLM serving {model:?} (evicting Ollama)…");
vllm_up(
config_path,
model,
node,
&default_vllm_plan_args(),
false,
true,
dry_run,
)
.await
}
EndpointKind::Ollama => switch_to_ollama(config_path, model, node, dry_run).await,
other => anyhow::bail!("cannot switch to endpoint kind {other}"),
}
}
async fn switch_to_ollama(
config_path: Option<&Path>,
model: &str,
node: Option<&str>,
dry_run: bool,
) -> anyhow::Result<()> {
let mut dgx = dgx_config(config_path)?;
if let Some(n) = node {
dgx.active_node = Some(n.to_string());
}
println!("Switching → Ollama serving {model:?} (stopping any vLLM server)…");
if dry_run {
println!(
" [dry-run] would: stop the vLLM server, verify {model:?} is pulled, \
set active=ollama:{model}, and warm it"
);
return Ok(());
}
evict_vllm_server(&RealSsh, &dgx).await?;
let base = dgx.resolve_endpoint_for(EndpointKind::Ollama)?;
let installed = fetch_ollama_models(&http_client(), &base)
.await
.unwrap_or_default();
if !ollama_has_model(&installed, model) {
anyhow::bail!(
"{model:?} is not pulled on the Ollama endpoint — run `newt dgx pull {model}` first"
);
}
persist_active(config_path, EndpointKind::Ollama, model)?;
let warm_client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(600))
.build()
.expect("build reqwest client");
match warm_model(&warm_client, &base, model, "30m").await {
Ok(Some(secs)) => println!(" warmed {model} in {secs:.1}s — now resident"),
Ok(None) => println!(" {model} ready (already resident)"),
Err(e) => eprintln!(" WARNING: warm failed: {e}"),
}
Ok(())
}
fn derive_live_state(
vllm: &[String],
ollama: &[(String, Option<u64>)],
) -> Option<(EndpointKind, String)> {
if let Some(m) = vllm.first() {
Some((EndpointKind::Vllm, m.clone()))
} else {
ollama
.first()
.map(|(m, _)| (EndpointKind::Ollama, m.clone()))
}
}
#[derive(Debug, PartialEq, Eq)]
enum ReconcileVerdict {
InSync {
kind: EndpointKind,
model: String,
},
Mismatch {
cfg_kind: EndpointKind,
cfg_model: Option<String>,
live_kind: EndpointKind,
live_model: String,
},
NoLiveState,
}
fn reconcile(
cfg_kind: EndpointKind,
cfg_model: Option<&str>,
vllm: &[String],
ollama: &[(String, Option<u64>)],
) -> ReconcileVerdict {
let configured_is_live = match (cfg_kind, cfg_model) {
(EndpointKind::Vllm, Some(m)) => vllm.iter().any(|x| x == m),
(EndpointKind::Ollama, Some(m)) => ollama.iter().any(|(x, _)| x == m),
_ => false,
};
if let (true, Some(m)) = (configured_is_live, cfg_model) {
return ReconcileVerdict::InSync {
kind: cfg_kind,
model: m.to_string(),
};
}
match derive_live_state(vllm, ollama) {
None => ReconcileVerdict::NoLiveState,
Some((live_kind, live_model)) => ReconcileVerdict::Mismatch {
cfg_kind,
cfg_model: cfg_model.map(str::to_string),
live_kind,
live_model,
},
}
}
#[derive(Debug, PartialEq, Eq)]
enum ReconcileAction {
Adopt,
Enforce,
Report,
}
fn reconcile_menu() -> newt_core::InteractionDefinition {
newt_core::interaction_form::menu(
"config and the node disagree",
"",
&[
("a", "adopt the node's state into config"),
("e", "enforce config onto the node"),
("c", "cancel, change nothing"),
],
)
}
fn reconcile_action(
adopt: bool,
enforce: bool,
is_tty: bool,
tty_answer: Option<&str>,
) -> ReconcileAction {
if adopt {
return ReconcileAction::Adopt;
}
if enforce {
return ReconcileAction::Enforce;
}
if !is_tty {
return ReconcileAction::Report;
}
match tty_answer {
Some("a") => ReconcileAction::Adopt,
Some("e") => ReconcileAction::Enforce,
_ => ReconcileAction::Report,
}
}
async fn adopt_cmd(
config_path: Option<&Path>,
adopt: bool,
enforce: bool,
node: Option<&str>,
) -> anyhow::Result<()> {
let mut dgx = dgx_config(config_path)?;
if let Some(n) = node {
dgx.active_node = Some(n.to_string());
}
let client = http_client();
let ollama = match dgx.resolve_endpoint_for(EndpointKind::Ollama) {
Ok(base) => fetch_ollama_ps(&client, &base).await.unwrap_or_default(),
Err(_) => Vec::new(),
};
let vllm = match dgx.resolve_endpoint_for(EndpointKind::Vllm) {
Ok(base) => fetch_vllm_models(&base).await.unwrap_or_default(),
Err(_) => Vec::new(),
};
match reconcile(
dgx.active_endpoint,
dgx.active_model.as_deref(),
&vllm,
&ollama,
) {
ReconcileVerdict::InSync { kind, model } => {
println!("dgx1 in sync: the configured {kind} model {model:?} is serving on the node.");
Ok(())
}
ReconcileVerdict::NoLiveState => {
println!(
"Nothing is running on the node (no vLLM server, no resident Ollama model). \
Config wants {} serving {:?}; start it with `dgx switch` / `vllm up` / `warm`.",
dgx.active_endpoint,
dgx.active_model.as_deref().unwrap_or("<none>")
);
Ok(())
}
ReconcileVerdict::Mismatch {
cfg_kind,
cfg_model,
live_kind,
live_model,
} => {
println!("dgx1 mismatch — config and the node disagree:");
println!(
" config: {cfg_kind} : {}",
cfg_model.as_deref().unwrap_or("<none>")
);
println!(" node: {live_kind} : {live_model}");
let is_tty = std::io::stdin().is_terminal();
let answer = if !adopt && !enforce && is_tty {
let window = newt_core::tty::Terminal::suspend_for_prompt(
newt_core::tty::TerminalTaker::PlainCliConfirm,
);
newt_core::interaction_terminal::resolve_on_terminal(&window, &reconcile_menu())
.map(|picked| picked.as_str().to_string())
} else {
None
};
match reconcile_action(adopt, enforce, is_tty, answer.as_deref()) {
ReconcileAction::Adopt => {
println!("Adopting: pointing newt at {live_kind} : {live_model}.");
persist_active(config_path, live_kind, &live_model)
}
ReconcileAction::Enforce => {
if !matches!(cfg_kind, EndpointKind::Ollama | EndpointKind::Vllm) {
anyhow::bail!(
"cannot enforce a non-local endpoint ({cfg_kind}) — `dgx switch` only \
manages ollama/vllm on the node; adopt the node's state instead"
);
}
let model = cfg_model.ok_or_else(|| {
anyhow::anyhow!(
"cannot enforce: no active_model configured — set one (`dgx use`) \
or adopt the node's state instead"
)
})?;
println!("Enforcing: switching the node to {cfg_kind} : {model}.");
switch(config_path, cfg_kind.as_str(), &model, node, false).await
}
ReconcileAction::Report => {
println!(
"No change. Re-run with --adopt (point newt at the node) or \
--enforce (switch the node to the config)."
);
Ok(())
}
}
}
}
}
fn drift_notice_line(verdict: &ReconcileVerdict) -> Option<String> {
match verdict {
ReconcileVerdict::Mismatch {
cfg_kind,
cfg_model,
live_kind,
live_model,
} => Some(format!(
"dgx note: the node is running {live_kind}:{live_model}, but config expects \
{cfg_kind}:{} — run `newt dgx adopt` to reconcile.",
cfg_model.as_deref().unwrap_or("<none>")
)),
_ => None,
}
}
pub(crate) async fn startup_drift_notice(config: Option<&Config>) {
let Some(dgx) = config.and_then(|c| c.dgx.clone()) else {
return;
};
if dgx.nodes.is_empty() && dgx.active_model.is_none() {
return;
}
let client = http_client();
let probe = async {
let ollama = match dgx.resolve_endpoint_for(EndpointKind::Ollama) {
Ok(base) => fetch_ollama_ps(&client, &base).await.unwrap_or_default(),
Err(_) => Vec::new(),
};
let vllm = match dgx.resolve_endpoint_for(EndpointKind::Vllm) {
Ok(base) => fetch_vllm_models(&base).await.unwrap_or_default(),
Err(_) => Vec::new(),
};
(ollama, vllm)
};
let (ollama, vllm) =
match tokio::time::timeout(std::time::Duration::from_millis(2500), probe).await {
Ok(r) => r,
Err(_) => return,
};
let verdict = reconcile(
dgx.active_endpoint,
dgx.active_model.as_deref(),
&vllm,
&ollama,
);
if let Some(line) = drift_notice_line(&verdict) {
eprintln!("{line}");
}
}
#[cfg(test)]
#[path = "dgx/tests.rs"]
mod tests;