use anyhow::Result;
use clap::{Parser, Subcommand};
use crate::claude::model_config::{get_model_registry, ModelSource, MODELS_YAML};
#[derive(Parser)]
pub struct ConfigCommand {
#[command(subcommand)]
pub command: ConfigSubcommands,
}
#[derive(Subcommand)]
pub enum ConfigSubcommands {
Models(ModelsCommand),
}
#[derive(Parser)]
pub struct ModelsCommand {
#[command(subcommand)]
pub command: ModelsSubcommands,
}
#[derive(Subcommand)]
pub enum ModelsSubcommands {
Show(ShowCommand),
}
#[derive(Parser)]
pub struct ShowCommand {
#[arg(long)]
pub embedded_only: bool,
}
impl ConfigCommand {
pub fn execute(self) -> Result<()> {
match self.command {
ConfigSubcommands::Models(models_cmd) => models_cmd.execute(),
}
}
}
impl ModelsCommand {
pub fn execute(self) -> Result<()> {
match self.command {
ModelsSubcommands::Show(show_cmd) => show_cmd.execute(),
}
}
}
impl ShowCommand {
pub fn execute(self) -> Result<()> {
if self.embedded_only {
print!("{MODELS_YAML}");
return Ok(());
}
let registry = get_model_registry();
let yaml = render_merged_yaml(registry.config())?;
print!("{yaml}");
Ok(())
}
}
fn render_merged_yaml(config: &crate::claude::model_config::ModelConfiguration) -> Result<String> {
let yaml = serde_yaml::to_string(config)?;
Ok(prepend_layer_summary(&yaml, config))
}
fn prepend_layer_summary(
yaml: &str,
config: &crate::claude::model_config::ModelConfiguration,
) -> String {
let mut counts: std::collections::BTreeMap<ModelSource, usize> =
std::collections::BTreeMap::new();
for spec in &config.models {
*counts.entry(spec.source).or_default() += 1;
}
let mut header = String::new();
header.push_str("# Merged model catalog (project > user > embedded).\n");
header.push_str("# Each entry's `source:` field indicates the layer that contributed it.\n");
header.push_str("# Models by source: ");
let parts: Vec<String> = counts.iter().map(|(s, n)| format!("{s}={n}")).collect();
if parts.is_empty() {
header.push_str("(none)");
} else {
header.push_str(&parts.join(", "));
}
header.push_str(".\n#\n");
let mut out = header;
out.push_str(yaml);
out
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::claude::model_config::ModelRegistry;
use std::io::Write;
use std::path::Path;
fn write(dir: &Path, name: &str, contents: &str) -> std::path::PathBuf {
let path = dir.join(name);
std::fs::File::create(&path)
.unwrap()
.write_all(contents.as_bytes())
.unwrap();
path
}
#[test]
fn rendered_yaml_includes_source_for_each_entry() {
let dir = tempfile::tempdir().unwrap();
let user = write(
dir.path(),
"user.yaml",
r#"
version: "1"
models:
- provider: "claude"
model: "Custom"
api_identifier: "claude-custom-x"
max_output_tokens: 1
input_context: 1
generation: 1.0
tier: "flagship"
"#,
);
let registry = ModelRegistry::load_layered_from_paths(None, Some(&user), None).unwrap();
let yaml = render_merged_yaml(registry.config()).unwrap();
assert!(yaml.contains("Merged model catalog"));
assert!(yaml.contains("embedded="));
assert!(yaml.contains("user="));
assert!(yaml.contains("api_identifier: claude-custom-x"));
assert!(yaml.contains("source: user"));
assert!(yaml.contains("source: embedded"));
}
#[test]
fn embedded_only_flag_round_trips_embedded_yaml() {
let cmd = ShowCommand {
embedded_only: true,
};
cmd.execute().unwrap();
assert!(MODELS_YAML.contains("version: \"1\""));
}
#[test]
fn layer_summary_handles_empty_models() {
let config = crate::claude::model_config::ModelConfiguration {
version: Some("1".into()),
models: Vec::new(),
providers: std::collections::HashMap::new(),
};
let summary = prepend_layer_summary("", &config);
assert!(summary.contains("Models by source: (none)"));
}
}