Skip to content

Commit 5f3c63a

Browse files
mvanhornclaude
andauthored
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>
1 parent 18127f1 commit 5f3c63a

2 files changed

Lines changed: 185 additions & 3 deletions

File tree

crates/goose-cli/src/cli.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,8 @@ enum Command {
709709
/// Show verbose information including current configuration
710710
#[arg(short, long, help = "Show verbose information including config.yaml")]
711711
verbose: bool,
712+
#[arg(long, help = "Test provider connection and show status")]
713+
check: bool,
712714
},
713715

714716
#[command(about = "Check that your Goose setup is working")]
@@ -1765,7 +1767,7 @@ pub async fn cli() -> anyhow::Result<()> {
17651767
}
17661768
Some(Command::Configure {}) => handle_configure().await,
17671769
Some(Command::Doctor {}) => crate::commands::doctor::handle_doctor().await,
1768-
Some(Command::Info { verbose }) => handle_info(verbose),
1770+
Some(Command::Info { verbose, check }) => handle_info(verbose, check).await,
17691771
Some(Command::Mcp { server }) => handle_mcp_command(server).await,
17701772
Some(Command::Acp { builtins }) => goose::acp::server::run(builtins).await,
17711773
Some(Command::Serve {

crates/goose-cli/src/commands/info.rs

Lines changed: 182 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
use anyhow::Result;
1+
use anyhow::{anyhow, Result};
22
use console::style;
33
use goose::config::paths::Paths;
44
use goose::config::Config;
5+
use goose::conversation::message::Message;
6+
use goose::providers::errors::ProviderError;
57
use goose::session::session_manager::{DB_NAME, SESSIONS_FOLDER};
68
use serde_yaml;
9+
use std::time::Duration;
710

811
fn print_aligned(label: &str, value: &str, width: usize) {
912
println!(" {:<width$} {}", label, value, width = width);
@@ -32,7 +35,74 @@ fn check_path_status(path: &Path) -> String {
3235
}
3336
}
3437

35-
pub fn handle_info(verbose: bool) -> Result<()> {
38+
struct ProviderCheckSuccess {
39+
provider: String,
40+
model: String,
41+
elapsed: Duration,
42+
}
43+
44+
enum ProviderCheckError {
45+
NotConfigured {
46+
label: &'static str,
47+
error: String,
48+
},
49+
InvalidModel(String),
50+
ProviderCreate {
51+
error: String,
52+
show_api_key_hint: bool,
53+
},
54+
ProviderRequest(ProviderError),
55+
}
56+
57+
async fn check_provider(
58+
config: &Config,
59+
) -> std::result::Result<ProviderCheckSuccess, ProviderCheckError> {
60+
let (provider, model) = match (config.get_goose_provider(), config.get_goose_model()) {
61+
(Ok(provider), Ok(model)) => (provider, model),
62+
(Err(e), _) => {
63+
return Err(ProviderCheckError::NotConfigured {
64+
label: "Provider:",
65+
error: e.to_string(),
66+
});
67+
}
68+
(_, Err(e)) => {
69+
return Err(ProviderCheckError::NotConfigured {
70+
label: "Model:",
71+
error: e.to_string(),
72+
});
73+
}
74+
};
75+
76+
let model_config = goose::model::ModelConfig::new(&model)
77+
.map_err(|e| ProviderCheckError::InvalidModel(e.to_string()))?
78+
.with_canonical_limits(&provider);
79+
80+
let provider_client = goose::providers::create(&provider, model_config, Vec::new())
81+
.await
82+
.map_err(|e| {
83+
let error = e.to_string();
84+
ProviderCheckError::ProviderCreate {
85+
show_api_key_hint: error.contains("not found") || error.contains("API_KEY"),
86+
error,
87+
}
88+
})?;
89+
90+
let test_msg = Message::user().with_text("Say 'ok'");
91+
let model_config = provider_client.get_model_config();
92+
let start = std::time::Instant::now();
93+
provider_client
94+
.complete(&model_config, "check", "", &[test_msg], &[])
95+
.await
96+
.map_err(ProviderCheckError::ProviderRequest)?;
97+
98+
Ok(ProviderCheckSuccess {
99+
provider,
100+
model,
101+
elapsed: start.elapsed(),
102+
})
103+
}
104+
105+
pub async fn handle_info(verbose: bool, check: bool) -> Result<()> {
36106
let logs_dir = Paths::in_state_dir("logs");
37107
let sessions_dir = Paths::in_data_dir(SESSIONS_FOLDER);
38108
let sessions_db = sessions_dir.join(DB_NAME);
@@ -90,5 +160,115 @@ pub fn handle_info(verbose: bool) -> Result<()> {
90160
}
91161
}
92162

163+
if check {
164+
println!("\n{}", style("Provider Check:").cyan().bold());
165+
166+
let result = check_provider(config).await;
167+
match &result {
168+
Ok(success) => {
169+
print_aligned("Provider:", &success.provider, label_padding);
170+
print_aligned("Model:", &success.model, label_padding);
171+
print_aligned("Auth:", &style("ok").green().to_string(), label_padding);
172+
print_aligned(
173+
"Connection:",
174+
&format!(
175+
"{} (verified in {:.1}s)",
176+
style("ok").green(),
177+
success.elapsed.as_secs_f64()
178+
),
179+
label_padding,
180+
);
181+
}
182+
Err(ProviderCheckError::NotConfigured { label, error }) => {
183+
print_aligned(
184+
label,
185+
&format!("{} {}", style("not configured:").red(), error),
186+
label_padding,
187+
);
188+
print_aligned(
189+
"Hint:",
190+
&format!("Run '{}'", style("goose configure").cyan()),
191+
label_padding,
192+
);
193+
}
194+
Err(ProviderCheckError::InvalidModel(error)) => {
195+
print_aligned(
196+
"Model:",
197+
&format!("{} {}", style("invalid:").red(), error),
198+
label_padding,
199+
);
200+
}
201+
Err(ProviderCheckError::ProviderCreate {
202+
error,
203+
show_api_key_hint,
204+
}) => {
205+
// Split auth failures (missing/invalid credential) from provider
206+
// construction failures (unknown provider, malformed provider
207+
// config). Labeling the latter as "Auth: FAILED" misdirects
208+
// troubleshooting toward rotating API keys.
209+
if *show_api_key_hint {
210+
print_aligned(
211+
"Auth:",
212+
&format!("{} {}", style("FAILED").red().bold(), error),
213+
label_padding,
214+
);
215+
print_aligned(
216+
"Hint:",
217+
&format!(
218+
"Set the API key in your environment or run '{}'",
219+
style("goose configure").cyan()
220+
),
221+
label_padding,
222+
);
223+
} else {
224+
print_aligned(
225+
"Provider:",
226+
&format!("{} {}", style("FAILED").red().bold(), error),
227+
label_padding,
228+
);
229+
print_aligned(
230+
"Hint:",
231+
&format!(
232+
"Check the provider name and config, or run '{}'",
233+
style("goose configure").cyan()
234+
),
235+
label_padding,
236+
);
237+
}
238+
}
239+
Err(ProviderCheckError::ProviderRequest(error)) => match error {
240+
ProviderError::Authentication(_) => {
241+
print_aligned(
242+
"Auth:",
243+
&format!("{} {}", style("FAILED").red().bold(), error),
244+
label_padding,
245+
);
246+
print_aligned(
247+
"Hint:",
248+
&format!(
249+
"Check your API key or run '{}'",
250+
style("goose configure").cyan()
251+
),
252+
label_padding,
253+
);
254+
}
255+
_ => {
256+
print_aligned(
257+
"Check:",
258+
&format!("{} {}", style("FAILED").red().bold(), error),
259+
label_padding,
260+
);
261+
}
262+
},
263+
}
264+
265+
// Propagate non-zero exit status so automation (CI scripts, install
266+
// checks, health probes) can rely on `goose info --check` as a
267+
// pre-flight verifier.
268+
if result.is_err() {
269+
return Err(anyhow!("provider check failed"));
270+
}
271+
}
272+
93273
Ok(())
94274
}

0 commit comments

Comments
 (0)