use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use anyhow::{Context, bail};
#[derive(serde::Deserialize)]
struct Sibling {
rfilename: String,
}
#[derive(serde::Deserialize)]
struct ModelInfo {
siblings: Vec<Sibling>,
}
fn cache_root() -> PathBuf {
if let Ok(dir) = std::env::var("TRITIUM_MODEL_CACHE") {
return PathBuf::from(dir);
}
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
Path::new(&home).join(".cache/tritium-models")
}
pub(crate) fn run(repo: &str, file: Option<&str>, revision: &str) -> anyhow::Result<()> {
if repo.split('/').count() != 2 || repo.contains("..") {
bail!("repo must look like `owner/name`, got {repo:?}");
}
let filename = match file {
Some(f) => f.to_owned(),
None => {
let url = format!("https://huggingface.co/api/models/{repo}");
let mut req = ureq::get(&url);
if let Ok(token) = std::env::var("HF_TOKEN")
&& !token.is_empty()
{
req = req.header("Authorization", format!("Bearer {token}"));
}
let info: ModelInfo = req
.call()
.with_context(|| format!("querying {url}"))?
.body_mut()
.read_json()
.context("parsing hub model info")?;
let mut ggufs: Vec<String> = info
.siblings
.into_iter()
.map(|s| s.rfilename)
.filter(|f| f.ends_with(".gguf"))
.collect();
ggufs.sort();
match ggufs.len() {
0 => bail!(
"{repo} has no .gguf files — tritium serves GGUF; pick a \
-gguf repo (or convert and use --file)"
),
1 => ggufs.remove(0),
_ => {
eprintln!("{repo} has {} .gguf files:", ggufs.len());
for f in &ggufs {
eprintln!(" {f}");
}
bail!("pick one with --file <name>");
}
}
}
};
let dir = cache_root().join(repo.replace('/', "--"));
std::fs::create_dir_all(&dir).with_context(|| format!("creating {}", dir.display()))?;
let dest = dir.join(
Path::new(&filename)
.file_name()
.context("bad --file name")?,
);
if dest.exists() {
eprintln!("already present: {}", dest.display());
print_next_steps(&dest);
return Ok(());
}
let url = format!("https://huggingface.co/{repo}/resolve/{revision}/{filename}");
let file_name = dest.file_name().and_then(|n| n.to_str()).unwrap_or("model");
let part = dir.join(format!("{file_name}.part"));
let meta_path = dir.join(format!("{file_name}.part.meta"));
let meta: Option<(String, u64)> = std::fs::read_to_string(&meta_path).ok().and_then(|m| {
let (etag, total) = m.trim().rsplit_once(' ')?;
Some((etag.to_owned(), total.parse().ok()?))
});
let have = match (&meta, part.metadata().map(|m| m.len())) {
(Some(_), Ok(len)) => len,
_ => 0, };
if let Some((_, total)) = &meta
&& have == *total
&& have > 0
{
std::fs::rename(&part, &dest)
.with_context(|| format!("renaming into {}", dest.display()))?;
let _ = std::fs::remove_file(&meta_path);
eprintln!("pulled {} (recovered complete download)", dest.display());
print_next_steps(&dest);
return Ok(());
}
eprintln!("pulling {url}");
let mut req = ureq::get(&url)
.config()
.http_status_as_error(false)
.build();
if let Ok(token) = std::env::var("HF_TOKEN")
&& !token.is_empty()
{
req = req.header("Authorization", format!("Bearer {token}"));
}
if have > 0
&& let Some((etag, _)) = &meta
{
eprintln!("resuming at {} MiB", have / (1024 * 1024));
req = req
.header("Range", format!("bytes={have}-"))
.header("If-Range", etag);
}
let mut resp = req.call().with_context(|| format!("GET {url}"))?;
let status = resp.status();
let etag = resp
.headers()
.get("ETag")
.and_then(|v| v.to_str().ok())
.unwrap_or("-")
.to_owned();
let (mut out, mut done) = if status == 206 && have > 0 {
let f = std::fs::OpenOptions::new()
.append(true)
.open(&part)
.with_context(|| format!("opening {}", part.display()))?;
(f, have)
} else if status == 200 {
let f =
std::fs::File::create(&part).with_context(|| format!("creating {}", part.display()))?;
(f, 0)
} else if status == 416 {
let _ = std::fs::remove_file(&part);
let _ = std::fs::remove_file(&meta_path);
bail!("range not satisfiable — cleared stale partial state, re-run to restart");
} else {
bail!("GET {url} -> HTTP {status} (gated repo? set HF_TOKEN)");
};
let total = resp
.headers()
.get("Content-Length")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u64>().ok())
.map(|len| len + done);
if let Some(total) = total {
std::fs::write(&meta_path, format!("{etag} {total}"))
.with_context(|| format!("writing {}", meta_path.display()))?;
} else {
let _ = std::fs::remove_file(&meta_path);
}
let mut reader = resp.body_mut().with_config().limit(u64::MAX).reader();
let mut buf = vec![0u8; 1 << 20];
let mut last_pct = u64::MAX;
loop {
let n = reader.read(&mut buf).context("network read")?;
if n == 0 {
break;
}
out.write_all(&buf[..n]).context("cache write")?;
done += n as u64;
if let Some(total) = total {
let pct = done * 100 / total.max(1);
if pct != last_pct {
eprint!("\r{pct:>3}% {} / {} MiB", done >> 20, total >> 20);
last_pct = pct;
}
} else if done.trailing_zeros() >= 24 {
eprint!("\r{} MiB", done >> 20);
}
}
eprintln!();
out.flush().ok();
drop(out);
if let Some(total) = total
&& done != total
{
bail!(
"short read: got {done} of {total} bytes — re-run to resume \
({} kept)",
part.display()
);
}
std::fs::rename(&part, &dest).with_context(|| format!("renaming into {}", dest.display()))?;
let _ = std::fs::remove_file(&meta_path);
eprintln!("pulled {}", dest.display());
print_next_steps(&dest);
Ok(())
}
fn print_next_steps(dest: &Path) {
eprintln!("\nnext:");
eprintln!(" tritium inspect {}", dest.display());
eprintln!(
" tritium-serve --model {} --backend cuda # text chat on /v1/chat/completions",
dest.display()
);
}