Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0ed2765
feat: Add backend auto-detection API endpoint
spencrmartin Oct 30, 2025
456acd9
feat: Integrate frontend with backend auto-detection
spencrmartin Oct 30, 2025
1f3f891
feat: Improve onboarding layout design
spencrmartin Oct 30, 2025
0d33d81
fix: Correct route to configure-providers page
spencrmartin Oct 30, 2025
4d3d73b
docs: Add integration documentation and update API client
spencrmartin Oct 30, 2025
9e1cca0
cleanup: Remove documentation and backup files
spencrmartin Oct 30, 2025
fe869a9
fix: Remove TypeScript any type in ApiKeyTester
spencrmartin Oct 30, 2025
c5cfcc9
fix: Apply Rust formatting (cargo fmt)
spencrmartin Oct 30, 2025
c9f2bee
feat: Improve API key validation with format-specific detection
spencrmartin Oct 30, 2025
efb23bc
fix: Prevent unwanted redirects during API key testing
spencrmartin Oct 30, 2025
77926c2
feat: Add backend support for disabling Ollama fallback
spencrmartin Oct 30, 2025
c8e1177
feat: Integrate parallel provider detection from PR #5147
spencrmartin Oct 30, 2025
e87dd52
feat: Add cloud-only provider detection to prevent Ollama fallback
spencrmartin Oct 30, 2025
4bf7f22
fix: Revert to existing endpoint with frontend Ollama filtering
spencrmartin Oct 30, 2025
fbbccd9
fix: Correct imports for cloud provider detection functions
spencrmartin Oct 30, 2025
397e7e3
fix: Add back missing Ollama rejection in Quick Setup
spencrmartin Oct 30, 2025
4d13e0e
feat: Implement hybrid detection - format detection + parallel fallback
spencrmartin Oct 30, 2025
c8b924c
feat: Add frontend format detection and improved error messages
spencrmartin Oct 30, 2025
fab8230
feat: Complete API key detection with smart provider workarounds
spencrmartin Oct 30, 2025
4341a8f
fix: Resolve React key uniqueness warning in ProgressiveMessageList
spencrmartin Oct 30, 2025
0fdc8cc
feat: Upgrade default model selection to Claude 4 (Opus)
spencrmartin Oct 30, 2025
ca67221
fix: Update to correct Claude Sonnet 4 model name
spencrmartin Oct 30, 2025
fe892bc
feat: Add OpenAI support with GPT-4.1 default model
spencrmartin Oct 30, 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
4 changes: 4 additions & 0 deletions crates/goose-server/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ derive_utoipa!(Icon as IconSchema);
super::routes::status::status,
super::routes::status::diagnostics,
super::routes::config_management::backup_config,
super::routes::config_management::detect_provider,
super::routes::config_management::recover_config,
super::routes::config_management::validate_config,
super::routes::config_management::init_config,
Expand Down Expand Up @@ -386,6 +387,9 @@ derive_utoipa!(Icon as IconSchema);
components(schemas(
super::routes::config_management::UpsertConfigQuery,
super::routes::config_management::ConfigKeyQuery,
super::routes::config_management::DetectProviderRequest,
super::routes::config_management::DetectProviderResponse,
super::routes::config_management::DetectProviderError,
super::routes::config_management::ConfigResponse,
super::routes::config_management::ProvidersResponse,
super::routes::config_management::ProviderDetails,
Expand Down
84 changes: 84 additions & 0 deletions crates/goose-server/src/routes/config_management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use goose::config::paths::Paths;
use goose::config::ExtensionEntry;
use goose::config::{Config, ConfigError};
use goose::model::ModelConfig;
use goose::providers::auto_detect::{detect_provider_from_api_key, detect_cloud_provider_from_api_key};
use goose::providers::base::{ProviderMetadata, ProviderType};
use goose::providers::pricing::{
get_all_pricing, get_model_pricing, parse_model_id, refresh_pricing,
Expand Down Expand Up @@ -88,6 +89,23 @@ pub struct UpdateCustomProviderRequest {
pub supports_streaming: Option<bool>,
}

#[derive(Deserialize, ToSchema)]
pub struct DetectProviderRequest {
pub api_key: String,
}

#[derive(Serialize, ToSchema)]
pub struct DetectProviderResponse {
pub provider_name: String,
pub models: Vec<String>,
}

#[derive(Serialize, ToSchema)]
pub struct DetectProviderError {
pub error: String,
pub detected_format: Option<String>,
pub suggestions: Vec<String>,
}
#[utoipa::path(
post,
path = "/config/upsert",
Expand Down Expand Up @@ -516,6 +534,48 @@ pub async fn upsert_permissions(
Ok(Json("Permissions updated successfully".to_string()))
}

#[utoipa::path(
post,
path = "/config/detect-provider",
request_body = DetectProviderRequest,
responses(
(status = 200, description = "Provider detected successfully", body = DetectProviderResponse),
(status = 400, description = "Invalid API key format or key validation failed", body = DetectProviderError),
(status = 500, description = "Internal server error")
)
)]
pub async fn detect_provider(
Json(detect_request): Json<DetectProviderRequest>,
) -> Result<Json<DetectProviderResponse>, StatusCode> {
let api_key = detect_request.api_key.trim();

match detect_provider_from_api_key(api_key).await {
Some((provider_name, models)) => Ok(Json(DetectProviderResponse {
provider_name,
models,
})),
None => Err(StatusCode::NOT_FOUND),
}
}

fn detect_key_format(api_key: &str) -> Option<String> {
let trimmed_key = api_key.trim();

if trimmed_key.starts_with("sk-ant-") {
Some("Anthropic".to_string())
} else if trimmed_key.starts_with("sk-") {
Some("OpenAI".to_string())
} else if trimmed_key.starts_with("AIza") {
Some("Google".to_string())
} else if trimmed_key.starts_with("gsk_") {
Some("Groq".to_string())
} else if trimmed_key.starts_with("xai-") {
Some("xAI".to_string())
} else {
None
}
}

#[utoipa::path(
post,
path = "/config/backup",
Expand Down Expand Up @@ -606,6 +666,28 @@ pub async fn validate_config() -> Result<Json<String>, StatusCode> {
}
}
}
#[utoipa::path(
post,
path = "/config/detect-cloud-provider",
request_body = DetectProviderRequest,
responses(
(status = 200, description = "Cloud provider detected successfully", body = DetectProviderResponse),
(status = 404, description = "No matching cloud provider found"),
(status = 500, description = "Internal server error")
)
)]
pub async fn detect_cloud_provider(
Json(detect_request): Json<DetectProviderRequest>,
) -> Result<Json<DetectProviderResponse>, StatusCode> {
match detect_cloud_provider_from_api_key(&detect_request.api_key).await {
Some((provider_name, models)) => Ok(Json(DetectProviderResponse {
provider_name,
models,
})),
None => Err(StatusCode::NOT_FOUND),
}
}


#[utoipa::path(
post,
Expand Down Expand Up @@ -718,6 +800,8 @@ pub fn routes(state: Arc<AppState>) -> Router {
.route("/config/extensions/{name}", delete(remove_extension))
.route("/config/providers", get(providers))
.route("/config/providers/{name}/models", get(get_provider_models))
.route("/config/detect-provider", post(detect_provider))
.route("/config/detect-cloud-provider", post(detect_cloud_provider))
.route("/config/pricing", post(get_pricing))
.route("/config/init", post(init_config))
.route("/config/backup", post(backup_config))
Expand Down
196 changes: 196 additions & 0 deletions crates/goose/src/providers/auto_detect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
use crate::model::ModelConfig;

/// Detect the provider based on API key format
fn detect_provider_from_key_format(api_key: &str) -> Option<&'static str> {
let trimmed_key = api_key.trim();

// Anthropic keys start with sk-ant-
if trimmed_key.starts_with("sk-ant-") {
return Some("anthropic");
}

// OpenAI keys start with sk- but not sk-ant-
if trimmed_key.starts_with("sk-") && !trimmed_key.starts_with("sk-ant-") {
return Some("openai");
}

// Google keys typically start with AIza
if trimmed_key.starts_with("AIza") {
return Some("google");
}

// Groq keys start with gsk_
if trimmed_key.starts_with("gsk_") {
return Some("groq");
}

// xAI keys start with xai-
if trimmed_key.starts_with("xai-") {
return Some("xai");
}

// OpenRouter keys start with sk-or-
if trimmed_key.starts_with("sk-or-") {
return Some("openrouter");
}

// If we can't detect the format, return None
None
}

/// Test a specific provider with the API key
async fn test_provider(provider_name: &str, api_key: &str) -> Option<(String, Vec<String>)> {
let env_key = match provider_name {
"anthropic" => "ANTHROPIC_API_KEY",
"openai" => "OPENAI_API_KEY",
"google" => "GOOGLE_API_KEY",
"groq" => "GROQ_API_KEY",
"xai" => "XAI_API_KEY",
"openrouter" => "OPENROUTER_API_KEY",
"ollama" => "OLLAMA_API_KEY",
_ => return None,
};

let original_value = std::env::var(env_key).ok();
std::env::set_var(env_key, api_key);

let result = match crate::providers::create(provider_name, ModelConfig::new_or_fail("default")).await {
Ok(provider) => match provider.fetch_supported_models().await {
Ok(Some(models)) => Some((provider_name.to_string(), models)),
_ => None,
},
Err(_) => None,
};

// Restore original value
match original_value {
Some(val) => std::env::set_var(env_key, val),
None => std::env::remove_var(env_key),
}

result
}

pub async fn detect_provider_from_api_key(api_key: &str) -> Option<(String, Vec<String>)> {
// First, try to detect the provider from the key format
if let Some(detected_provider) = detect_provider_from_key_format(api_key) {
// Test the detected provider first
if let Some(result) = test_provider(detected_provider, api_key).await {
return Some(result);
}
}

// If format detection failed or the detected provider didn't work,
// fall back to testing all providers in parallel
let provider_tests = vec![
("anthropic", "ANTHROPIC_API_KEY"),
("openai", "OPENAI_API_KEY"),
("google", "GOOGLE_API_KEY"),
("groq", "GROQ_API_KEY"),
("xai", "XAI_API_KEY"),
("ollama", "OLLAMA_API_KEY"),
];

let tasks: Vec<_> = provider_tests
.into_iter()
.map(|(provider_name, env_key)| {
let api_key = api_key.to_string();
tokio::spawn(async move {
let original_value = std::env::var(env_key).ok();
std::env::set_var(env_key, &api_key);

let result = match crate::providers::create(
provider_name,
ModelConfig::new_or_fail("default"),
)
.await
{
Ok(provider) => match provider.fetch_supported_models().await {
Ok(Some(models)) => Some((provider_name.to_string(), models)),
_ => None,
},
Err(_) => None,
};

match original_value {
Some(val) => std::env::set_var(env_key, val),
None => std::env::remove_var(env_key),
}

result
})
})
.collect();

for task in tasks {
if let Ok(Some(result)) = task.await {
return Some(result);
}
}

None
}

/// Detect provider from API key, testing only cloud providers (no Ollama)
/// This is useful for Quick Setup flows where Ollama fallback is not desired
pub async fn detect_cloud_provider_from_api_key(api_key: &str) -> Option<(String, Vec<String>)> {
// First, try to detect the provider from the key format
if let Some(detected_provider) = detect_provider_from_key_format(api_key) {
// Skip Ollama in cloud-only mode
if detected_provider != "ollama" {
if let Some(result) = test_provider(detected_provider, api_key).await {
return Some(result);
}
}
}

// If format detection failed or the detected provider didn't work,
// fall back to testing cloud providers in parallel (excluding Ollama)
let provider_tests = vec![
("anthropic", "ANTHROPIC_API_KEY"),
("openai", "OPENAI_API_KEY"),
("google", "GOOGLE_API_KEY"),
("groq", "GROQ_API_KEY"),
("xai", "XAI_API_KEY"),
// Ollama excluded for cloud-only detection
];

let tasks: Vec<_> = provider_tests
.into_iter()
.map(|(provider_name, env_key)| {
let api_key = api_key.to_string();
tokio::spawn(async move {
let original_value = std::env::var(env_key).ok();
std::env::set_var(env_key, &api_key);

let result = match crate::providers::create(
provider_name,
ModelConfig::new_or_fail("default"),
)
.await
{
Ok(provider) => match provider.fetch_supported_models().await {
Ok(Some(models)) => Some((provider_name.to_string(), models)),
_ => None,
},
Err(_) => None,
};

match original_value {
Some(val) => std::env::set_var(env_key, val),
None => std::env::remove_var(env_key),
}

result
})
})
.collect();

for task in tasks {
if let Ok(Some(result)) = task.await {
return Some(result);
}
}

None
}
1 change: 1 addition & 0 deletions crates/goose/src/providers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod anthropic;
mod api_client;
pub mod auto_detect;
pub mod azure;
pub mod azureauth;
pub mod base;
Expand Down
Binary file added temporal-service/temporal-service
Binary file not shown.
Loading
Loading