Structured per-provider config block, non-destructive provider switching (#8977)

Signed-off-by: Douwe Osinga <douwe@squareup.com>
Signed-off-by: Aaron Yourk <ayourk@gmail.com>
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
ayourk
2026-05-18 18:22:50 +00:00
committed by GitHub
parent 60c482de25
commit badb97a01e
26 changed files with 1014 additions and 95 deletions
@@ -69,6 +69,8 @@ pub struct ProviderDetails {
pub metadata: ProviderMetadata,
pub is_configured: bool,
pub provider_type: ProviderType,
#[serde(skip_serializing_if = "Option::is_none")]
pub saved_model: Option<String>,
}
#[derive(Serialize, ToSchema)]
@@ -171,6 +173,29 @@ pub async fn upsert_config(
Json(query): Json<UpsertConfigQuery>,
) -> Result<Json<Value>, ErrorResponse> {
let config = Config::global();
// Intercept legacy keys to write structured provider config
if query.key == "GOOSE_PROVIDER" {
if let Some(name) = query.value.as_str() {
// Preserve the target provider's saved model rather than copying
// the current active provider's model into the new entry.
let model = goose::config::get_provider_entry(config, name)
.map(|e| e.model)
.or_else(|| config.get_goose_model().ok())
.unwrap_or_default();
goose::config::set_active_provider(config, name, &model)?;
return Ok(Json(Value::String(format!("Upserted key {}", query.key))));
}
}
if query.key == "GOOSE_MODEL" {
if let Some(model) = query.value.as_str() {
if let Ok(provider) = config.get_goose_provider() {
goose::config::set_active_provider(config, &provider, model)?;
return Ok(Json(Value::String(format!("Upserted key {}", query.key))));
}
}
}
config.set(&query.key, &query.value, query.is_secret)?;
Ok(Json(Value::String(format!("Upserted key {}", query.key))))
}
@@ -192,6 +217,14 @@ pub async fn remove_config(
if query.is_secret {
config.delete_secret(&query.key)?;
} else if query.key == "GOOSE_PROVIDER" || query.key == "active_provider" {
config.delete("active_provider")?;
config.delete("GOOSE_PROVIDER")?;
} else if query.key == "GOOSE_MODEL" {
if let Ok(provider) = config.get_goose_provider() {
goose::config::set_active_provider(config, &provider, "")?;
}
config.delete("GOOSE_MODEL")?;
} else {
config.delete(&query.key)?;
}
@@ -236,6 +269,20 @@ pub async fn read_config(
) -> Result<Json<ConfigValueResponse>, ErrorResponse> {
let config = Config::global();
// Intercept legacy keys to return structured provider config
if query.key == "GOOSE_PROVIDER" || query.key == "active_provider" {
if let Ok(val) = config.get_goose_provider() {
return Ok(Json(ConfigValueResponse::Value(Value::String(val))));
}
return Ok(Json(ConfigValueResponse::Value(Value::Null)));
}
if query.key == "GOOSE_MODEL" {
if let Ok(val) = config.get_goose_model() {
return Ok(Json(ConfigValueResponse::Value(Value::String(val))));
}
return Ok(Json(ConfigValueResponse::Value(Value::Null)));
}
let response_value = match config.get(&query.key, query.is_secret) {
Ok(value) => {
if query.is_secret {
@@ -341,17 +388,22 @@ pub async fn read_all_config() -> Result<Json<ConfigResponse>, ErrorResponse> {
)
)]
pub async fn providers() -> Result<Json<Vec<ProviderDetails>>, ErrorResponse> {
let config = Config::global();
let providers = get_providers().await;
let providers_response: Vec<ProviderDetails> = providers
.into_iter()
.map(|(metadata, provider_type)| {
let is_configured = check_provider_configured(&metadata, provider_type);
let saved_model = goose::config::get_provider_entry(config, &metadata.name)
.map(|e| e.model)
.filter(|m| !m.is_empty());
ProviderDetails {
name: metadata.name.clone(),
metadata,
is_configured,
provider_type,
saved_model,
}
})
.collect();
@@ -729,9 +781,7 @@ pub async fn set_config_provider(
.await
.and_then(|_| {
let config = Config::global();
config
.set_goose_provider(provider.clone())
.and_then(|_| config.set_goose_model(model.clone()))
goose::config::set_active_provider(config, &provider, &model)
.map_err(|e| anyhow::anyhow!(e))
})
.map_err(|err| {
@@ -839,9 +889,28 @@ pub async fn configure_provider_oauth(
})?;
// Mark the provider as configured after successful OAuth
let configured_marker = format!("{}_configured", provider_name);
let config = goose::config::Config::global();
config.set_param(&configured_marker, true)?;
if let Some(mut entry) = goose::config::get_provider_entry(config, &provider_name) {
entry.configured = true;
goose::config::set_provider_entry(config, &provider_name, &entry)?;
} else {
let model = if goose::config::get_active_provider(config).as_deref()
== Some(provider_name.as_str())
{
config.get_goose_model().unwrap_or_default()
} else {
String::new()
};
goose::config::set_provider_entry(
config,
&provider_name,
&goose::config::ProviderEntry {
enabled: true,
model,
configured: true,
},
)?;
}
Ok(Json("OAuth configuration completed".to_string()))
}
+21 -4
View File
@@ -118,20 +118,37 @@ pub fn check_provider_configured(metadata: &ProviderMetadata, provider_type: Pro
}
}
// Special case: OAuth providers - check for configured marker
// OAuth providers: trust the structured configured flag or legacy marker
let has_oauth_key = metadata.config_keys.iter().any(|key| key.oauth_flow);
if has_oauth_key {
if let Some(entry) = goose::config::get_provider_entry(config, &metadata.name) {
if entry.configured {
return true;
}
}
let configured_marker = format!("{}_configured", metadata.name);
if matches!(config.get_param::<bool>(&configured_marker), Ok(true)) {
return true;
}
}
// Special case: Zero-config providers (no config keys)
// Zero-config providers (no config keys): trust structured flag or active status
if metadata.config_keys.is_empty() {
// Check if the provider has been explicitly configured via the UI
if let Some(entry) = goose::config::get_provider_entry(config, &metadata.name) {
if entry.configured {
return true;
}
}
let configured_marker = format!("{}_configured", metadata.name);
return config.get_param::<bool>(&configured_marker).is_ok();
if config.get_param::<bool>(&configured_marker).is_ok() {
return true;
}
if let Ok(current) = config.get_goose_provider() {
if current == metadata.name {
return true;
}
}
return false;
}
// Get all required keys