diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index ab0568729ec1..31c0cb4fabcc 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -691,21 +691,22 @@ pub async fn configure_provider_dialog() -> anyhow::Result { }; spin.stop(style("Model fetch complete").green()); - // Select a model: on fetch error show styled error and abort; if Some(models), show list; if None, free-text input - let model: String = match models_res { - Err(e) => { - // Provider hook error - cliclack::outro(style(e.to_string()).on_red().white())?; - return Ok(false); - } - Ok(Some(models)) => select_model_from_list(&models, provider_meta)?, - Ok(None) => { - let default_model = - std::env::var("GOOSE_MODEL").unwrap_or(provider_meta.default_model.clone()); + if let Err(ref e) = models_res { + let _ = cliclack::log::warning(format!("Could not fetch models: {e}")); + } + let model: String = match provider_meta.resolve_models_with_fallback(models_res) { + Ok(models) if !models.is_empty() => select_model_from_list(&models, provider_meta)?, + Ok(_) => { + let default_model = std::env::var("GOOSE_MODEL") + .unwrap_or_else(|_| provider_meta.default_model.clone()); cliclack::input("Enter a model from that provider:") .default_input(&default_model) .interact()? } + Err(e) => { + cliclack::outro(style(e.to_string()).on_red().white())?; + return Ok(false); + } }; if model.to_lowercase().starts_with("gemini-3") { diff --git a/crates/goose-server/src/routes/config_management.rs b/crates/goose-server/src/routes/config_management.rs index 6c12b6c0e1f9..36dccded6090 100644 --- a/crates/goose-server/src/routes/config_management.rs +++ b/crates/goose-server/src/routes/config_management.rs @@ -372,19 +372,6 @@ pub async fn providers() -> Result>, ErrorResponse> { pub async fn get_provider_models( Path(name): Path, ) -> Result>, ErrorResponse> { - let loaded_provider = goose::config::declarative_providers::load_provider(name.as_str()).ok(); - // TODO(Douwe): support a get models url for custom providers - if let Some(loaded_provider) = loaded_provider { - return Ok(Json( - loaded_provider - .config - .models - .into_iter() - .map(|m| m.name) - .collect::>(), - )); - } - let all = get_providers().await.into_iter().collect::>(); let Some((metadata, provider_type)) = all.into_iter().find(|(m, _)| m.name == name) else { return Err(ErrorResponse::bad_request(format!( @@ -403,10 +390,11 @@ pub async fn get_provider_models( let provider = goose::providers::create(&name, model_config).await?; let models_result = provider.fetch_recommended_models().await; - - match models_result { - Ok(Some(models)) => Ok(Json(models)), - Ok(None) => Ok(Json(Vec::new())), + if let Err(ref e) = models_result { + tracing::warn!("Model fetch failed for provider '{}': {e}", name); + } + match metadata.resolve_models_with_fallback(models_result) { + Ok(models) => Ok(Json(models)), Err(provider_error) => Err(provider_error.into()), } } diff --git a/crates/goose/src/providers/base.rs b/crates/goose/src/providers/base.rs index ab343750e1a9..b8782f84c327 100644 --- a/crates/goose/src/providers/base.rs +++ b/crates/goose/src/providers/base.rs @@ -185,6 +185,34 @@ impl ProviderMetadata { self.allows_unlisted_models = true; self } + + pub fn resolve_models_with_fallback( + &self, + fetch_result: Result>, ProviderError>, + ) -> Result, ProviderError> { + if let Ok(Some(ref models)) = fetch_result { + if !models.is_empty() { + return Ok(models.clone()); + } + } + + let known: Vec = self.known_models.iter().map(|m| m.name.clone()).collect(); + if !known.is_empty() { + return Ok(known); + } + + if self.allows_unlisted_models { + return Ok(Vec::new()); + } + + match fetch_result { + Err(e) => Err(e), + _ => Err(ProviderError::ExecutionError(format!( + "No models available for provider '{}'", + self.name + ))), + } + } } /// Configuration key metadata for provider setup @@ -786,4 +814,56 @@ mod tests { assert_eq!(info.output_token_cost, Some(0.00001)); assert_eq!(info.currency, Some("$".to_string())); } + + fn metadata_with_known_models(models: Vec<&str>, allows_unlisted: bool) -> ProviderMetadata { + let mut meta = ProviderMetadata::empty(); + meta.name = "test-provider".to_string(); + meta.known_models = models + .into_iter() + .map(|n| ModelInfo::new(n, 4096)) + .collect(); + meta.allows_unlisted_models = allows_unlisted; + meta + } + + #[test] + fn resolve_models_uses_fetched_models() { + let meta = metadata_with_known_models(vec!["known-1"], false); + let result = meta + .resolve_models_with_fallback(Ok(Some(vec!["fetched-1".into()]))) + .unwrap(); + assert_eq!(result, vec!["fetched-1"]); + } + + #[test] + fn resolve_models_falls_back_to_known_models() { + let meta = metadata_with_known_models(vec!["known-1"], false); + // Empty fetch, None, and error all fall back to known models + for fetch in [ + Ok(Some(Vec::new())), + Ok(None), + Err(ProviderError::RequestFailed("fail".into())), + ] { + let result = meta.resolve_models_with_fallback(fetch).unwrap(); + assert_eq!(result, vec!["known-1"]); + } + } + + #[test] + fn resolve_models_allows_empty_when_unlisted() { + let meta = metadata_with_known_models(vec![], true); + assert!(meta + .resolve_models_with_fallback(Ok(None)) + .unwrap() + .is_empty()); + } + + #[test] + fn resolve_models_errors_when_no_fallback() { + let meta = metadata_with_known_models(vec![], false); + assert!(meta + .resolve_models_with_fallback(Err(ProviderError::RequestFailed("fail".into()))) + .is_err()); + assert!(meta.resolve_models_with_fallback(Ok(None)).is_err()); + } }