Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/goose-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ shlex = "1.3.0"
async-trait = "0.1.86"
base64 = "0.22.1"
regex = "1.11.1"
uuid = { version = "1.11", features = ["v4"] }
nix = { version = "0.30.1", features = ["process", "signal"] }
tar = "0.4"
# Web server dependencies
Expand Down
132 changes: 132 additions & 0 deletions crates/goose-cli/src/commands/configure.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
use cliclack::spinner;
use console::style;
use etcetera::{choose_app_strategy, AppStrategy};
use goose::agents::extension::ToolInfo;
use goose::agents::extension_manager::get_parameter_names;
use goose::agents::platform_tools::{
PLATFORM_LIST_RESOURCES_TOOL_NAME, PLATFORM_READ_RESOURCE_TOOL_NAME,
};
use goose::agents::Agent;
use goose::agents::{extension::Envs, ExtensionConfig};
use goose::config::base::APP_STRATEGY;
use goose::config::custom_providers::CustomProviderConfig;
use goose::config::extensions::name_to_key;
use goose::config::permission::PermissionLevel;
use goose::config::{
Expand Down Expand Up @@ -221,6 +224,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",
Expand All @@ -241,6 +249,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!(),
}
}
Expand Down Expand Up @@ -1780,3 +1789,126 @@ pub async fn handle_openrouter_auth() -> Result<(), Box<dyn Error>> {

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" => {
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("Inputed 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))?;
}
"remove" => {
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))?;
}
_ => unreachable!(),
}

Ok(())
}
1 change: 1 addition & 0 deletions crates/goose-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ serde_yaml = "0.9.34"
utoipa = { version = "4.1", features = ["axum_extras", "chrono"] }
reqwest = { version = "0.12.9", features = ["json", "rustls-tls", "blocking", "multipart"], default-features = false }
tokio-util = "0.7.15"
uuid = { version = "1.11", features = ["v4"] }

[[bin]]
name = "goosed"
Expand Down
3 changes: 3 additions & 0 deletions crates/goose-server/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,8 @@ impl<'__s> ToSchema<'__s> for AnnotatedSchema {
super::routes::config_management::read_all_config,
super::routes::config_management::providers,
super::routes::config_management::upsert_permissions,
super::routes::config_management::create_custom_provider,
super::routes::config_management::remove_custom_provider,
super::routes::agent::get_tools,
super::routes::agent::add_sub_recipes,
super::routes::agent::extend_prompt,
Expand Down Expand Up @@ -401,6 +403,7 @@ impl<'__s> ToSchema<'__s> for AnnotatedSchema {
super::routes::config_management::ExtensionQuery,
super::routes::config_management::ToolPermission,
super::routes::config_management::UpsertPermissionsQuery,
super::routes::config_management::CreateCustomProviderRequest,
super::routes::reply::PermissionConfirmationRequest,
super::routes::context::ContextManageRequest,
super::routes::context::ContextManageResponse,
Expand Down
4 changes: 2 additions & 2 deletions crates/goose-server/src/routes/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ async fn update_agent_provider(
let agent = state
.get_agent()
.await
.map_err(|_| StatusCode::PRECONDITION_FAILED)?;
.map_err(|_e| StatusCode::PRECONDITION_FAILED)?;

let config = Config::global();
let model = match payload
Expand All @@ -210,7 +210,7 @@ async fn update_agent_provider(
agent
.update_provider(new_provider)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
.map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;

Ok(StatusCode::OK)
}
Expand Down
Loading
Loading