use super::*; use crate::config::declarative_providers; use std::str::FromStr; fn inventory_entry_to_dto(entry: ProviderInventoryEntry) -> ProviderInventoryEntryDto { let stale = ProviderInventoryService::is_stale(&entry); ProviderInventoryEntryDto { provider_id: entry.provider_id, provider_name: entry.provider_name, description: entry.description, default_model: entry.default_model, configured: entry.configured, provider_type: format!("{:?}", entry.provider_type), category: provider_setup_category_to_dto(entry.category), config_keys: entry .config_keys .into_iter() .map(provider_config_key_to_dto) .collect(), setup_steps: entry.setup_steps, supports_refresh: entry.supports_refresh, refreshing: entry.refreshing, models: entry .models .into_iter() .map(|m| ProviderInventoryModelDto { id: m.id, name: m.name, family: m.family, context_limit: m.context_limit, reasoning: m.reasoning, recommended: m.recommended, }) .collect(), last_updated_at: entry.last_updated_at.map(|t| t.to_rfc3339()), last_refresh_attempt_at: entry.last_refresh_attempt_at.map(|t| t.to_rfc3339()), last_refresh_error: entry.last_refresh_error, stale, model_selection_hint: entry.model_selection_hint, } } fn provider_config_key_to_dto(key: crate::providers::base::ConfigKey) -> ProviderConfigKey { ProviderConfigKey { name: key.name, required: key.required, secret: key.secret, default: key.default, oauth_flow: key.oauth_flow, device_code_flow: key.device_code_flow, primary: key.primary, } } const SECRET_MASK_PREFIX_LEN: usize = 4; const SECRET_MASK_SUFFIX_LEN: usize = 3; const SECRET_MASK_FALLBACK: &str = "***"; fn mask_secret_value(value: &str) -> String { let prefix: String = value.chars().take(SECRET_MASK_PREFIX_LEN).collect(); let suffix_chars: Vec = value.chars().rev().take(SECRET_MASK_SUFFIX_LEN).collect(); let suffix: String = suffix_chars.into_iter().rev().collect(); if prefix.is_empty() || suffix.is_empty() || value.chars().count() <= SECRET_MASK_PREFIX_LEN + SECRET_MASK_SUFFIX_LEN { return SECRET_MASK_FALLBACK.to_string(); } format!("{prefix}...{suffix}") } fn config_value_to_string(value: &serde_json::Value) -> Option { match value { serde_json::Value::Null => None, serde_json::Value::String(value) if value.is_empty() => None, serde_json::Value::String(value) => Some(value.clone()), other => serde_json::to_string(other).ok(), } } fn provider_config_field_value( config: &Config, key: &crate::providers::base::ConfigKey, secrets: Option<&HashMap>, ) -> ProviderConfigFieldValueDto { let value = if key.secret { std::env::var(key.name.to_uppercase()).ok().or_else(|| { secrets .and_then(|values| values.get(&key.name)) .and_then(config_value_to_string) }) } else { config .get_param::(&key.name) .ok() .and_then(|value| config_value_to_string(&value)) }; ProviderConfigFieldValueDto { key: key.name.clone(), value: value.as_deref().map(|value| { if key.secret { mask_secret_value(value) } else { value.to_string() } }), is_set: value.is_some(), is_secret: key.secret, required: key.required, } } fn provider_catalog_entry_to_dto( entry: crate::providers::catalog::ProviderCatalogEntry, ) -> ProviderTemplateCatalogEntryDto { ProviderTemplateCatalogEntryDto { provider_id: entry.id, name: entry.name, format: entry.format, api_url: entry.api_url, model_count: entry.model_count, doc_url: entry.doc_url, env_var: entry.env_var, } } fn provider_setup_category_to_dto( category: crate::providers::catalog::ProviderSetupCategory, ) -> ProviderSetupCategoryDto { match category { crate::providers::catalog::ProviderSetupCategory::Agent => ProviderSetupCategoryDto::Agent, crate::providers::catalog::ProviderSetupCategory::Model => ProviderSetupCategoryDto::Model, } } fn provider_setup_method_to_dto( method: crate::providers::catalog::ProviderSetupMethod, ) -> ProviderSetupMethodDto { match method { crate::providers::catalog::ProviderSetupMethod::None => ProviderSetupMethodDto::None, crate::providers::catalog::ProviderSetupMethod::SingleApiKey => { ProviderSetupMethodDto::SingleApiKey } crate::providers::catalog::ProviderSetupMethod::ConfigFields => { ProviderSetupMethodDto::ConfigFields } crate::providers::catalog::ProviderSetupMethod::HostWithOauthFallback => { ProviderSetupMethodDto::HostWithOauthFallback } crate::providers::catalog::ProviderSetupMethod::OauthBrowser => { ProviderSetupMethodDto::OauthBrowser } crate::providers::catalog::ProviderSetupMethod::OauthDeviceCode => { ProviderSetupMethodDto::OauthDeviceCode } crate::providers::catalog::ProviderSetupMethod::CloudCredentials => { ProviderSetupMethodDto::CloudCredentials } crate::providers::catalog::ProviderSetupMethod::Local => ProviderSetupMethodDto::Local, crate::providers::catalog::ProviderSetupMethod::CliAuth => ProviderSetupMethodDto::CliAuth, } } fn provider_setup_group_to_dto( group: crate::providers::catalog::ProviderSetupGroup, ) -> ProviderSetupGroupDto { match group { crate::providers::catalog::ProviderSetupGroup::Default => ProviderSetupGroupDto::Default, crate::providers::catalog::ProviderSetupGroup::Additional => { ProviderSetupGroupDto::Additional } } } fn provider_setup_entry_to_dto( entry: crate::providers::catalog::ProviderSetupCatalogEntry, ) -> ProviderSetupCatalogEntryDto { ProviderSetupCatalogEntryDto { provider_id: entry.provider_id, name: entry.display_name, category: provider_setup_category_to_dto(entry.category), description: entry.description, setup_method: provider_setup_method_to_dto(entry.setup_method), native_connect_query: entry.native_connect_query, fields: entry .fields .into_iter() .map(|field| ProviderSetupFieldDto { key: field.key, label: field.label, secret: field.secret, required: field.required, placeholder: field.placeholder, default_value: field.default_value, }) .collect(), binary_name: entry.binary_name, doc_url: entry.docs_url, group: provider_setup_group_to_dto(entry.group), show_only_when_installed: entry.show_only_when_installed, aliases: entry.aliases, supports_install: entry.setup_capabilities.install, supports_auth: entry.setup_capabilities.auth, supports_auth_status: entry.setup_capabilities.auth_status, } } fn provider_template_to_dto( template: crate::providers::catalog::ProviderTemplate, ) -> ProviderTemplateDto { ProviderTemplateDto { provider_id: template.id, name: template.name, format: template.format, api_url: template.api_url, models: template .models .into_iter() .map(|model| ProviderTemplateModelDto { id: model.id, name: model.name, context_limit: model.context_limit, capabilities: ProviderTemplateCapabilitiesDto { tool_call: model.capabilities.tool_call, reasoning: model.capabilities.reasoning, attachment: model.capabilities.attachment, temperature: model.capabilities.temperature, }, deprecated: model.deprecated, }) .collect(), supports_streaming: template.supports_streaming, env_var: template.env_var, doc_url: template.doc_url, } } fn custom_provider_engine_to_dto(engine: &declarative_providers::ProviderEngine) -> &'static str { match engine { declarative_providers::ProviderEngine::OpenAI => "openai_compatible", declarative_providers::ProviderEngine::Anthropic => "anthropic_compatible", declarative_providers::ProviderEngine::Ollama => "ollama_compatible", } } fn normalize_custom_provider_engine(engine: &str) -> Result { let engine = engine.trim().to_lowercase(); if declarative_providers::ProviderEngine::from_str(&engine).is_err() { return Err(agent_client_protocol::Error::invalid_params() .data(format!("Unsupported custom provider engine: {engine}"))); } match engine.as_str() { "openai" | "openai_compatible" => Ok("openai_compatible".to_string()), "anthropic" | "anthropic_compatible" => Ok("anthropic_compatible".to_string()), "ollama" | "ollama_compatible" => Ok("ollama_compatible".to_string()), _ => unreachable!("provider engine was validated above"), } } fn non_empty_trimmed(value: String, field: &str) -> Result { let value = value.trim().to_string(); if value.is_empty() { return Err( agent_client_protocol::Error::invalid_params().data(format!("{field} cannot be empty")) ); } Ok(value) } fn normalize_optional_string(value: Option) -> Option { value.and_then(|value| { let value = value.trim().to_string(); (!value.is_empty()).then_some(value) }) } fn normalize_custom_provider_upsert( mut provider: CustomProviderUpsertDto, require_api_key: bool, ) -> Result { provider.engine = normalize_custom_provider_engine(&provider.engine)?; provider.display_name = non_empty_trimmed(provider.display_name, "displayName")?; provider.api_url = non_empty_trimmed(provider.api_url, "apiUrl")?; let url = url::Url::parse(&provider.api_url).map_err(|_| { agent_client_protocol::Error::invalid_params().data("apiUrl must be a valid URL") })?; if !matches!(url.scheme(), "http" | "https") { return Err( agent_client_protocol::Error::invalid_params().data("apiUrl must use HTTP or HTTPS") ); } provider.api_key = provider.api_key.and_then(|api_key| { let api_key = api_key.trim().to_string(); (!api_key.is_empty()).then_some(api_key) }); if require_api_key && provider.requires_auth && provider.api_key.is_none() { return Err(agent_client_protocol::Error::invalid_params().data("apiKey cannot be empty")); } provider.models = provider .models .into_iter() .filter_map(|model| { let model = model.trim().to_string(); (!model.is_empty()).then_some(model) }) .collect(); if provider.models.is_empty() { return Err(agent_client_protocol::Error::invalid_params().data("models cannot be empty")); } provider.headers = provider .headers .into_iter() .map(|(key, value)| { let key = key.trim().to_string(); let value = value.trim().to_string(); if key.is_empty() { return Ok(None); } reqwest::header::HeaderName::from_bytes(key.as_bytes()).map_err(|_| { agent_client_protocol::Error::invalid_params() .data(format!("Invalid header name: {key}")) })?; reqwest::header::HeaderValue::from_str(&value).map_err(|_| { agent_client_protocol::Error::invalid_params() .data(format!("Invalid header value for: {key}")) })?; Ok(Some((key, value))) }) .collect::, agent_client_protocol::Error>>()? .into_iter() .flatten() .collect(); provider.catalog_provider_id = normalize_optional_string(provider.catalog_provider_id); provider.base_path = normalize_optional_string(provider.base_path); Ok(provider) } fn custom_provider_headers(headers: HashMap) -> Option> { (!headers.is_empty()).then_some(headers) } fn load_declarative_provider_for_client( provider_id: &str, ) -> Result { declarative_providers::load_provider(provider_id).map_err(|error| { if error.to_string().contains("Provider not found") { agent_client_protocol::Error::invalid_params() .data(format!("Unknown provider: {provider_id}")) } else if error.to_string().contains("Invalid provider id") { agent_client_protocol::Error::invalid_params().data(error.to_string()) } else { agent_client_protocol::Error::internal_error().data(error.to_string()) } }) } fn custom_provider_config_to_dto( config: &declarative_providers::DeclarativeProviderConfig, ) -> CustomProviderConfigDto { let api_key_env = normalize_optional_string(Some(config.api_key_env.clone())); let api_key_set = api_key_env .as_ref() .map(|key| { Config::global() .get_secret::(key) .is_ok() }) .unwrap_or(false); CustomProviderConfigDto { provider_id: config.name.clone(), engine: custom_provider_engine_to_dto(&config.engine).to_string(), display_name: config.display_name.clone(), api_url: config.base_url.clone(), models: config .models .iter() .map(|model| model.name.clone()) .collect(), supports_streaming: config.supports_streaming, headers: config.headers.clone().unwrap_or_default(), requires_auth: config.requires_auth, catalog_provider_id: config.catalog_provider_id.clone(), base_path: config.base_path.clone(), api_key_env, api_key_set, preserves_thinking: config.preserves_thinking, } } fn refresh_skip_reason_to_dto(reason: RefreshSkipReason) -> RefreshProviderInventorySkipReasonDto { match reason { RefreshSkipReason::UnknownProvider => { RefreshProviderInventorySkipReasonDto::UnknownProvider } RefreshSkipReason::NotConfigured => RefreshProviderInventorySkipReasonDto::NotConfigured, RefreshSkipReason::DoesNotSupportRefresh => { RefreshProviderInventorySkipReasonDto::DoesNotSupportRefresh } RefreshSkipReason::AlreadyRefreshing => { RefreshProviderInventorySkipReasonDto::AlreadyRefreshing } } } fn refresh_plan_to_response(refresh_plan: RefreshPlan) -> RefreshProviderInventoryResponse { RefreshProviderInventoryResponse { started: refresh_plan.started, skipped: refresh_plan .skipped .into_iter() .map(|entry| RefreshProviderInventorySkipDto { provider_id: entry.provider_id, reason: refresh_skip_reason_to_dto(entry.reason), }) .collect(), } } impl GooseAcpAgent { pub(super) async fn on_list_providers( &self, req: ListProvidersRequest, ) -> Result { let entries = self .provider_inventory .entries(&req.provider_ids) .await .internal_err()?; Ok(ListProvidersResponse { entries: entries.into_iter().map(inventory_entry_to_dto).collect(), }) } pub(super) async fn on_list_provider_catalog( &self, req: ProviderCatalogListRequest, ) -> Result { let formats = match req.format { Some(format) => vec![format .parse::() .map_err(|error| agent_client_protocol::Error::invalid_params().data(error))?], None => vec![ crate::providers::catalog::ProviderFormat::OpenAI, crate::providers::catalog::ProviderFormat::Anthropic, crate::providers::catalog::ProviderFormat::Ollama, ], }; let mut providers = Vec::new(); for format in formats { providers.extend( crate::providers::catalog::get_providers_by_format(format) .await .into_iter() .map(provider_catalog_entry_to_dto), ); } providers.sort_by(|a, b| { a.name .cmp(&b.name) .then_with(|| a.provider_id.cmp(&b.provider_id)) }); Ok(ProviderCatalogListResponse { providers }) } pub(super) async fn on_list_provider_setup_catalog( &self, _req: ProviderSetupCatalogListRequest, ) -> Result { let providers = crate::providers::catalog::get_setup_catalog_entries() .await .into_iter() .map(provider_setup_entry_to_dto) .collect(); Ok(ProviderSetupCatalogListResponse { providers }) } pub(super) async fn on_get_provider_catalog_template( &self, req: ProviderCatalogTemplateRequest, ) -> Result { let template = crate::providers::catalog::get_provider_template(&req.provider_id) .ok_or_else(|| { agent_client_protocol::Error::invalid_params() .data(format!("Unknown catalog provider: {}", req.provider_id)) })?; Ok(ProviderCatalogTemplateResponse { template: provider_template_to_dto(template), }) } pub(super) async fn on_create_custom_provider( &self, req: CustomProviderCreateRequest, ) -> Result { let provider = normalize_custom_provider_upsert(req.provider, true)?; let config = declarative_providers::create_custom_provider( declarative_providers::CreateCustomProviderParams { engine: provider.engine, display_name: provider.display_name, api_url: provider.api_url, api_key: provider.api_key, models: provider.models, supports_streaming: provider.supports_streaming, headers: custom_provider_headers(provider.headers), requires_auth: provider.requires_auth, catalog_provider_id: provider.catalog_provider_id, base_path: provider.base_path, preserves_thinking: provider.preserves_thinking, }, ) .internal_err_ctx("Failed to create custom provider")?; Config::global().invalidate_secrets_cache(); crate::providers::refresh_custom_providers() .await .internal_err_ctx("Failed to refresh custom providers")?; let provider_id = config.name; let provider_ids = [provider_id.clone()]; let status = Self::provider_config_status(provider_id.clone()).await; let refresh = self.start_provider_inventory_refresh(&provider_ids).await?; Ok(CustomProviderCreateResponse { provider_id, status, refresh, }) } pub(super) async fn on_read_custom_provider( &self, req: CustomProviderReadRequest, ) -> Result { let loaded = load_declarative_provider_for_client(&req.provider_id)?; let status = Self::provider_config_status(req.provider_id).await; Ok(CustomProviderReadResponse { provider: custom_provider_config_to_dto(&loaded.config), editable: loaded.is_editable, status, }) } pub(super) async fn on_update_custom_provider( &self, req: CustomProviderUpdateRequest, ) -> Result { let loaded = load_declarative_provider_for_client(&req.provider_id)?; if !loaded.is_editable { return Err(agent_client_protocol::Error::invalid_params() .data(format!("Provider is not editable: {}", req.provider_id))); } let provider = normalize_custom_provider_upsert(req.provider, false)?; if provider.requires_auth && provider.api_key.is_none() { let api_key_env = if loaded.config.api_key_env.is_empty() { declarative_providers::generate_api_key_name(&req.provider_id) } else { loaded.config.api_key_env.clone() }; if Config::global().get_secret::(&api_key_env).is_err() { return Err(agent_client_protocol::Error::invalid_params() .data("apiKey is required when auth is enabled and no secret is stored")); } } declarative_providers::update_custom_provider( declarative_providers::UpdateCustomProviderParams { id: req.provider_id.clone(), engine: provider.engine, display_name: provider.display_name, api_url: provider.api_url, api_key: provider.api_key, models: provider.models, supports_streaming: provider.supports_streaming, headers: Some(provider.headers), requires_auth: provider.requires_auth, catalog_provider_id: provider.catalog_provider_id, base_path: provider.base_path, preserves_thinking: provider.preserves_thinking, }, ) .internal_err_ctx("Failed to update custom provider")?; Config::global().invalidate_secrets_cache(); crate::providers::refresh_custom_providers() .await .internal_err_ctx("Failed to refresh custom providers")?; let provider_ids = [req.provider_id.clone()]; let status = Self::provider_config_status(req.provider_id.clone()).await; let refresh = self.start_provider_inventory_refresh(&provider_ids).await?; Ok(CustomProviderUpdateResponse { provider_id: req.provider_id, status, refresh, }) } pub(super) async fn on_delete_custom_provider( &self, req: CustomProviderDeleteRequest, ) -> Result { let loaded = load_declarative_provider_for_client(&req.provider_id)?; if !loaded.is_editable { return Err(agent_client_protocol::Error::invalid_params() .data(format!("Provider is not editable: {}", req.provider_id))); } if Config::global() .get_param::("GOOSE_PROVIDER") .ok() .as_deref() == Some(req.provider_id.as_str()) { return Err(agent_client_protocol::Error::invalid_params().data(format!( "Cannot delete active provider: {}", req.provider_id ))); } declarative_providers::remove_custom_provider(&req.provider_id) .internal_err_ctx("Failed to delete custom provider")?; Config::global().invalidate_secrets_cache(); crate::providers::refresh_custom_providers() .await .internal_err_ctx("Failed to refresh custom providers")?; Ok(CustomProviderDeleteResponse { provider_id: req.provider_id, refresh: RefreshProviderInventoryResponse { started: Vec::new(), skipped: Vec::new(), }, }) } pub(super) async fn provider_config_status(provider_id: String) -> ProviderConfigStatusDto { let is_configured = match crate::providers::get_from_registry(&provider_id).await { Ok(entry) => { match tokio::task::spawn_blocking(move || entry.inventory_configured()).await { Ok(is_configured) => is_configured, Err(error) => { warn!( provider = %provider_id, error = %error, "provider config status check failed" ); false } } } Err(_) => false, }; ProviderConfigStatusDto { provider_id, is_configured, } } pub(super) async fn provider_config_statuses( provider_ids: &[String], ) -> Vec { let mut ids = if provider_ids.is_empty() { crate::providers::providers() .await .into_iter() .map(|(metadata, _)| metadata.name) .collect::>() } else { provider_ids.to_vec() }; ids.sort(); ids.dedup(); let mut statuses = stream::iter(ids) .map(Self::provider_config_status) .buffer_unordered(PROVIDER_CONFIG_STATUS_CHECK_CONCURRENCY) .collect::>() .await; statuses.sort_by(|a, b| a.provider_id.cmp(&b.provider_id)); statuses } pub(super) fn spawn_provider_inventory_refresh_jobs(&self, refresh_plan: &RefreshJobPlan) { for refresh_job in refresh_plan.started.iter().cloned() { let provider_inventory = self.provider_inventory.clone(); let provider_factory = Arc::clone(&self.provider_factory); let provider_id = refresh_job.provider_id.clone(); let identity = refresh_job.identity.clone(); tokio::spawn(async move { let mut refresh_guard = provider_inventory.refresh_guard(&identity); let provider_result = AssertUnwindSafe(async { let metadata = crate::providers::get_from_registry(&provider_id).await?; let model_config = crate::model::ModelConfig::new(&metadata.metadata().default_model)? .with_canonical_limits(&provider_id); provider_factory(provider_id.clone(), model_config, Vec::new(), None).await }) .catch_unwind() .await; let fetch_result: Result> = match provider_result { Ok(Ok(provider)) => { match ensure_refresh_identity_current(&provider_id, &identity).await { Ok(()) => match AssertUnwindSafe(provider.fetch_recommended_models()) .catch_unwind() .await { Ok(Ok(models)) => Ok(models), Ok(Err(error)) => Err(anyhow::anyhow!(error.to_string())), Err(_) => { Err(anyhow::anyhow!("provider inventory refresh task panicked")) } }, Err(error) => Err(error), } } Ok(Err(error)) => Err(error), Err(_) => Err(anyhow::anyhow!("provider inventory refresh task panicked")), }; match fetch_result { Ok(models) => match provider_inventory .store_refreshed_models_for_identity(&identity, &models) .await { Ok(()) => refresh_guard.complete(), Err(error) => warn!( provider = %provider_id, error = %error, "failed to store refreshed provider inventory" ), }, Err(error) => { let error_message = error.to_string(); match provider_inventory .store_refresh_error_for_identity(&identity, error_message.clone()) .await { Ok(()) => refresh_guard.complete(), Err(store_error) => warn!( provider = %provider_id, error = %store_error, refresh_error = %error_message, "failed to store provider inventory refresh error" ), } warn!(provider = %provider_id, error = %error_message, "provider inventory refresh failed"); } } }); } } pub(super) async fn start_provider_inventory_refresh( &self, provider_ids: &[String], ) -> Result { let refresh_job_plan = self .provider_inventory .plan_refresh_jobs(provider_ids) .await .internal_err()?; self.spawn_provider_inventory_refresh_jobs(&refresh_job_plan); Ok(refresh_plan_to_response( refresh_job_plan.into_public_plan(), )) } pub(super) async fn on_refresh_provider_inventory( &self, req: RefreshProviderInventoryRequest, ) -> Result { Config::global().invalidate_secrets_cache(); self.start_provider_inventory_refresh(&req.provider_ids) .await } pub(super) async fn on_read_provider_config( &self, req: ProviderConfigReadRequest, ) -> Result { let entry = crate::providers::get_from_registry(&req.provider_id) .await .invalid_params_err_ctx("Unknown provider")?; let config = Config::global(); let config_keys = &entry.metadata().config_keys; let secrets = if config_keys.iter().any(|key| key.secret) { Some(config.all_secrets().internal_err()?) } else { None }; Ok(ProviderConfigReadResponse { fields: config_keys .iter() .map(|key| provider_config_field_value(config, key, secrets.as_ref())) .collect(), }) } pub(super) async fn on_provider_config_status( &self, req: ProviderConfigStatusRequest, ) -> Result { Ok(ProviderConfigStatusResponse { statuses: Self::provider_config_statuses(&req.provider_ids).await, }) } pub(super) async fn on_save_provider_config( &self, req: ProviderConfigSaveRequest, ) -> Result { let entry = crate::providers::get_from_registry(&req.provider_id) .await .invalid_params_err_ctx("Unknown provider")?; let metadata = entry.metadata().clone(); let config = Config::global(); let mut config_updates = Vec::new(); let mut secret_updates = Vec::new(); for field in &req.fields { let Some(config_key) = metadata .config_keys .iter() .find(|config_key| config_key.name == field.key) else { return Err(agent_client_protocol::Error::invalid_params() .data(format!("Unsupported provider config field: {}", field.key))); }; let value = field.value.trim(); if value.is_empty() { return Err(agent_client_protocol::Error::invalid_params().data(format!( "Provider config field cannot be empty: {}", field.key ))); } if config_key.secret { secret_updates.push(( config_key.name.clone(), serde_json::Value::String(value.to_string()), )); } else { config_updates.push((config_key.name.clone(), value.to_string())); } } for (key, value) in config_updates { config .set_param(&key, &value) .internal_err_ctx("Failed to save provider config field")?; } config .set_secret_values(&secret_updates) .internal_err_ctx("Failed to save provider secret fields")?; let provider_ids = [req.provider_id.clone()]; let status = Self::provider_config_status(req.provider_id.clone()).await; let refresh = self.start_provider_inventory_refresh(&provider_ids).await?; Ok(ProviderConfigChangeResponse { status, refresh }) } pub(super) async fn on_delete_provider_config( &self, req: ProviderConfigDeleteRequest, ) -> Result { let entry = crate::providers::get_from_registry(&req.provider_id) .await .invalid_params_err_ctx("Unknown provider")?; let metadata = entry.metadata().clone(); let config = Config::global(); let mut secret_keys = Vec::new(); for config_key in &metadata.config_keys { if config_key.secret { secret_keys.push(config_key.name.clone()); } else { config .delete(&config_key.name) .internal_err_ctx("Failed to delete provider config field")?; } } config .delete_secret_values(&secret_keys) .internal_err_ctx("Failed to delete provider secret fields")?; crate::providers::cleanup_provider(&req.provider_id) .await .internal_err_ctx("Failed to clean up provider state")?; let provider_ids = [req.provider_id.clone()]; let status = Self::provider_config_status(req.provider_id.clone()).await; let refresh = self.start_provider_inventory_refresh(&provider_ids).await?; Ok(ProviderConfigChangeResponse { status, refresh }) } pub(super) async fn on_authenticate_provider_config( &self, req: ProviderConfigAuthenticateRequest, ) -> Result { let entry = crate::providers::get_from_registry(&req.provider_id) .await .invalid_params_err_ctx("Unknown provider")?; let metadata = entry.metadata().clone(); if !metadata.config_keys.iter().any(|key| key.oauth_flow) { return Err(agent_client_protocol::Error::invalid_params().data(format!( "Provider does not support native authentication: {}", req.provider_id ))); } let provider = entry .create_with_default_model(Vec::new()) .await .internal_err_ctx("Failed to initialize provider")?; provider .configure_oauth() .await .internal_err_ctx("Failed to authenticate provider")?; Config::global().invalidate_secrets_cache(); let provider_ids = [req.provider_id.clone()]; let status = Self::provider_config_status(req.provider_id.clone()).await; let refresh = self.start_provider_inventory_refresh(&provider_ids).await?; Ok(ProviderConfigChangeResponse { status, refresh }) } }