From b2b64dcc64560bd395d9384325a760646dc3351e Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sun, 1 Mar 2026 22:04:32 -0500 Subject: [PATCH] Restructure CLI: nest runs under `arc run`, promote models to top-level MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `arc run ` → `arc run start ` - `arc runs list/prune` → `arc run list/prune` - `arc llm models list` → `arc models list` - Remove `arc llm models sync` - Add `arc models test [--provider, --model]` for connectivity testing - Clean up dead code (RunsArgs, RunsCommand, runs_command) Co-Authored-By: Claude Opus 4.6 --- crates/arc-cli/src/main.rs | 58 +++++++++++------- crates/arc-llm/src/cli.rs | 89 ++++++++++++++++++---------- crates/arc-workflows/src/cli/runs.rs | 31 +--------- 3 files changed, 97 insertions(+), 81 deletions(-) diff --git a/crates/arc-cli/src/main.rs b/crates/arc-cli/src/main.rs index 8107db9f1..5869d995e 100644 --- a/crates/arc-cli/src/main.rs +++ b/crates/arc-cli/src/main.rs @@ -17,32 +17,43 @@ struct Cli { #[derive(Subcommand)] enum Command { - /// LLM prompt and model operations + /// LLM prompt operations Llm { #[command(subcommand)] command: LlmCommand, }, /// Run an agentic coding session Agent(arc_agent::cli::AgentArgs), - /// Launch a pipeline - Run(arc_workflows::cli::RunArgs), + /// Launch and manage pipeline runs + Run { + #[command(subcommand)] + command: RunCommand, + }, /// Validate a pipeline Validate(arc_workflows::cli::ValidateArgs), - /// List and manage pipeline runs - Runs(arc_workflows::cli::runs::RunsArgs), + /// List and test LLM models + Models { + #[command(subcommand)] + command: Option, + }, /// Start the HTTP API server Serve(arc_api::serve::ServeArgs), } +#[derive(Subcommand)] +enum RunCommand { + /// Launch a pipeline from a .dot or .toml task file + Start(arc_workflows::cli::RunArgs), + /// List pipeline runs + List(arc_workflows::cli::runs::RunsListArgs), + /// Delete old pipeline runs + Prune(arc_workflows::cli::runs::RunsPruneArgs), +} + #[derive(Subcommand)] enum LlmCommand { /// Execute a prompt Prompt(arc_llm::cli::PromptArgs), - /// Manage models - Models { - #[command(subcommand)] - command: Option, - }, } #[tokio::main] @@ -59,9 +70,9 @@ async fn main() -> Result<()> { let command_name = match &cli.command { Command::Llm { .. } => "llm", Command::Agent(_) => "agent", - Command::Run(_) => "run", + Command::Run { .. } => "run", Command::Validate(_) => "validate", - Command::Runs(_) => "runs", + Command::Models { .. } => "models", Command::Serve(_) => "serve", }; debug!(command = %command_name, "CLI command started"); @@ -69,21 +80,26 @@ async fn main() -> Result<()> { match cli.command { Command::Llm { command } => match command { LlmCommand::Prompt(args) => arc_llm::cli::run_prompt(args).await?, - LlmCommand::Models { command } => arc_llm::cli::run_models(command).await?, }, Command::Agent(args) => arc_agent::cli::run_with_args(args).await?, - Command::Run(args) => { - let styles: &'static arc_util::terminal::Styles = - Box::leak(Box::new(arc_util::terminal::Styles::detect_stderr())); - arc_workflows::cli::run::run_command(args, styles).await?; - } + Command::Run { command } => match command { + RunCommand::Start(args) => { + let styles: &'static arc_util::terminal::Styles = + Box::leak(Box::new(arc_util::terminal::Styles::detect_stderr())); + arc_workflows::cli::run::run_command(args, styles).await?; + } + RunCommand::List(args) => { + arc_workflows::cli::runs::list_command(&args)?; + } + RunCommand::Prune(args) => { + arc_workflows::cli::runs::prune_command(&args)?; + } + }, Command::Validate(args) => { let styles = arc_util::terminal::Styles::detect_stderr(); arc_workflows::cli::validate::validate_command(&args, &styles)?; } - Command::Runs(args) => { - arc_workflows::cli::runs::runs_command(args)?; - } + Command::Models { command } => arc_llm::cli::run_models(command).await?, Command::Serve(args) => { let styles: &'static arc_util::terminal::Styles = Box::leak(Box::new(arc_util::terminal::Styles::detect_stderr())); diff --git a/crates/arc-llm/src/cli.rs b/crates/arc-llm/src/cli.rs index ccf94df48..d8071e37f 100644 --- a/crates/arc-llm/src/cli.rs +++ b/crates/arc-llm/src/cli.rs @@ -1,4 +1,5 @@ use std::io::{self, IsTerminal, Read}; +use std::time::Duration; use anyhow::{bail, Context, Result}; use clap::{Args, Subcommand}; @@ -50,15 +51,15 @@ pub enum ModelsCommand { query: Option, }, - /// Download model metadata from OpenRouter - Sync { - /// URL to fetch models from - #[arg(long, default_value = "https://openrouter.ai/api/v1/models")] - url: String, + /// Test model availability by sending a simple prompt + Test { + /// Filter by provider + #[arg(short, long)] + provider: Option, - /// Output file path - #[arg(short, long, default_value = "openrouter_models.json")] - output: String, + /// Test a specific model + #[arg(short, long)] + model: Option, }, } @@ -233,26 +234,6 @@ pub async fn run_prompt(args: PromptArgs) -> Result<()> { Ok(()) } -async fn sync_models(url: &str, output: &str) -> Result<()> { - let body = reqwest::get(url) - .await - .context("failed to connect to models endpoint")? - .error_for_status() - .context("models endpoint returned an error")? - .text() - .await - .context("failed to read response body")?; - - let json: serde_json::Value = - serde_json::from_str(&body).context("response is not valid JSON")?; - let pretty = serde_json::to_string_pretty(&json).context("failed to format JSON")?; - - std::fs::write(output, &pretty).with_context(|| format!("failed to write {output}"))?; - - eprintln!("Saved models to {output}"); - Ok(()) -} - pub async fn run_models(command: Option) -> Result<()> { let command = command.unwrap_or(ModelsCommand::List { provider: None, @@ -276,10 +257,58 @@ pub async fn run_models(command: Option) -> Result<()> { print_models_table(&models); } - ModelsCommand::Sync { url, output } => { - sync_models(&url, &output).await?; + ModelsCommand::Test { provider, model } => { + test_models(provider.as_deref(), model.as_deref()).await?; } } Ok(()) } + +async fn test_models(provider: Option<&str>, model: Option<&str>) -> Result<()> { + let models_to_test = if let Some(model_id) = model { + match catalog::get_model_info(model_id) { + Some(info) => vec![info], + None => bail!("Unknown model: {model_id}"), + } + } else { + catalog::list_models(provider) + }; + + if models_to_test.is_empty() { + bail!("No models found"); + } + + println!("{:<30} {:<12} RESULT", "MODEL", "PROVIDER"); + + let mut failures = 0u32; + for info in &models_to_test { + let params = GenerateParams::new(&info.id) + .provider(&info.provider) + .prompt("Say OK") + .max_tokens(5); + + let result = + tokio::time::timeout(Duration::from_secs(10), generate::generate(params)).await; + + let status = match result { + Ok(Ok(_)) => "ok".to_string(), + Ok(Err(e)) => { + failures += 1; + format!("error: {e}") + } + Err(_) => { + failures += 1; + "error: timeout (10s)".to_string() + } + }; + + println!("{:<30} {:<12} {status}", info.id, info.provider); + } + + if failures > 0 { + bail!("{failures} model(s) failed"); + } + + Ok(()) +} diff --git a/crates/arc-workflows/src/cli/runs.rs b/crates/arc-workflows/src/cli/runs.rs index 74054ddd5..e1d43bd1d 100644 --- a/crates/arc-workflows/src/cli/runs.rs +++ b/crates/arc-workflows/src/cli/runs.rs @@ -2,25 +2,10 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; use anyhow::Result; -use clap::{Args, Subcommand}; +use clap::Args; use serde::Serialize; use tracing::{debug, info}; -/// Arguments for the `arc runs` command. -#[derive(Args)] -pub struct RunsArgs { - #[command(subcommand)] - pub command: Option, -} - -#[derive(Subcommand)] -pub enum RunsCommand { - /// List pipeline runs - List(RunsListArgs), - /// Delete old pipeline runs - Prune(RunsPruneArgs), -} - #[derive(Args)] pub struct RunsListArgs { /// Only show runs started before this date (YYYY-MM-DD prefix match) @@ -336,20 +321,6 @@ pub fn prune_from(args: &RunsPruneArgs, base: &Path) -> Result<()> { Ok(()) } -pub fn runs_command(args: RunsArgs) -> Result<()> { - match args.command { - None => list_command(&RunsListArgs { - before: None, - pipeline: None, - label: Vec::new(), - orphans: false, - json: false, - }), - Some(RunsCommand::List(args)) => list_command(&args), - Some(RunsCommand::Prune(args)) => prune_command(&args), - } -} - #[cfg(test)] mod tests { use super::*;