From 86cedad794895d302f9da29210f7024b7a4d6361 Mon Sep 17 00:00:00 2001 From: Fini Date: Sun, 31 May 2026 14:20:25 +0800 Subject: [PATCH] fix(ai): dedupe built-in providers --- crates/op-editor-core/src/agent_settings.rs | 34 +++++++++-- .../src/tests_agent_settings_draft.rs | 12 ++++ crates/op-host-desktop/src/settings_io.rs | 60 ++++++++++++++++++- 3 files changed, 101 insertions(+), 5 deletions(-) diff --git a/crates/op-editor-core/src/agent_settings.rs b/crates/op-editor-core/src/agent_settings.rs index 85cde92e..112a5ec2 100644 --- a/crates/op-editor-core/src/agent_settings.rs +++ b/crates/op-editor-core/src/agent_settings.rs @@ -182,6 +182,21 @@ impl BuiltinAgentConfig { pub fn ready(&self) -> bool { self.enabled && !self.api_key.trim().is_empty() && !self.model.trim().is_empty() } + + pub fn matches_config( + &self, + display_name: &str, + api_key: &str, + model: &str, + kind: BuiltinAgentKind, + base_url: &str, + ) -> bool { + self.kind == kind + && self.display_name.trim() == display_name.trim() + && self.api_key.trim() == api_key.trim() + && self.model.trim() == model.trim() + && self.base_url.trim().trim_end_matches('/') == base_url.trim().trim_end_matches('/') + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -474,15 +489,26 @@ impl AgentSettings { kind: BuiltinAgentKind, base_url: impl Into, ) -> String { + let display_name = display_name.into(); + let api_key = api_key.into(); + let model = model.into(); + let base_url = base_url.into(); + if let Some(existing) = self + .builtin_agents + .iter() + .find(|agent| agent.matches_config(&display_name, &api_key, &model, kind, &base_url)) + { + return existing.id.clone(); + } let id = format!("builtin-{}", self.next_builtin_agent_id.max(1)); self.next_builtin_agent_id = self.next_builtin_agent_id.max(1).saturating_add(1); self.builtin_agents.push(BuiltinAgentConfig { id: id.clone(), - display_name: display_name.into(), + display_name, kind, - api_key: api_key.into(), - model: model.into(), - base_url: base_url.into(), + api_key, + model, + base_url, enabled: true, }); id diff --git a/crates/op-editor-core/src/tests_agent_settings_draft.rs b/crates/op-editor-core/src/tests_agent_settings_draft.rs index 6ec667b6..461f1281 100644 --- a/crates/op-editor-core/src/tests_agent_settings_draft.rs +++ b/crates/op-editor-core/src/tests_agent_settings_draft.rs @@ -1,5 +1,17 @@ use crate::agent_settings::AgentSettings; +#[test] +fn duplicate_builtin_agent_config_reuses_existing_provider() { + let mut s = AgentSettings::default(); + + let first = s.add_builtin_agent_with_defaults("MINIMAX", "sk-test", "MiniMax-M2.7"); + let second = s.add_builtin_agent_with_defaults("MINIMAX", "sk-test", "MiniMax-M2.7"); + + assert_eq!(second, first); + assert_eq!(s.builtin_agents.len(), 1); + assert_eq!(s.next_builtin_agent_id, 2); +} + #[test] fn builtin_agent_draft_does_not_persist_until_save() { let mut s = AgentSettings::default(); diff --git a/crates/op-host-desktop/src/settings_io.rs b/crates/op-host-desktop/src/settings_io.rs index 131146f3..590c85f1 100644 --- a/crates/op-host-desktop/src/settings_io.rs +++ b/crates/op-host-desktop/src/settings_io.rs @@ -252,10 +252,11 @@ fn apply_payload(state: &mut EditorState, payload: SettingsPayload) { eui.agent_settings.connected = c; } if let Some(agents) = payload.builtin_agents { - eui.agent_settings.builtin_agents = agents + let agents = agents .into_iter() .filter_map(builtin_agent_from_payload) .collect(); + eui.agent_settings.builtin_agents = dedupe_builtin_agents(agents); eui.agent_settings.next_builtin_agent_id = next_builtin_agent_id(&eui.agent_settings.builtin_agents); } @@ -442,6 +443,25 @@ fn next_builtin_agent_id(agents: &[BuiltinAgentConfig]) -> u64 { .saturating_add(1) } +fn dedupe_builtin_agents(agents: Vec) -> Vec { + let mut deduped: Vec = Vec::new(); + for agent in agents { + let is_duplicate = deduped.iter().any(|existing| { + existing.matches_config( + &agent.display_name, + &agent.api_key, + &agent.model, + agent.kind, + &agent.base_url, + ) + }); + if !is_duplicate { + deduped.push(agent); + } + } + deduped +} + fn next_acp_agent_id(agents: &[AcpAgentConfig]) -> u64 { agents .iter() @@ -668,6 +688,44 @@ mod tests { ); } + #[test] + fn duplicate_builtin_agents_are_deduped_on_load() { + let settings = r#"{ + "version": 1, + "builtin_agents": [ + { + "id": "builtin-1", + "display_name": "MINIMAX", + "kind": "openai-compat", + "api_key": "sk-test", + "model": "MiniMax-M2.7", + "base_url": "https://api.minimaxi.com/v1", + "enabled": true + }, + { + "id": "builtin-2", + "display_name": "MINIMAX", + "kind": "openai-compat", + "api_key": "sk-test", + "model": "MiniMax-M2.7", + "base_url": "https://api.minimaxi.com/v1", + "enabled": true + } + ] + }"#; + let payload: SettingsPayload = serde_json::from_str(settings).unwrap(); + let mut dst = EditorState::new(); + + apply_payload(&mut dst, payload); + + assert_eq!(dst.editor_ui.agent_settings.builtin_agents.len(), 1); + assert_eq!( + dst.editor_ui.agent_settings.builtin_agents[0].id, + "builtin-1" + ); + assert_eq!(dst.editor_ui.agent_settings.next_builtin_agent_id, 2); + } + #[test] fn acp_agents_round_trip_through_payload() { let mut src = EditorState::new();