From b65b23b2120a562dea26deb710e7f774291b518d Mon Sep 17 00:00:00 2001 From: viettranx Date: Sun, 5 Apr 2026 08:47:10 +0700 Subject: [PATCH] fix(pool): strip stale member names from settings on save When a pool member provider is deleted/disabled, its name persists in the owner's extra_provider_names config. Validation already skips stale refs, but the names accumulated as garbage in the DB. Now stripStalePoolMembers() runs after validation passes, removing member names that don't exist in the active provider set before the settings are written to the database. --- .../http/chatgpt_oauth_pool_validation.go | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/internal/http/chatgpt_oauth_pool_validation.go b/internal/http/chatgpt_oauth_pool_validation.go index 2a8d7219..8765ad85 100644 --- a/internal/http/chatgpt_oauth_pool_validation.go +++ b/internal/http/chatgpt_oauth_pool_validation.go @@ -128,7 +128,66 @@ func validateChatGPTOAuthProviderCandidate( finalProviders = append(finalProviders, *candidate) } - return validateChatGPTOAuthPoolGraph(finalProviders) + if err := validateChatGPTOAuthPoolGraph(finalProviders); err != nil { + return err + } + + // Strip stale member references from the candidate's settings so they + // don't accumulate as garbage in the DB after deleted/disabled providers. + stripStalePoolMembers(candidate, finalProviders) + return nil +} + +// stripStalePoolMembers removes pool member names from the candidate's +// settings that no longer exist in the active provider set. This prevents +// deleted/disabled provider names from accumulating as garbage in the DB. +func stripStalePoolMembers(candidate *store.LLMProviderData, activeProviders []store.LLMProviderData) { + if candidate == nil || len(candidate.Settings) == 0 { + return + } + + // Parse full settings as generic map to preserve non-pool fields. + var raw map[string]any + if json.Unmarshal(candidate.Settings, &raw) != nil { + return + } + cpRaw, ok := raw["codex_pool"] + if !ok { + return + } + cpMap, ok := cpRaw.(map[string]any) + if !ok { + return + } + namesRaw, ok := cpMap["extra_provider_names"] + if !ok { + return + } + namesSlice, ok := namesRaw.([]any) + if !ok || len(namesSlice) == 0 { + return + } + + known := make(map[string]bool, len(activeProviders)) + for _, p := range activeProviders { + known[p.Name] = true + } + + filtered := make([]any, 0, len(namesSlice)) + for _, n := range namesSlice { + if name, ok := n.(string); ok && known[name] { + filtered = append(filtered, name) + } + } + + if len(filtered) == len(namesSlice) { + return // nothing changed + } + + cpMap["extra_provider_names"] = filtered + if updated, err := json.Marshal(raw); err == nil { + candidate.Settings = updated + } } func validateChatGPTOAuthAgentRouting(