use std::fs;
use std::path::{Path, PathBuf};
use crate::cli::ProvidersGenerateArgs;
use crate::commands::providers::artifacts::{
artifact_drift_from_generated, generated_artifacts_from_artifact, validate_artifact_contract,
};
const PROVIDER_CONFIG_GENERATED_HEADER: &str = "\
# @generated by `harn provider catalog generate`; do not edit directly.
# Edit crates/harn-vm/src/llm/catalog_sources/**/*.toml instead.
";
const PROVIDER_CAPABILITIES_GENERATED_HEADER: &str = "\
# @generated by `harn provider catalog generate`; do not edit directly.
# Edit crates/harn-vm/src/llm/capability_sources/**/*.toml instead.
";
pub(crate) fn run_generate(args: &ProvidersGenerateArgs) -> Result<(), String> {
let provider_config = generated_provider_config(&args.source_dir)?;
let capabilities = generated_provider_capabilities(&args.capability_source_dir)?;
let artifact = harn_vm::provider_catalog::artifact_from_config_and_capabilities(
&provider_config.config,
&capabilities.capabilities,
);
let validation = validate_artifact_contract(&artifact)?;
if !validation.errors.is_empty() || !validation.schema_errors.is_empty() {
for warning in validation.warnings {
eprintln!("warning: {warning}");
}
for error in validation.errors {
eprintln!("error: {error}");
}
for error in validation.schema_errors {
eprintln!("error: {error}");
}
return Err("generated provider catalog contract is invalid".to_string());
}
for warning in validation.warnings {
eprintln!("warning: {warning}");
}
let artifacts = generated_artifacts_from_artifact(&artifact)?;
if args.check {
let mut drift = Vec::new();
if file_drifted(&args.providers_output, &provider_config.body) {
drift.push(args.providers_output.display().to_string());
}
if file_drifted(&args.capabilities_output, &capabilities.body) {
drift.push(args.capabilities_output.display().to_string());
}
drift.extend(artifact_drift_from_generated(
&args.artifact_dir,
&artifacts,
)?);
if drift.is_empty() {
println!("provider catalog generated artifacts are up to date");
return Ok(());
}
for path in drift {
eprintln!("error: generated provider catalog artifact is stale or missing: {path}");
}
return Err("provider catalog generated artifacts drifted".to_string());
}
write_generated_file(&args.providers_output, &provider_config.body)?;
println!(
"wrote {} from {} TOML fragments",
args.providers_output.display(),
provider_config.fragment_count
);
write_generated_file(&args.capabilities_output, &capabilities.body)?;
println!(
"wrote {} from {} TOML fragments",
args.capabilities_output.display(),
capabilities.fragment_count
);
fs::create_dir_all(&args.artifact_dir).map_err(|error| {
format!(
"failed to create provider catalog artifact directory {}: {error}",
args.artifact_dir.display()
)
})?;
for artifact in artifacts {
let path = args.artifact_dir.join(artifact.relative_path);
write_generated_file(&path, &artifact.body)?;
println!("wrote {}", path.display());
}
Ok(())
}
struct GeneratedProviderConfig {
body: String,
fragment_count: usize,
config: harn_vm::llm_config::ProvidersConfig,
}
struct GeneratedProviderCapabilities {
body: String,
fragment_count: usize,
capabilities: harn_vm::llm::capabilities::CapabilitiesFile,
}
fn generated_provider_config(source_dir: &Path) -> Result<GeneratedProviderConfig, String> {
let mut fragments = Vec::new();
collect_toml_fragments(source_dir, &mut fragments)?;
fragments.sort();
if fragments.is_empty() {
return Err(format!(
"provider catalog source directory has no TOML fragments: {}",
source_dir.display()
));
}
let mut body = String::from(PROVIDER_CONFIG_GENERATED_HEADER);
for path in &fragments {
let fragment = fs::read_to_string(path)
.map_err(|error| format!("failed to read {}: {error}", path.display()))?;
let label = fragment_label(source_dir, path);
if is_model_fragment(&label) {
if let Some(error) = leading_bare_key_error(&label, &fragment) {
return Err(error);
}
}
body.push_str("\n# --- source: ");
body.push_str(&label);
body.push_str(" ---\n");
body.push_str(fragment.trim_end());
body.push('\n');
}
let parsed = harn_vm::llm_config::parse_config_toml_with_diagnostics(&body)
.map_err(|error| format!("generated provider config does not parse: {error}"))?;
if !parsed.diagnostics.is_empty() {
let rendered = parsed
.diagnostics
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n");
return Err(format!(
"generated provider config contains unknown fields:\n{rendered}"
));
}
let default_issues = harn_vm::llm_config::model_default_issues(&parsed.config);
if !default_issues.is_empty() {
return Err(format!(
"generated provider config has invalid model defaults:\n{}",
default_issues.join("\n")
));
}
Ok(GeneratedProviderConfig {
body,
fragment_count: fragments.len(),
config: parsed.config,
})
}
fn generated_provider_capabilities(
source_dir: &Path,
) -> Result<GeneratedProviderCapabilities, String> {
let mut fragments = Vec::new();
collect_toml_fragments(source_dir, &mut fragments)?;
fragments.sort();
if fragments.is_empty() {
return Err(format!(
"provider capability source directory has no TOML fragments: {}",
source_dir.display()
));
}
let mut body = String::from(PROVIDER_CAPABILITIES_GENERATED_HEADER);
for path in &fragments {
let fragment = fs::read_to_string(path)
.map_err(|error| format!("failed to read {}: {error}", path.display()))?;
body.push_str("\n# --- source: ");
body.push_str(&fragment_label(source_dir, path));
body.push_str(" ---\n");
body.push_str(fragment.trim_end());
body.push('\n');
}
let parsed = toml::from_str::<harn_vm::llm::capabilities::CapabilitiesFile>(&body)
.map_err(|error| format!("generated provider capabilities do not parse: {error}"))?;
let audit = harn_vm::llm::capability_audit::audit_capabilities(&parsed);
if !audit.is_clean() {
return Err(format!(
"capability matrix footgun check failed:\n{}",
audit.render()
));
}
Ok(GeneratedProviderCapabilities {
body,
fragment_count: fragments.len(),
capabilities: parsed,
})
}
fn file_drifted(path: &Path, expected: &str) -> bool {
!matches!(fs::read_to_string(path), Ok(existing) if existing == expected)
}
fn write_generated_file(path: &Path, body: &str) -> Result<(), String> {
if let Some(parent) = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
{
fs::create_dir_all(parent)
.map_err(|error| format!("failed to create directory {}: {error}", parent.display()))?;
}
fs::write(path, body).map_err(|error| format!("failed to write {}: {error}", path.display()))
}
fn collect_toml_fragments(dir: &Path, fragments: &mut Vec<PathBuf>) -> Result<(), String> {
let mut entries = Vec::new();
let read_dir = fs::read_dir(dir).map_err(|error| {
format!(
"failed to read provider catalog source dir {}: {error}",
dir.display()
)
})?;
for entry in read_dir {
let entry = entry.map_err(|error| {
format!(
"failed to read provider catalog source dir entry in {}: {error}",
dir.display()
)
})?;
entries.push(entry.path());
}
entries.sort();
for path in entries {
if path.is_dir() {
collect_toml_fragments(&path, fragments)?;
} else if path.extension().and_then(|extension| extension.to_str()) == Some("toml") {
fragments.push(path);
}
}
Ok(())
}
fn fragment_label(source_dir: &Path, path: &Path) -> String {
path.strip_prefix(source_dir)
.unwrap_or(path)
.to_string_lossy()
.replace('\\', "/")
}
fn is_model_fragment(label: &str) -> bool {
label.starts_with("60-models/") || label.contains("/60-models/")
}
fn leading_bare_key_error(label: &str, fragment: &str) -> Option<String> {
for (line_number, raw) in fragment.lines().enumerate() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if line.starts_with('[') {
return None;
}
if line.contains('=') {
let key = line.split('=').next().unwrap_or(line).trim();
return Some(format!(
"provider catalog fragment {label} has a leading bare key `{key}` on line {} \
before its first [table] header. Move it inside the model's `[models.X]` table: \
a bare key before the first table binds to the PREVIOUS fragment's last model \
after concatenation, silently mislabeling that model.",
line_number + 1
));
}
}
None
}
#[cfg(test)]
mod tests {
use std::fs;
use crate::cli::ProvidersGenerateArgs;
use super::{generated_provider_config, leading_bare_key_error, run_generate};
#[test]
fn rejects_leading_bare_key_before_first_table() {
let fragment = "\
# a comment is fine
tier = \"mid\"
open_weight = true
[models.\"x\"]
name = \"X\"
";
let error =
leading_bare_key_error("60-models/test.toml", fragment).expect("leading key rejected");
assert!(error.contains("leading bare key `tier`"), "got: {error}");
assert!(error.contains("line 2"), "should report the line: {error}");
}
#[test]
fn accepts_fragment_with_only_comments_before_first_table() {
let fragment = "\
# header comment
# another note
[models.\"x\"]
name = \"X\"
tier = \"mid\"
open_weight = true
";
assert!(leading_bare_key_error("60-models/test.toml", fragment).is_none());
}
#[test]
fn accepts_keys_inside_tables() {
let fragment = "\
[models.\"x\"]
name = \"X\"
tier = \"mid\"
[models.\"y\"]
name = \"Y\"
tier = \"frontier\"
";
assert!(leading_bare_key_error("60-models/test.toml", fragment).is_none());
}
#[test]
fn generated_config_rejects_unknown_model_fields() {
let tempdir = tempfile::tempdir().expect("tempdir");
let model_dir = tempdir.path().join("60-models");
fs::create_dir_all(&model_dir).expect("create model dir");
fs::write(
model_dir.join("bad.toml"),
r#"
[models."demo/bad"]
name = "Bad"
provider = "demo"
context_window = 8192
fast_mode = true
"#,
)
.expect("write fragment");
let error = match generated_provider_config(tempdir.path()) {
Ok(_) => panic!("unknown field should be rejected"),
Err(error) => error,
};
assert!(
error.contains("generated provider config contains unknown fields"),
"got: {error}"
);
assert!(error.contains("fast_mode"), "got: {error}");
assert!(error.contains("serving_tiers"), "got: {error}");
}
#[test]
fn generated_config_rejects_unknown_logical_default() {
let tempdir = tempfile::tempdir().expect("tempdir");
let defaults_dir = tempdir.path().join("40-defaults");
fs::create_dir_all(&defaults_dir).expect("create defaults dir");
fs::write(
defaults_dir.join("generation.toml"),
r#"
[model_defaults."logical:not-in-catalog"]
temperature = 1.0
"#,
)
.expect("write fragment");
let error = match generated_provider_config(tempdir.path()) {
Ok(_) => panic!("unknown logical selector should be rejected"),
Err(error) => error,
};
assert!(
error.contains("logical:not-in-catalog references an unknown logical model"),
"got: {error}"
);
}
#[test]
fn generated_config_rejects_malformed_route_tombstone() {
let tempdir = tempfile::tempdir().expect("tempdir");
let defaults_dir = tempdir.path().join("40-defaults");
fs::create_dir_all(&defaults_dir).expect("create defaults dir");
fs::write(
defaults_dir.join("generation.toml"),
r#"
[model_defaults."provider/model"]
_unset = ["not_a_generation_default"]
"#,
)
.expect("write fragment");
let error = match generated_provider_config(tempdir.path()) {
Ok(_) => panic!("invalid route tombstone should be rejected"),
Err(error) => error,
};
assert!(
error.contains("_unset must be a non-empty list of supported route-default keys"),
"got: {error}"
);
}
#[test]
fn generate_uses_source_fragments_for_toml_and_catalog_artifacts() {
let tempdir = tempfile::tempdir().expect("tempdir");
let catalog_source = tempdir.path().join("catalog_sources");
let capability_source = tempdir.path().join("capability_sources");
fs::create_dir_all(catalog_source.join("00-base")).expect("create catalog source");
fs::create_dir_all(capability_source.join("00-base")).expect("create capability source");
fs::write(
catalog_source.join("00-base/private.toml"),
r#"
[providers.private]
display_name = "Private"
base_url = "http://127.0.0.1:9000"
auth_style = "none"
chat_endpoint = "/v1/chat/completions"
[models."private/fast"]
name = "Private Fast"
provider = "private"
context_window = 8192
family = "private"
lineage = "private"
"#,
)
.expect("write catalog source");
let capability_path = capability_source.join("00-base/private.toml");
fs::write(
&capability_path,
r#"
[[provider.private]]
model_match = "*"
native_tools = true
prompt_caching = true
"#,
)
.expect("write capability source");
let args = ProvidersGenerateArgs {
source_dir: catalog_source,
capability_source_dir: capability_source,
providers_output: tempdir.path().join("out/providers.toml"),
capabilities_output: tempdir.path().join("out/capabilities.toml"),
artifact_dir: tempdir.path().join("out/catalog"),
check: false,
};
run_generate(&args).expect("generate succeeds");
let providers = fs::read_to_string(&args.providers_output).expect("providers output");
assert!(providers.contains("[models.\"private/fast\"]"));
let capabilities =
fs::read_to_string(&args.capabilities_output).expect("capabilities output");
assert!(capabilities.contains("native_tools = true"));
let catalog_json = fs::read_to_string(args.artifact_dir.join("provider-catalog.json"))
.expect("catalog json");
let catalog: serde_json::Value =
serde_json::from_str(&catalog_json).expect("catalog parses");
let model = catalog["models"]
.as_array()
.expect("models array")
.iter()
.find(|model| model["id"] == "private/fast")
.expect("private model");
assert_eq!(model["tool_support"]["native"], true);
assert_eq!(model["prompt_cache"], true);
fs::write(
&capability_path,
r#"
[[provider.private]]
model_match = "*"
native_tools = false
prompt_caching = false
"#,
)
.expect("mutate capability source");
let mut check_args = args;
check_args.check = true;
let error = run_generate(&check_args).expect_err("mutated source should drift");
assert!(error.contains("drifted"), "got: {error}");
}
}