Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
dc4caa0
feat: allow custom provider setup for compatible OpenAI and Anthropic…
Developerayo Aug 4, 2025
aa22f0d
check-box to pass `notrequired` as `apikey` for local models
Developerayo Aug 4, 2025
52cf377
Merge branch 'main' into fix/compatible-providers
Developerayo Aug 4, 2025
ffdcb90
fix custom placeholder
Developerayo Aug 4, 2025
6daff6e
chore: migrate to json engine approach
Developerayo Aug 5, 2025
16f26f8
Merge branch 'main' into fix/compatible-providers
Developerayo Aug 5, 2025
3eed394
Move hardcoded LLM prompts to template files
Aug 7, 2025
fdabe90
Merge branch 'main' of https://github.com/block/goose
Aug 8, 2025
7d44e5e
add bool `supports_streaming` to custom providers & clean up
Developerayo Aug 9, 2025
a8b8901
Merge main into fix/compatible-providers branch
Developerayo Aug 9, 2025
8284d2b
clean-up
Developerayo Aug 10, 2025
053c737
`cfmt`
Developerayo Aug 10, 2025
bc35a8e
lock
Developerayo Aug 10, 2025
58b00c1
Merge branch 'main' of https://github.com/block/goose
Aug 12, 2025
a444998
Merge remote-tracking branch 'origin/main' into fix/compatible-providers
Developerayo Aug 12, 2025
9a5c0a7
Merge branch 'main' of https://github.com/block/goose
Aug 12, 2025
d6c00d5
Merge remote-tracking branch 'origin/main' into fix/compatible-providers
Developerayo Aug 13, 2025
652ee90
Merge branch 'main' of https://github.com/block/goose
Aug 14, 2025
ae2f65b
Update
Aug 14, 2025
e2a91a9
Merge branch 'main' of https://github.com/block/goose
Aug 14, 2025
990ea7f
Merge branch 'main' into custom-providers-update
Aug 14, 2025
96e0525
Split
Aug 14, 2025
ba2c969
Merge branch 'main' into custom-providers-update
Aug 16, 2025
f19534d
Revert a bit
Aug 16, 2025
62ed823
One thing
Aug 19, 2025
6790134
What zane said
Aug 19, 2025
00b48f3
MErge
Aug 19, 2025
5a6deb3
How was this missing?!?
Aug 19, 2025
ab6cb21
HUh?
Aug 19, 2025
d961719
Merge branch 'main' into custom-providers-update
Aug 19, 2025
5cecc67
guess we needed client-fetch after all
zanesq Aug 19, 2025
4a09c0f
nevermind dont need client-fetch
zanesq Aug 19, 2025
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
151 changes: 139 additions & 12 deletions crates/goose-cli/src/commands/configure.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -7,22 +8,22 @@ 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::{
Config, ConfigError, ExperimentManager, ExtensionConfigManager, ExtensionEntry,
PermissionManager,
};
use goose::conversation::message::Message;
use goose::model::ModelConfig;
use goose::providers::{create, providers};
use rmcp::model::{Tool, ToolAnnotations};
use rmcp::object;
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 = "<";
Expand Down Expand Up @@ -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",
Expand All @@ -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!(),
}
}
Expand All @@ -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
Expand All @@ -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();

Expand Down Expand Up @@ -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
};
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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!(),
}
}
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 @@ -402,6 +404,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