feat(provider): support additive provider key lifecycle management

Add `addToLive` parameter to add_provider so callers can opt out of
writing to the live config (e.g. when duplicating an inactive provider).
Add `originalId` parameter to update_provider to support provider key
renames — the old key is removed from live config before the new one
is written.

Frontend: ProviderForm now exposes provider-key input for openclaw app
type, and EditProviderDialog forwards originalId on save. Deep-link
import passes addToLive=true to preserve existing behavior.
This commit is contained in:
YoVinchen
2026-03-28 01:49:07 +08:00
parent eaf83f4fbe
commit e9ead2841d
13 changed files with 282 additions and 53 deletions
+6 -2
View File
@@ -36,9 +36,11 @@ pub fn add_provider(
state: State<'_, AppState>,
app: String,
provider: Provider,
#[allow(non_snake_case)] addToLive: Option<bool>,
) -> Result<bool, String> {
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
ProviderService::add(state.inner(), app_type, provider).map_err(|e| e.to_string())
ProviderService::add(state.inner(), app_type, provider, addToLive.unwrap_or(true))
.map_err(|e| e.to_string())
}
#[tauri::command]
@@ -46,9 +48,11 @@ pub fn update_provider(
state: State<'_, AppState>,
app: String,
provider: Provider,
#[allow(non_snake_case)] originalId: Option<String>,
) -> Result<bool, String> {
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
ProviderService::update(state.inner(), app_type, provider).map_err(|e| e.to_string())
ProviderService::update(state.inner(), app_type, originalId.as_deref(), provider)
.map_err(|e| e.to_string())
}
#[tauri::command]
+1 -1
View File
@@ -110,7 +110,7 @@ pub fn import_provider_from_deeplink(
let provider_id = provider.id.clone();
// Use ProviderService to add the provider
ProviderService::add(state, app_type.clone(), provider)?;
ProviderService::add(state, app_type.clone(), provider, true)?;
// Add extra endpoints as custom endpoints (skip first one as it's the primary)
for ep in all_endpoints.iter().skip(1) {
+15
View File
@@ -33,6 +33,21 @@ pub(crate) fn sanitize_claude_settings_for_live(settings: &Value) -> Value {
v
}
pub(crate) fn provider_exists_in_live_config(
app_type: &AppType,
provider_id: &str,
) -> Result<bool, AppError> {
match app_type {
AppType::OpenCode => crate::opencode_config::get_providers()
.map(|providers| providers.contains_key(provider_id))
.map_err(Into::into),
AppType::OpenClaw => crate::openclaw_config::get_providers()
.map(|providers| providers.contains_key(provider_id))
.map_err(Into::into),
_ => Ok(false),
}
}
fn json_is_subset(target: &Value, source: &Value) -> bool {
match source {
Value::Object(source_map) => {
+57 -4
View File
@@ -29,7 +29,8 @@ pub use live::{
pub(crate) use live::sanitize_claude_settings_for_live;
pub(crate) use live::{
build_effective_settings_with_common_config, normalize_provider_common_config_for_storage,
strip_common_config_from_live_settings, sync_current_provider_for_app_to_live,
provider_exists_in_live_config, strip_common_config_from_live_settings,
sync_current_provider_for_app_to_live,
write_live_with_common_config,
};
@@ -166,7 +167,12 @@ impl ProviderService {
}
/// Add a new provider
pub fn add(state: &AppState, app_type: AppType, provider: Provider) -> Result<bool, AppError> {
pub fn add(
state: &AppState,
app_type: AppType,
provider: Provider,
add_to_live: bool,
) -> Result<bool, AppError> {
let mut provider = provider;
// Normalize Claude model keys
Self::normalize_provider_if_claude(&app_type, &mut provider);
@@ -176,7 +182,7 @@ impl ProviderService {
// Save to database
state.db.save_provider(app_type.as_str(), &provider)?;
// Additive mode apps (OpenCode, OpenClaw) - always write to live config
// Additive mode apps (OpenCode, OpenClaw): optionally write to live config.
if app_type.is_additive_mode() {
// OMO / OMO Slim providers use exclusive mode and write to dedicated config file.
if matches!(app_type, AppType::OpenCode)
@@ -186,6 +192,9 @@ impl ProviderService {
// Users must explicitly switch/apply an OMO provider to activate it.
return Ok(true);
}
if !add_to_live {
return Ok(true);
}
write_live_with_common_config(state.db.as_ref(), &app_type, &provider)?;
return Ok(true);
}
@@ -207,18 +216,59 @@ impl ProviderService {
pub fn update(
state: &AppState,
app_type: AppType,
original_id: Option<&str>,
provider: Provider,
) -> Result<bool, AppError> {
let mut provider = provider;
let original_id = original_id.unwrap_or(provider.id.as_str()).to_string();
let provider_id_changed = original_id != provider.id;
// Normalize Claude model keys
Self::normalize_provider_if_claude(&app_type, &mut provider);
Self::validate_provider_settings(&app_type, &provider)?;
normalize_provider_common_config_for_storage(state.db.as_ref(), &app_type, &mut provider)?;
if provider_id_changed {
if !app_type.is_additive_mode() {
return Err(AppError::Message(
"Only additive-mode providers support changing provider key".to_string(),
));
}
if provider_exists_in_live_config(&app_type, &original_id)? {
return Err(AppError::Message(
"Provider key cannot be changed after the provider has been added to the app config"
.to_string(),
));
}
if state
.db
.get_provider_by_id(&provider.id, app_type.as_str())?
.is_some()
|| provider_exists_in_live_config(&app_type, &provider.id)?
{
return Err(AppError::Message(format!(
"Provider '{}' already exists in app '{}'",
provider.id,
app_type.as_str()
)));
}
state.db.save_provider(app_type.as_str(), &provider)?;
state.db.delete_provider(app_type.as_str(), &original_id)?;
if crate::settings::get_current_provider(&app_type).as_deref() == Some(&original_id) {
crate::settings::set_current_provider(&app_type, Some(provider.id.as_str()))?;
}
return Ok(true);
}
// Save to database
state.db.save_provider(app_type.as_str(), &provider)?;
// Additive mode apps (OpenCode, OpenClaw) - always update in live config
// Additive mode apps (OpenCode, OpenClaw): only sync to live when the provider
// already exists in live config. Editing a DB-only provider must not auto-add it.
if app_type.is_additive_mode() {
if matches!(app_type, AppType::OpenCode) && provider.category.as_deref() == Some("omo")
{
@@ -250,6 +300,9 @@ impl ProviderService {
}
return Ok(true);
}
if !provider_exists_in_live_config(&app_type, &provider.id)? {
return Ok(true);
}
write_live_with_common_config(state.db.as_ref(), &app_type, &provider)?;
return Ok(true);
}