use std::fs;
use std::path::Path;
use std::process;
use harn_vm::llm::capabilities::ProviderCapabilityMatrixRow;
use crate::cli::CheckOutputFormat;
const FEATURES: &[&str] = &[
"thinking",
"vision",
"audio",
"pdf",
"streaming",
"json_schema",
"tools",
"cache",
];
pub(crate) fn run(format: CheckOutputFormat, filter: Option<&str>) {
let rows = filtered_rows(filter);
match format {
CheckOutputFormat::Text => print_text(&rows),
CheckOutputFormat::Json => print_json(&rows),
}
}
pub(crate) fn run_docs(output_path: &str, check_only: bool) {
let generated = generate_markdown();
let path = Path::new(output_path);
if check_only {
let existing = match fs::read_to_string(path) {
Ok(s) => s,
Err(e) => {
eprintln!("error: cannot read {}: {e}", path.display());
eprintln!("hint: run `make gen-provider-matrix` to regenerate.");
process::exit(1);
}
};
if existing != generated {
eprintln!(
"error: {} is stale relative to the provider capability matrix.",
path.display()
);
eprintln!("hint: run `make gen-provider-matrix` to regenerate.");
process::exit(1);
}
return;
}
if let Some(parent) = path.parent() {
if let Err(e) = fs::create_dir_all(parent) {
eprintln!("error: cannot create {}: {e}", parent.display());
process::exit(1);
}
}
if let Err(e) = fs::write(path, &generated) {
eprintln!("error: cannot write {}: {e}", path.display());
process::exit(1);
}
println!("wrote {}", path.display());
}
pub(crate) fn generate_markdown() -> String {
let rows = harn_vm::llm::capabilities::matrix_rows();
let mut out = String::new();
out.push_str("# Provider Capability Matrix\n\n");
out.push_str("<!-- GENERATED by `harn dump-provider-matrix` -- do not edit by hand. -->\n");
out.push_str("<!-- Source of truth: crates/harn-vm/src/llm/capabilities.toml. -->\n\n");
out.push_str("<!-- markdownlint-disable MD013 -->\n\n");
out.push_str(
"This table is generated from Harn's live provider capability rules. `Model pattern` is the `model_match` rule used by the runtime; first match wins within each provider.\n\n",
);
out.push_str("Regenerate with `make gen-provider-matrix` and verify with `make check-provider-matrix`.\n\n");
out.push_str(
"| Provider | Model pattern | Thinking | Vision | Audio | PDF | Streaming | JSON schema | Tools | Cache |\n",
);
out.push_str("|---|---|---|---:|---:|---:|---:|---|---:|---:|\n");
for row in rows {
out.push_str(&format!(
"| `{}` | `{}` | {} | {} | {} | {} | {} | {} | {} | {} |\n",
row.provider,
row.model,
markdown_cell(&thinking_cell(&row)),
yes_no(row.vision),
yes_no(row.audio),
yes_no(row.pdf),
yes_no(row.streaming),
markdown_cell(&json_schema_cell(&row)),
yes_no(row.tools),
yes_no(row.cache),
));
}
out
}
fn filtered_rows(filter: Option<&str>) -> Vec<ProviderCapabilityMatrixRow> {
let rows = harn_vm::llm::capabilities::matrix_rows();
let Some(feature) = filter else {
return rows;
};
let normalized = normalize_feature(feature);
if !FEATURES.contains(&normalized.as_str()) {
eprintln!(
"error: unknown provider matrix feature `{feature}`. Expected one of: {}",
FEATURES.join(", ")
);
process::exit(2);
}
rows.into_iter()
.filter(|row| row_supports_feature(row, &normalized))
.collect()
}
fn normalize_feature(feature: &str) -> String {
feature.trim().to_lowercase().replace('-', "_")
}
fn row_supports_feature(row: &ProviderCapabilityMatrixRow, feature: &str) -> bool {
match feature {
"thinking" => !row.thinking.is_empty(),
"vision" => row.vision,
"audio" => row.audio,
"pdf" => row.pdf,
"streaming" => row.streaming,
"json_schema" => row.json_schema.is_some(),
"tools" => row.tools,
"cache" => row.cache,
_ => false,
}
}
fn print_text(rows: &[ProviderCapabilityMatrixRow]) {
let table_rows: Vec<[String; 9]> = rows
.iter()
.map(|row| {
[
provider_model_cell(row),
thinking_cell(row),
yes_no(row.vision).to_string(),
yes_no(row.audio).to_string(),
yes_no(row.pdf).to_string(),
yes_no(row.streaming).to_string(),
json_schema_cell(row),
yes_no(row.tools).to_string(),
yes_no(row.cache).to_string(),
]
})
.collect();
let headers = [
"provider/model".to_string(),
"thinking".to_string(),
"vision".to_string(),
"audio".to_string(),
"pdf".to_string(),
"streaming".to_string(),
"json_schema".to_string(),
"tools".to_string(),
"cache".to_string(),
];
let mut widths: Vec<usize> = headers.iter().map(String::len).collect();
for row in &table_rows {
for (index, value) in row.iter().enumerate() {
widths[index] = widths[index].max(value.len());
}
}
print_row(&headers, &widths);
for row in table_rows {
print_row(&row, &widths);
}
}
fn print_row<const N: usize>(cells: &[String; N], widths: &[usize]) {
for (index, cell) in cells.iter().enumerate() {
if index > 0 {
print!(" ");
}
print!("{cell:<width$}", width = widths[index]);
}
println!();
}
fn print_json(rows: &[ProviderCapabilityMatrixRow]) {
println!(
"{}",
serde_json::to_string_pretty(rows).unwrap_or_else(|error| {
eprintln!("error: failed to serialize provider matrix: {error}");
process::exit(1);
})
);
}
fn thinking_cell(row: &ProviderCapabilityMatrixRow) -> String {
if row.thinking.is_empty() {
"no".to_string()
} else {
row.thinking.join(",")
}
}
fn provider_model_cell(row: &ProviderCapabilityMatrixRow) -> String {
if row
.model
.strip_prefix(row.provider.as_str())
.is_some_and(|rest| rest.starts_with('/'))
{
row.model.clone()
} else {
format!("{}/{}", row.provider, row.model)
}
}
fn json_schema_cell(row: &ProviderCapabilityMatrixRow) -> String {
row.json_schema.clone().unwrap_or_else(|| "no".to_string())
}
fn yes_no(value: bool) -> &'static str {
if value {
"yes"
} else {
"no"
}
}
fn markdown_cell(value: &str) -> String {
if value == "no" {
value.to_string()
} else {
format!("`{value}`")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn json_schema_filter_only_keeps_supported_rows() {
let rows = filtered_rows(Some("json-schema"));
assert!(!rows.is_empty());
assert!(rows.iter().all(|row| row.json_schema.is_some()));
}
#[test]
fn markdown_is_generated_from_matrix_rows() {
let markdown = generate_markdown();
assert!(markdown.contains("Provider Capability Matrix"));
assert!(markdown.contains("Source of truth"));
assert!(markdown.contains(
"| Provider | Model pattern | Thinking | Vision | Audio | PDF | Streaming | JSON schema | Tools | Cache |"
));
}
}