diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index 0a5efeb75c..ecf7f81d76 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -709,6 +709,8 @@ enum Command { /// Show verbose information including current configuration #[arg(short, long, help = "Show verbose information including config.yaml")] verbose: bool, + #[arg(long, help = "Test provider connection and show status")] + check: bool, }, #[command(about = "Check that your Goose setup is working")] @@ -1765,7 +1767,7 @@ pub async fn cli() -> anyhow::Result<()> { } Some(Command::Configure {}) => handle_configure().await, Some(Command::Doctor {}) => crate::commands::doctor::handle_doctor().await, - Some(Command::Info { verbose }) => handle_info(verbose), + Some(Command::Info { verbose, check }) => handle_info(verbose, check).await, Some(Command::Mcp { server }) => handle_mcp_command(server).await, Some(Command::Acp { builtins }) => goose::acp::server::run(builtins).await, Some(Command::Serve { diff --git a/crates/goose-cli/src/commands/info.rs b/crates/goose-cli/src/commands/info.rs index 56c9d295b0..d702b3e43e 100644 --- a/crates/goose-cli/src/commands/info.rs +++ b/crates/goose-cli/src/commands/info.rs @@ -1,9 +1,12 @@ -use anyhow::Result; +use anyhow::{anyhow, Result}; use console::style; use goose::config::paths::Paths; use goose::config::Config; +use goose::conversation::message::Message; +use goose::providers::errors::ProviderError; use goose::session::session_manager::{DB_NAME, SESSIONS_FOLDER}; use serde_yaml; +use std::time::Duration; fn print_aligned(label: &str, value: &str, width: usize) { println!(" {: String { } } -pub fn handle_info(verbose: bool) -> Result<()> { +struct ProviderCheckSuccess { + provider: String, + model: String, + elapsed: Duration, +} + +enum ProviderCheckError { + NotConfigured { + label: &'static str, + error: String, + }, + InvalidModel(String), + ProviderCreate { + error: String, + show_api_key_hint: bool, + }, + ProviderRequest(ProviderError), +} + +async fn check_provider( + config: &Config, +) -> std::result::Result { + let (provider, model) = match (config.get_goose_provider(), config.get_goose_model()) { + (Ok(provider), Ok(model)) => (provider, model), + (Err(e), _) => { + return Err(ProviderCheckError::NotConfigured { + label: "Provider:", + error: e.to_string(), + }); + } + (_, Err(e)) => { + return Err(ProviderCheckError::NotConfigured { + label: "Model:", + error: e.to_string(), + }); + } + }; + + let model_config = goose::model::ModelConfig::new(&model) + .map_err(|e| ProviderCheckError::InvalidModel(e.to_string()))? + .with_canonical_limits(&provider); + + let provider_client = goose::providers::create(&provider, model_config, Vec::new()) + .await + .map_err(|e| { + let error = e.to_string(); + ProviderCheckError::ProviderCreate { + show_api_key_hint: error.contains("not found") || error.contains("API_KEY"), + error, + } + })?; + + let test_msg = Message::user().with_text("Say 'ok'"); + let model_config = provider_client.get_model_config(); + let start = std::time::Instant::now(); + provider_client + .complete(&model_config, "check", "", &[test_msg], &[]) + .await + .map_err(ProviderCheckError::ProviderRequest)?; + + Ok(ProviderCheckSuccess { + provider, + model, + elapsed: start.elapsed(), + }) +} + +pub async fn handle_info(verbose: bool, check: bool) -> Result<()> { let logs_dir = Paths::in_state_dir("logs"); let sessions_dir = Paths::in_data_dir(SESSIONS_FOLDER); let sessions_db = sessions_dir.join(DB_NAME); @@ -90,5 +160,115 @@ pub fn handle_info(verbose: bool) -> Result<()> { } } + if check { + println!("\n{}", style("Provider Check:").cyan().bold()); + + let result = check_provider(config).await; + match &result { + Ok(success) => { + print_aligned("Provider:", &success.provider, label_padding); + print_aligned("Model:", &success.model, label_padding); + print_aligned("Auth:", &style("ok").green().to_string(), label_padding); + print_aligned( + "Connection:", + &format!( + "{} (verified in {:.1}s)", + style("ok").green(), + success.elapsed.as_secs_f64() + ), + label_padding, + ); + } + Err(ProviderCheckError::NotConfigured { label, error }) => { + print_aligned( + label, + &format!("{} {}", style("not configured:").red(), error), + label_padding, + ); + print_aligned( + "Hint:", + &format!("Run '{}'", style("goose configure").cyan()), + label_padding, + ); + } + Err(ProviderCheckError::InvalidModel(error)) => { + print_aligned( + "Model:", + &format!("{} {}", style("invalid:").red(), error), + label_padding, + ); + } + Err(ProviderCheckError::ProviderCreate { + error, + show_api_key_hint, + }) => { + // Split auth failures (missing/invalid credential) from provider + // construction failures (unknown provider, malformed provider + // config). Labeling the latter as "Auth: FAILED" misdirects + // troubleshooting toward rotating API keys. + if *show_api_key_hint { + print_aligned( + "Auth:", + &format!("{} {}", style("FAILED").red().bold(), error), + label_padding, + ); + print_aligned( + "Hint:", + &format!( + "Set the API key in your environment or run '{}'", + style("goose configure").cyan() + ), + label_padding, + ); + } else { + print_aligned( + "Provider:", + &format!("{} {}", style("FAILED").red().bold(), error), + label_padding, + ); + print_aligned( + "Hint:", + &format!( + "Check the provider name and config, or run '{}'", + style("goose configure").cyan() + ), + label_padding, + ); + } + } + Err(ProviderCheckError::ProviderRequest(error)) => match error { + ProviderError::Authentication(_) => { + print_aligned( + "Auth:", + &format!("{} {}", style("FAILED").red().bold(), error), + label_padding, + ); + print_aligned( + "Hint:", + &format!( + "Check your API key or run '{}'", + style("goose configure").cyan() + ), + label_padding, + ); + } + _ => { + print_aligned( + "Check:", + &format!("{} {}", style("FAILED").red().bold(), error), + label_padding, + ); + } + }, + } + + // Propagate non-zero exit status so automation (CI scripts, install + // checks, health probes) can rely on `goose info --check` as a + // pre-flight verifier. + if result.is_err() { + return Err(anyhow!("provider check failed")); + } + } + Ok(()) }