use std::fmt::Write as _;
use std::path::Path;
use anyhow::Context as _;
use tritium_format::{GgufFile, GgufValue, read_gguf};
fn ggml_type_name(ggml_type: u32) -> String {
match ggml_type {
0 => "F32".to_owned(),
1 => "F16".to_owned(),
tritium_format::GGML_TYPE_TQ1_0 => "TQ1_0".to_owned(),
tritium_format::GGML_TYPE_TQ2_0 => "TQ2_0".to_owned(),
other => format!("type {other}"),
}
}
fn format_dims(dims: &[u64]) -> String {
let mut s = String::from("[");
for (i, d) in dims.iter().enumerate() {
if i > 0 {
s.push_str(", ");
}
let _ = write!(s, "{d}");
}
s.push(']');
s
}
#[must_use]
pub(crate) fn render_inspect(file: &GgufFile) -> String {
let mut out = String::new();
let _ = writeln!(out, "GGUF version: {}", file.version);
let _ = writeln!(out, "Metadata count: {}", file.metadata.len());
match file
.get_metadata("general.architecture")
.and_then(GgufValue::as_str)
{
Some(arch) => {
let _ = writeln!(out, "Architecture: {arch}");
}
None => {
let _ = writeln!(out, "Architecture: (unset)");
}
}
let _ = writeln!(out, "Alignment: {}", file.alignment());
let _ = writeln!(out, "Tensor count: {}", file.tensors.len());
let name_w = file
.tensors
.iter()
.map(|t| t.name.len())
.max()
.unwrap_or(0)
.max("NAME".len());
let dims_w = file
.tensors
.iter()
.map(|t| format_dims(&t.dims).len())
.max()
.unwrap_or(0)
.max("DIMS".len());
let type_w = file
.tensors
.iter()
.map(|t| ggml_type_name(t.ggml_type).len())
.max()
.unwrap_or(0)
.max("TYPE".len());
let _ = writeln!(out, "\nTensors:");
let _ = writeln!(
out,
" {:<name_w$} {:<dims_w$} {:<type_w$} {:>12} {:>12}",
"NAME", "DIMS", "TYPE", "OFFSET", "N_BYTES",
);
for t in &file.tensors {
let _ = writeln!(
out,
" {:<name_w$} {:<dims_w$} {:<type_w$} {:>12} {:>12}",
t.name,
format_dims(&t.dims),
ggml_type_name(t.ggml_type),
t.offset,
t.n_bytes,
);
}
out
}
pub(crate) fn run(path: &Path) -> anyhow::Result<()> {
let bytes = std::fs::read(path)
.with_context(|| format!("failed to read GGUF file `{}`", path.display()))?;
let file = read_gguf(&bytes)
.with_context(|| format!("failed to parse `{}` as GGUF", path.display()))?;
print!("{}", render_inspect(&file));
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeMap;
use tritium_format::TensorInfo;
fn sample_file() -> GgufFile {
let mut metadata = BTreeMap::new();
metadata.insert(
"general.architecture".to_owned(),
GgufValue::String("llama".to_owned()),
);
metadata.insert("general.alignment".to_owned(), GgufValue::U32(32));
GgufFile::new(
3,
metadata,
vec![
TensorInfo::new(
"blk.0.weight".to_owned(),
vec![256, 4],
tritium_format::GGML_TYPE_TQ2_0,
0,
264,
),
TensorInfo::new(
"blk.0.qh".to_owned(),
vec![256],
tritium_format::GGML_TYPE_TQ1_0,
264,
54,
),
TensorInfo::new("output_norm.bias".to_owned(), vec![4], 0, 320, 16),
TensorInfo::new("token_embd.weight".to_owned(), vec![8], 1, 336, 16),
TensorInfo::new("mystery".to_owned(), vec![1], 99, 352, 0),
],
0,
)
}
#[test]
fn type_names_map_correctly() {
assert_eq!(ggml_type_name(0), "F32");
assert_eq!(ggml_type_name(1), "F16");
assert_eq!(ggml_type_name(34), "TQ1_0");
assert_eq!(ggml_type_name(35), "TQ2_0");
assert_eq!(ggml_type_name(99), "type 99");
}
#[test]
fn dims_format_is_bracketed_csv() {
assert_eq!(format_dims(&[]), "[]");
assert_eq!(format_dims(&[256]), "[256]");
assert_eq!(format_dims(&[256, 4]), "[256, 4]");
}
#[test]
fn render_contains_header_fields() {
let report = render_inspect(&sample_file());
assert!(report.contains("GGUF version: 3"), "{report}");
assert!(report.contains("Architecture: llama"), "{report}");
assert!(report.contains("Alignment: 32"), "{report}");
assert!(report.contains("Metadata count: 2"), "{report}");
assert!(report.contains("Tensor count: 5"), "{report}");
}
#[test]
fn render_contains_tensor_names_and_types() {
let report = render_inspect(&sample_file());
assert!(report.contains("blk.0.weight"), "{report}");
assert!(report.contains("blk.0.qh"), "{report}");
assert!(report.contains("output_norm.bias"), "{report}");
assert!(report.contains("token_embd.weight"), "{report}");
assert!(report.contains("mystery"), "{report}");
assert!(report.contains("TQ2_0"), "{report}");
assert!(report.contains("TQ1_0"), "{report}");
assert!(report.contains("F32"), "{report}");
assert!(report.contains("F16"), "{report}");
assert!(report.contains("type 99"), "{report}");
assert!(report.contains("[256, 4]"), "{report}");
}
#[test]
fn render_handles_missing_architecture() {
let mut file = sample_file();
file.metadata.remove("general.architecture");
let report = render_inspect(&file);
assert!(report.contains("Architecture: (unset)"), "{report}");
}
#[test]
fn run_on_missing_file_errors_cleanly() {
let err = run(Path::new("/nonexistent/definitely/not/here.gguf"))
.expect_err("missing file must error");
let msg = format!("{err:#}");
assert!(msg.contains("failed to read GGUF file"), "{msg}");
}
#[test]
fn run_on_corrupt_file_errors_cleanly() {
let mut tmp = std::env::temp_dir();
tmp.push(format!("tritium-cli-corrupt-{}.gguf", std::process::id()));
std::fs::write(&tmp, b"not a gguf file at all").expect("write temp");
let err = run(&tmp).expect_err("corrupt file must error");
let _ = std::fs::remove_file(&tmp);
let msg = format!("{err:#}");
assert!(msg.contains("failed to parse"), "{msg}");
}
}