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:
@@ -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()))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user