feat(cli): add --check flag to goose info for provider testing (#8289)

Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Van Horn 2026-04-23 06:34:06 -07:00 committed by GitHub
parent 18127f1ff0
commit 5f3c63ae41
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 185 additions and 3 deletions

View file

@ -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 {

View file

@ -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!(" {:<width$} {}", label, value, width = width);
@ -32,7 +35,74 @@ fn check_path_status(path: &Path) -> 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<ProviderCheckSuccess, ProviderCheckError> {
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(())
}