Custom providers update (#4099)

Co-authored-by: developerayo <shodipovi@gmail.com>
Co-authored-by: Douwe Osinga <douwe@squareup.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Zane Staggs <zane@squareup.com>
This commit is contained in:
Douwe Osinga
2025-08-19 18:03:10 -04:00
committed by GitHub
parent 1d93a59f31
commit 942ef5b0a3
31 changed files with 1704 additions and 567 deletions
+139 -12
View File
@@ -1,3 +1,4 @@
use crate::recipes::github_recipe::GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY;
use cliclack::spinner;
use console::style;
use goose::agents::extension::ToolInfo;
@@ -7,6 +8,7 @@ use goose::agents::platform_tools::{
};
use goose::agents::Agent;
use goose::agents::{extension::Envs, ExtensionConfig};
use goose::config::custom_providers::CustomProviderConfig;
use goose::config::extensions::name_to_key;
use goose::config::permission::PermissionLevel;
use goose::config::{
@@ -14,6 +16,7 @@ use goose::config::{
PermissionManager,
};
use goose::conversation::message::Message;
use goose::model::ModelConfig;
use goose::providers::{create, providers};
use rmcp::model::{Tool, ToolAnnotations};
use rmcp::object;
@@ -21,8 +24,6 @@ use serde_json::Value;
use std::collections::HashMap;
use std::error::Error;
use crate::recipes::github_recipe::GOOSE_RECIPE_GITHUB_REPO_CONFIG_KEY;
// useful for light themes where there is no dicernible colour contrast between
// cursor-selected and cursor-unselected items.
const MULTISELECT_VISIBILITY_HINT: &str = "<";
@@ -221,6 +222,11 @@ pub async fn handle_configure() -> Result<(), Box<dyn Error>> {
"Configure Providers",
"Change provider or update credentials",
)
.item(
"custom_providers",
"Custom Providers",
"Add custom provider with compatible API",
)
.item("add", "Add Extension", "Connect to a new extension")
.item(
"toggle",
@@ -241,6 +247,7 @@ pub async fn handle_configure() -> Result<(), Box<dyn Error>> {
"remove" => remove_extension_dialog(),
"settings" => configure_settings_dialog().await.and(Ok(())),
"providers" => configure_provider_dialog().await.and(Ok(())),
"custom_providers" => configure_custom_provider_dialog(),
_ => unreachable!(),
}
}
@@ -250,10 +257,7 @@ pub async fn handle_configure() -> Result<(), Box<dyn Error>> {
async fn handle_oauth_configuration(
provider_name: &str,
key_name: &str,
) -> Result<(), Box<dyn std::error::Error>> {
use goose::model::ModelConfig;
use goose::providers::create;
) -> Result<(), Box<dyn Error>> {
let _ = cliclack::log::info(format!(
"Configuring {} using OAuth device code flow...",
key_name
@@ -279,8 +283,7 @@ async fn handle_oauth_configuration(
}
}
/// Interactive model search that truncates the list to improve UX
fn interactive_model_search(models: &[String]) -> Result<String, Box<dyn std::error::Error>> {
fn interactive_model_search(models: &[String]) -> Result<String, Box<dyn Error>> {
const MAX_VISIBLE: usize = 30;
let mut query = String::new();
@@ -553,7 +556,7 @@ pub async fn configure_provider_dialog() -> Result<bool, Box<dyn Error>> {
let spin = spinner();
spin.start("Attempting to fetch supported models...");
let models_res = {
let temp_model_config = goose::model::ModelConfig::new(&provider_meta.default_model)?;
let temp_model_config = ModelConfig::new(&provider_meta.default_model)?;
let temp_provider = create(provider_name, temp_model_config)?;
temp_provider.fetch_supported_models().await
};
@@ -585,7 +588,7 @@ pub async fn configure_provider_dialog() -> Result<bool, Box<dyn Error>> {
.map(|val| val == "1" || val.to_lowercase() == "true")
.unwrap_or(false);
let model_config = goose::model::ModelConfig::new(&model)?
let model_config = ModelConfig::new(&model)?
.with_max_tokens(Some(50))
.with_toolshim(toolshim_enabled)
.with_toolshim_model(std::env::var("GOOSE_TOOLSHIM_OLLAMA_MODEL").ok());
@@ -1429,7 +1432,7 @@ pub async fn configure_tool_permissions_dialog() -> Result<(), Box<dyn Error>> {
let model: String = config
.get_param("GOOSE_MODEL")
.expect("No model configured. Please set model first");
let model_config = goose::model::ModelConfig::new(&model)?;
let model_config = ModelConfig::new(&model)?;
// Create the agent
let agent = Agent::new();
@@ -1569,7 +1572,6 @@ fn configure_recipe_dialog() -> Result<(), Box<dyn Error>> {
recipe_repo_input = recipe_repo_input.default_input(&recipe_repo);
}
let input_value: String = recipe_repo_input.interact()?;
// if input is blank, it clears the recipe github repo settings in the config file
if input_value.clone().trim().is_empty() {
config.delete(key_name)?;
} else {
@@ -1767,3 +1769,128 @@ pub async fn handle_openrouter_auth() -> Result<(), Box<dyn Error>> {
Ok(())
}
fn add_provider() -> Result<(), Box<dyn Error>> {
let provider_type = cliclack::select("What type of API is this?")
.item(
"openai_compatible",
"OpenAI Compatible",
"Uses OpenAI API format",
)
.item(
"anthropic_compatible",
"Anthropic Compatible",
"Uses Anthropic API format",
)
.item(
"ollama_compatible",
"Ollama Compatible",
"Uses Ollama API format",
)
.interact()?;
let display_name: String = cliclack::input("What should we call this provider?")
.placeholder("Your Provider Name")
.validate(|input: &String| {
if input.is_empty() {
Err("Please enter a name")
} else {
Ok(())
}
})
.interact()?;
let api_url: String = cliclack::input("Provider API URL:")
.placeholder("https://api.example.com/v1/messages")
.validate(|input: &String| {
if !input.starts_with("http://") && !input.starts_with("https://") {
Err("URL must start with either http:// or https://")
} else {
Ok(())
}
})
.interact()?;
let api_key: String = cliclack::password("API key:").mask('▪').interact()?;
let models_input: String = cliclack::input("Available models (seperate with commas):")
.placeholder("model-a, model-b, model-c")
.validate(|input: &String| {
if input.trim().is_empty() {
Err("Please enter at least one model name")
} else {
Ok(())
}
})
.interact()?;
let models: Vec<String> = models_input
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
let supports_streaming = cliclack::confirm("Does this provider support streaming responses?")
.initial_value(true)
.interact()?;
CustomProviderConfig::create_and_save(
provider_type,
display_name.clone(),
api_url,
api_key,
models,
Some(supports_streaming),
)?;
cliclack::outro(format!("Custom provider added: {}", display_name))?;
Ok(())
}
fn remove_provider() -> Result<(), Box<dyn Error>> {
let custom_providers_dir = goose::config::custom_providers::custom_providers_dir();
let custom_providers = if custom_providers_dir.exists() {
goose::config::custom_providers::load_custom_providers(&custom_providers_dir)?
} else {
Vec::new()
};
if custom_providers.is_empty() {
cliclack::outro("No custom providers added just yet.")?;
return Ok(());
}
let provider_items: Vec<_> = custom_providers
.iter()
.map(|p| (p.name.as_str(), p.display_name.as_str(), "Custom provider"))
.collect();
let selected_id = cliclack::select("Which custom provider would you like to remove?")
.items(&provider_items)
.interact()?;
CustomProviderConfig::remove(selected_id)?;
cliclack::outro(format!("Removed custom provider: {}", selected_id))?;
Ok(())
}
pub fn configure_custom_provider_dialog() -> Result<(), Box<dyn Error>> {
let action = cliclack::select("What would you like to do?")
.item(
"add",
"Add A Custom Provider",
"Add a new OpenAI/Anthropic/Ollama compatible Provider",
)
.item(
"remove",
"Remove Custom Provider",
"Remove an existing custom provider",
)
.interact()?;
match action {
"add" => add_provider(),
"remove" => remove_provider(),
_ => unreachable!(),
}
}