diff --git a/internal/api/handlers/management/config_lists.go b/internal/api/handlers/management/config_lists.go index 5cca03ba..0153a381 100644 --- a/internal/api/handlers/management/config_lists.go +++ b/internal/api/handlers/management/config_lists.go @@ -796,10 +796,10 @@ func (h *Handler) DeleteOAuthModelAlias(c *gin.Context) { c.JSON(404, gin.H{"error": "channel not found"}) return } - delete(h.cfg.OAuthModelAlias, channel) - if len(h.cfg.OAuthModelAlias) == 0 { - h.cfg.OAuthModelAlias = nil - } + // Set to nil instead of deleting the key so that the "explicitly disabled" + // marker survives config reload and prevents SanitizeOAuthModelAlias from + // re-injecting default aliases (fixes #222). + h.cfg.OAuthModelAlias[channel] = nil h.persist(c) } diff --git a/internal/config/config.go b/internal/config/config.go index 50b3cbd5..88e1c605 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -767,7 +767,13 @@ func (cfg *Config) SanitizeOAuthModelAlias() { out := make(map[string][]OAuthModelAlias, len(cfg.OAuthModelAlias)) for rawChannel, aliases := range cfg.OAuthModelAlias { channel := strings.ToLower(strings.TrimSpace(rawChannel)) - if channel == "" || len(aliases) == 0 { + if channel == "" { + continue + } + // Preserve channels that were explicitly set to empty/nil – they act + // as "disabled" markers so default injection won't re-add them (#222). + if len(aliases) == 0 { + out[channel] = nil continue } seenAlias := make(map[string]struct{}, len(aliases)) diff --git a/internal/config/oauth_model_alias_test.go b/internal/config/oauth_model_alias_test.go index 7497eec8..5cf05502 100644 --- a/internal/config/oauth_model_alias_test.go +++ b/internal/config/oauth_model_alias_test.go @@ -128,6 +128,50 @@ func TestSanitizeOAuthModelAlias_DoesNotOverrideUserKiroAliases(t *testing.T) { } } +func TestSanitizeOAuthModelAlias_DoesNotReinjectAfterExplicitDeletion(t *testing.T) { + // When user explicitly deletes kiro aliases (key exists with nil value), + // defaults should NOT be re-injected on subsequent sanitize calls (#222). + cfg := &Config{ + OAuthModelAlias: map[string][]OAuthModelAlias{ + "kiro": nil, // explicitly deleted + "codex": {{Name: "gpt-5", Alias: "g5"}}, + }, + } + + cfg.SanitizeOAuthModelAlias() + + kiroAliases := cfg.OAuthModelAlias["kiro"] + if len(kiroAliases) != 0 { + t.Fatalf("expected kiro aliases to remain empty after explicit deletion, got %d aliases", len(kiroAliases)) + } + // The key itself must still be present to prevent re-injection on next reload + if _, exists := cfg.OAuthModelAlias["kiro"]; !exists { + t.Fatal("expected kiro key to be preserved as nil marker after sanitization") + } + // Other channels should be unaffected + if len(cfg.OAuthModelAlias["codex"]) != 1 { + t.Fatal("expected codex aliases to be preserved") + } +} + +func TestSanitizeOAuthModelAlias_DoesNotReinjectAfterExplicitDeletionEmpty(t *testing.T) { + // Same as above but with empty slice instead of nil (PUT with empty body). + cfg := &Config{ + OAuthModelAlias: map[string][]OAuthModelAlias{ + "kiro": {}, // explicitly set to empty + }, + } + + cfg.SanitizeOAuthModelAlias() + + if len(cfg.OAuthModelAlias["kiro"]) != 0 { + t.Fatalf("expected kiro aliases to remain empty, got %d aliases", len(cfg.OAuthModelAlias["kiro"])) + } + if _, exists := cfg.OAuthModelAlias["kiro"]; !exists { + t.Fatal("expected kiro key to be preserved") + } +} + func TestSanitizeOAuthModelAlias_InjectsDefaultKiroWhenEmpty(t *testing.T) { // When OAuthModelAlias is nil, kiro defaults should still be injected cfg := &Config{}