| package diff |
|
|
| import ( |
| "fmt" |
| "net/url" |
| "reflect" |
| "strings" |
|
|
| "github.com/router-for-me/CLIProxyAPI/v6/internal/config" |
| ) |
|
|
| |
| |
| func BuildConfigChangeDetails(oldCfg, newCfg *config.Config) []string { |
| changes := make([]string, 0, 16) |
| if oldCfg == nil || newCfg == nil { |
| return changes |
| } |
|
|
| |
| if oldCfg.Port != newCfg.Port { |
| changes = append(changes, fmt.Sprintf("port: %d -> %d", oldCfg.Port, newCfg.Port)) |
| } |
| if oldCfg.AuthDir != newCfg.AuthDir { |
| changes = append(changes, fmt.Sprintf("auth-dir: %s -> %s", oldCfg.AuthDir, newCfg.AuthDir)) |
| } |
| if oldCfg.Debug != newCfg.Debug { |
| changes = append(changes, fmt.Sprintf("debug: %t -> %t", oldCfg.Debug, newCfg.Debug)) |
| } |
| if oldCfg.LoggingToFile != newCfg.LoggingToFile { |
| changes = append(changes, fmt.Sprintf("logging-to-file: %t -> %t", oldCfg.LoggingToFile, newCfg.LoggingToFile)) |
| } |
| if oldCfg.UsageStatisticsEnabled != newCfg.UsageStatisticsEnabled { |
| changes = append(changes, fmt.Sprintf("usage-statistics-enabled: %t -> %t", oldCfg.UsageStatisticsEnabled, newCfg.UsageStatisticsEnabled)) |
| } |
| if oldCfg.DisableCooling != newCfg.DisableCooling { |
| changes = append(changes, fmt.Sprintf("disable-cooling: %t -> %t", oldCfg.DisableCooling, newCfg.DisableCooling)) |
| } |
| if oldCfg.RequestLog != newCfg.RequestLog { |
| changes = append(changes, fmt.Sprintf("request-log: %t -> %t", oldCfg.RequestLog, newCfg.RequestLog)) |
| } |
| if oldCfg.RequestRetry != newCfg.RequestRetry { |
| changes = append(changes, fmt.Sprintf("request-retry: %d -> %d", oldCfg.RequestRetry, newCfg.RequestRetry)) |
| } |
| if oldCfg.MaxRetryInterval != newCfg.MaxRetryInterval { |
| changes = append(changes, fmt.Sprintf("max-retry-interval: %d -> %d", oldCfg.MaxRetryInterval, newCfg.MaxRetryInterval)) |
| } |
| if oldCfg.ProxyURL != newCfg.ProxyURL { |
| changes = append(changes, fmt.Sprintf("proxy-url: %s -> %s", formatProxyURL(oldCfg.ProxyURL), formatProxyURL(newCfg.ProxyURL))) |
| } |
| if oldCfg.WebsocketAuth != newCfg.WebsocketAuth { |
| changes = append(changes, fmt.Sprintf("ws-auth: %t -> %t", oldCfg.WebsocketAuth, newCfg.WebsocketAuth)) |
| } |
| if oldCfg.ForceModelPrefix != newCfg.ForceModelPrefix { |
| changes = append(changes, fmt.Sprintf("force-model-prefix: %t -> %t", oldCfg.ForceModelPrefix, newCfg.ForceModelPrefix)) |
| } |
| if oldCfg.NonStreamKeepAliveInterval != newCfg.NonStreamKeepAliveInterval { |
| changes = append(changes, fmt.Sprintf("nonstream-keepalive-interval: %d -> %d", oldCfg.NonStreamKeepAliveInterval, newCfg.NonStreamKeepAliveInterval)) |
| } |
|
|
| |
| if oldCfg.QuotaExceeded.SwitchProject != newCfg.QuotaExceeded.SwitchProject { |
| changes = append(changes, fmt.Sprintf("quota-exceeded.switch-project: %t -> %t", oldCfg.QuotaExceeded.SwitchProject, newCfg.QuotaExceeded.SwitchProject)) |
| } |
| if oldCfg.QuotaExceeded.SwitchPreviewModel != newCfg.QuotaExceeded.SwitchPreviewModel { |
| changes = append(changes, fmt.Sprintf("quota-exceeded.switch-preview-model: %t -> %t", oldCfg.QuotaExceeded.SwitchPreviewModel, newCfg.QuotaExceeded.SwitchPreviewModel)) |
| } |
|
|
| |
| if len(oldCfg.APIKeys) != len(newCfg.APIKeys) { |
| changes = append(changes, fmt.Sprintf("api-keys count: %d -> %d", len(oldCfg.APIKeys), len(newCfg.APIKeys))) |
| } else if !reflect.DeepEqual(trimStrings(oldCfg.APIKeys), trimStrings(newCfg.APIKeys)) { |
| changes = append(changes, "api-keys: values updated (count unchanged, redacted)") |
| } |
| if len(oldCfg.GeminiKey) != len(newCfg.GeminiKey) { |
| changes = append(changes, fmt.Sprintf("gemini-api-key count: %d -> %d", len(oldCfg.GeminiKey), len(newCfg.GeminiKey))) |
| } else { |
| for i := range oldCfg.GeminiKey { |
| o := oldCfg.GeminiKey[i] |
| n := newCfg.GeminiKey[i] |
| if strings.TrimSpace(o.BaseURL) != strings.TrimSpace(n.BaseURL) { |
| changes = append(changes, fmt.Sprintf("gemini[%d].base-url: %s -> %s", i, strings.TrimSpace(o.BaseURL), strings.TrimSpace(n.BaseURL))) |
| } |
| if strings.TrimSpace(o.ProxyURL) != strings.TrimSpace(n.ProxyURL) { |
| changes = append(changes, fmt.Sprintf("gemini[%d].proxy-url: %s -> %s", i, formatProxyURL(o.ProxyURL), formatProxyURL(n.ProxyURL))) |
| } |
| if strings.TrimSpace(o.Prefix) != strings.TrimSpace(n.Prefix) { |
| changes = append(changes, fmt.Sprintf("gemini[%d].prefix: %s -> %s", i, strings.TrimSpace(o.Prefix), strings.TrimSpace(n.Prefix))) |
| } |
| if strings.TrimSpace(o.APIKey) != strings.TrimSpace(n.APIKey) { |
| changes = append(changes, fmt.Sprintf("gemini[%d].api-key: updated", i)) |
| } |
| if !equalStringMap(o.Headers, n.Headers) { |
| changes = append(changes, fmt.Sprintf("gemini[%d].headers: updated", i)) |
| } |
| oldModels := SummarizeGeminiModels(o.Models) |
| newModels := SummarizeGeminiModels(n.Models) |
| if oldModels.hash != newModels.hash { |
| changes = append(changes, fmt.Sprintf("gemini[%d].models: updated (%d -> %d entries)", i, oldModels.count, newModels.count)) |
| } |
| oldExcluded := SummarizeExcludedModels(o.ExcludedModels) |
| newExcluded := SummarizeExcludedModels(n.ExcludedModels) |
| if oldExcluded.hash != newExcluded.hash { |
| changes = append(changes, fmt.Sprintf("gemini[%d].excluded-models: updated (%d -> %d entries)", i, oldExcluded.count, newExcluded.count)) |
| } |
| } |
| } |
|
|
| |
| if len(oldCfg.ClaudeKey) != len(newCfg.ClaudeKey) { |
| changes = append(changes, fmt.Sprintf("claude-api-key count: %d -> %d", len(oldCfg.ClaudeKey), len(newCfg.ClaudeKey))) |
| } else { |
| for i := range oldCfg.ClaudeKey { |
| o := oldCfg.ClaudeKey[i] |
| n := newCfg.ClaudeKey[i] |
| if strings.TrimSpace(o.BaseURL) != strings.TrimSpace(n.BaseURL) { |
| changes = append(changes, fmt.Sprintf("claude[%d].base-url: %s -> %s", i, strings.TrimSpace(o.BaseURL), strings.TrimSpace(n.BaseURL))) |
| } |
| if strings.TrimSpace(o.ProxyURL) != strings.TrimSpace(n.ProxyURL) { |
| changes = append(changes, fmt.Sprintf("claude[%d].proxy-url: %s -> %s", i, formatProxyURL(o.ProxyURL), formatProxyURL(n.ProxyURL))) |
| } |
| if strings.TrimSpace(o.Prefix) != strings.TrimSpace(n.Prefix) { |
| changes = append(changes, fmt.Sprintf("claude[%d].prefix: %s -> %s", i, strings.TrimSpace(o.Prefix), strings.TrimSpace(n.Prefix))) |
| } |
| if strings.TrimSpace(o.APIKey) != strings.TrimSpace(n.APIKey) { |
| changes = append(changes, fmt.Sprintf("claude[%d].api-key: updated", i)) |
| } |
| if len(o.APIKeyEntries) != len(n.APIKeyEntries) { |
| changes = append(changes, fmt.Sprintf("claude[%d].api-key-entries count: %d -> %d", i, len(o.APIKeyEntries), len(n.APIKeyEntries))) |
| } else if !equalClaudeAPIKeyEntries(o.APIKeyEntries, n.APIKeyEntries) { |
| changes = append(changes, fmt.Sprintf("claude[%d].api-key-entries: updated", i)) |
| } |
| if !equalStringMap(o.Headers, n.Headers) { |
| changes = append(changes, fmt.Sprintf("claude[%d].headers: updated", i)) |
| } |
| oldModels := SummarizeClaudeModels(o.Models) |
| newModels := SummarizeClaudeModels(n.Models) |
| if oldModels.hash != newModels.hash { |
| changes = append(changes, fmt.Sprintf("claude[%d].models: updated (%d -> %d entries)", i, oldModels.count, newModels.count)) |
| } |
| oldExcluded := SummarizeExcludedModels(o.ExcludedModels) |
| newExcluded := SummarizeExcludedModels(n.ExcludedModels) |
| if oldExcluded.hash != newExcluded.hash { |
| changes = append(changes, fmt.Sprintf("claude[%d].excluded-models: updated (%d -> %d entries)", i, oldExcluded.count, newExcluded.count)) |
| } |
| } |
| } |
|
|
| |
| if len(oldCfg.CodexKey) != len(newCfg.CodexKey) { |
| changes = append(changes, fmt.Sprintf("codex-api-key count: %d -> %d", len(oldCfg.CodexKey), len(newCfg.CodexKey))) |
| } else { |
| for i := range oldCfg.CodexKey { |
| o := oldCfg.CodexKey[i] |
| n := newCfg.CodexKey[i] |
| if strings.TrimSpace(o.BaseURL) != strings.TrimSpace(n.BaseURL) { |
| changes = append(changes, fmt.Sprintf("codex[%d].base-url: %s -> %s", i, strings.TrimSpace(o.BaseURL), strings.TrimSpace(n.BaseURL))) |
| } |
| if strings.TrimSpace(o.ProxyURL) != strings.TrimSpace(n.ProxyURL) { |
| changes = append(changes, fmt.Sprintf("codex[%d].proxy-url: %s -> %s", i, formatProxyURL(o.ProxyURL), formatProxyURL(n.ProxyURL))) |
| } |
| if strings.TrimSpace(o.Prefix) != strings.TrimSpace(n.Prefix) { |
| changes = append(changes, fmt.Sprintf("codex[%d].prefix: %s -> %s", i, strings.TrimSpace(o.Prefix), strings.TrimSpace(n.Prefix))) |
| } |
| if strings.TrimSpace(o.APIKey) != strings.TrimSpace(n.APIKey) { |
| changes = append(changes, fmt.Sprintf("codex[%d].api-key: updated", i)) |
| } |
| if !equalStringMap(o.Headers, n.Headers) { |
| changes = append(changes, fmt.Sprintf("codex[%d].headers: updated", i)) |
| } |
| oldModels := SummarizeCodexModels(o.Models) |
| newModels := SummarizeCodexModels(n.Models) |
| if oldModels.hash != newModels.hash { |
| changes = append(changes, fmt.Sprintf("codex[%d].models: updated (%d -> %d entries)", i, oldModels.count, newModels.count)) |
| } |
| oldExcluded := SummarizeExcludedModels(o.ExcludedModels) |
| newExcluded := SummarizeExcludedModels(n.ExcludedModels) |
| if oldExcluded.hash != newExcluded.hash { |
| changes = append(changes, fmt.Sprintf("codex[%d].excluded-models: updated (%d -> %d entries)", i, oldExcluded.count, newExcluded.count)) |
| } |
| } |
| } |
|
|
| |
| oldAmpURL := strings.TrimSpace(oldCfg.AmpCode.UpstreamURL) |
| newAmpURL := strings.TrimSpace(newCfg.AmpCode.UpstreamURL) |
| if oldAmpURL != newAmpURL { |
| changes = append(changes, fmt.Sprintf("ampcode.upstream-url: %s -> %s", oldAmpURL, newAmpURL)) |
| } |
| oldAmpKey := strings.TrimSpace(oldCfg.AmpCode.UpstreamAPIKey) |
| newAmpKey := strings.TrimSpace(newCfg.AmpCode.UpstreamAPIKey) |
| switch { |
| case oldAmpKey == "" && newAmpKey != "": |
| changes = append(changes, "ampcode.upstream-api-key: added") |
| case oldAmpKey != "" && newAmpKey == "": |
| changes = append(changes, "ampcode.upstream-api-key: removed") |
| case oldAmpKey != newAmpKey: |
| changes = append(changes, "ampcode.upstream-api-key: updated") |
| } |
| if oldCfg.AmpCode.RestrictManagementToLocalhost != newCfg.AmpCode.RestrictManagementToLocalhost { |
| changes = append(changes, fmt.Sprintf("ampcode.restrict-management-to-localhost: %t -> %t", oldCfg.AmpCode.RestrictManagementToLocalhost, newCfg.AmpCode.RestrictManagementToLocalhost)) |
| } |
| oldMappings := SummarizeAmpModelMappings(oldCfg.AmpCode.ModelMappings) |
| newMappings := SummarizeAmpModelMappings(newCfg.AmpCode.ModelMappings) |
| if oldMappings.hash != newMappings.hash { |
| changes = append(changes, fmt.Sprintf("ampcode.model-mappings: updated (%d -> %d entries)", oldMappings.count, newMappings.count)) |
| } |
| if oldCfg.AmpCode.ForceModelMappings != newCfg.AmpCode.ForceModelMappings { |
| changes = append(changes, fmt.Sprintf("ampcode.force-model-mappings: %t -> %t", oldCfg.AmpCode.ForceModelMappings, newCfg.AmpCode.ForceModelMappings)) |
| } |
| oldUpstreamAPIKeysCount := len(oldCfg.AmpCode.UpstreamAPIKeys) |
| newUpstreamAPIKeysCount := len(newCfg.AmpCode.UpstreamAPIKeys) |
| if !equalUpstreamAPIKeys(oldCfg.AmpCode.UpstreamAPIKeys, newCfg.AmpCode.UpstreamAPIKeys) { |
| changes = append(changes, fmt.Sprintf("ampcode.upstream-api-keys: updated (%d -> %d entries)", oldUpstreamAPIKeysCount, newUpstreamAPIKeysCount)) |
| } |
|
|
| if entries, _ := DiffOAuthExcludedModelChanges(oldCfg.OAuthExcludedModels, newCfg.OAuthExcludedModels); len(entries) > 0 { |
| changes = append(changes, entries...) |
| } |
| if entries, _ := DiffOAuthModelAliasChanges(oldCfg.OAuthModelAlias, newCfg.OAuthModelAlias); len(entries) > 0 { |
| changes = append(changes, entries...) |
| } |
|
|
| |
| if oldCfg.RemoteManagement.AllowRemote != newCfg.RemoteManagement.AllowRemote { |
| changes = append(changes, fmt.Sprintf("remote-management.allow-remote: %t -> %t", oldCfg.RemoteManagement.AllowRemote, newCfg.RemoteManagement.AllowRemote)) |
| } |
| if oldCfg.RemoteManagement.DisableControlPanel != newCfg.RemoteManagement.DisableControlPanel { |
| changes = append(changes, fmt.Sprintf("remote-management.disable-control-panel: %t -> %t", oldCfg.RemoteManagement.DisableControlPanel, newCfg.RemoteManagement.DisableControlPanel)) |
| } |
| oldPanelRepo := strings.TrimSpace(oldCfg.RemoteManagement.PanelGitHubRepository) |
| newPanelRepo := strings.TrimSpace(newCfg.RemoteManagement.PanelGitHubRepository) |
| if oldPanelRepo != newPanelRepo { |
| changes = append(changes, fmt.Sprintf("remote-management.panel-github-repository: %s -> %s", oldPanelRepo, newPanelRepo)) |
| } |
| if oldCfg.RemoteManagement.SecretKey != newCfg.RemoteManagement.SecretKey { |
| switch { |
| case oldCfg.RemoteManagement.SecretKey == "" && newCfg.RemoteManagement.SecretKey != "": |
| changes = append(changes, "remote-management.secret-key: created") |
| case oldCfg.RemoteManagement.SecretKey != "" && newCfg.RemoteManagement.SecretKey == "": |
| changes = append(changes, "remote-management.secret-key: deleted") |
| default: |
| changes = append(changes, "remote-management.secret-key: updated") |
| } |
| } |
|
|
| |
| if compat := DiffOpenAICompatibility(oldCfg.OpenAICompatibility, newCfg.OpenAICompatibility); len(compat) > 0 { |
| changes = append(changes, "openai-compatibility:") |
| for _, c := range compat { |
| changes = append(changes, " "+c) |
| } |
| } |
|
|
| |
| if len(oldCfg.VertexCompatAPIKey) != len(newCfg.VertexCompatAPIKey) { |
| changes = append(changes, fmt.Sprintf("vertex-api-key count: %d -> %d", len(oldCfg.VertexCompatAPIKey), len(newCfg.VertexCompatAPIKey))) |
| } else { |
| for i := range oldCfg.VertexCompatAPIKey { |
| o := oldCfg.VertexCompatAPIKey[i] |
| n := newCfg.VertexCompatAPIKey[i] |
| if strings.TrimSpace(o.BaseURL) != strings.TrimSpace(n.BaseURL) { |
| changes = append(changes, fmt.Sprintf("vertex[%d].base-url: %s -> %s", i, strings.TrimSpace(o.BaseURL), strings.TrimSpace(n.BaseURL))) |
| } |
| if strings.TrimSpace(o.ProxyURL) != strings.TrimSpace(n.ProxyURL) { |
| changes = append(changes, fmt.Sprintf("vertex[%d].proxy-url: %s -> %s", i, formatProxyURL(o.ProxyURL), formatProxyURL(n.ProxyURL))) |
| } |
| if strings.TrimSpace(o.Prefix) != strings.TrimSpace(n.Prefix) { |
| changes = append(changes, fmt.Sprintf("vertex[%d].prefix: %s -> %s", i, strings.TrimSpace(o.Prefix), strings.TrimSpace(n.Prefix))) |
| } |
| if strings.TrimSpace(o.APIKey) != strings.TrimSpace(n.APIKey) { |
| changes = append(changes, fmt.Sprintf("vertex[%d].api-key: updated", i)) |
| } |
| oldModels := SummarizeVertexModels(o.Models) |
| newModels := SummarizeVertexModels(n.Models) |
| if oldModels.hash != newModels.hash { |
| changes = append(changes, fmt.Sprintf("vertex[%d].models: updated (%d -> %d entries)", i, oldModels.count, newModels.count)) |
| } |
| if !equalStringMap(o.Headers, n.Headers) { |
| changes = append(changes, fmt.Sprintf("vertex[%d].headers: updated", i)) |
| } |
| } |
| } |
|
|
| return changes |
| } |
|
|
| func trimStrings(in []string) []string { |
| out := make([]string, len(in)) |
| for i := range in { |
| out[i] = strings.TrimSpace(in[i]) |
| } |
| return out |
| } |
|
|
| func equalStringMap(a, b map[string]string) bool { |
| if len(a) != len(b) { |
| return false |
| } |
| for k, v := range a { |
| if b[k] != v { |
| return false |
| } |
| } |
| return true |
| } |
|
|
| func formatProxyURL(raw string) string { |
| trimmed := strings.TrimSpace(raw) |
| if trimmed == "" { |
| return "<none>" |
| } |
| parsed, err := url.Parse(trimmed) |
| if err != nil { |
| return "<redacted>" |
| } |
| host := strings.TrimSpace(parsed.Host) |
| scheme := strings.TrimSpace(parsed.Scheme) |
| if host == "" { |
| |
| parsed2, err2 := url.Parse("http://" + trimmed) |
| if err2 == nil { |
| host = strings.TrimSpace(parsed2.Host) |
| } |
| scheme = "" |
| } |
| if host == "" { |
| return "<redacted>" |
| } |
| if scheme == "" { |
| return host |
| } |
| return scheme + "://" + host |
| } |
|
|
| func equalStringSet(a, b []string) bool { |
| if len(a) == 0 && len(b) == 0 { |
| return true |
| } |
| aSet := make(map[string]struct{}, len(a)) |
| for _, k := range a { |
| aSet[strings.TrimSpace(k)] = struct{}{} |
| } |
| bSet := make(map[string]struct{}, len(b)) |
| for _, k := range b { |
| bSet[strings.TrimSpace(k)] = struct{}{} |
| } |
| if len(aSet) != len(bSet) { |
| return false |
| } |
| for k := range aSet { |
| if _, ok := bSet[k]; !ok { |
| return false |
| } |
| } |
| return true |
| } |
|
|
| |
| |
| func equalUpstreamAPIKeys(a, b []config.AmpUpstreamAPIKeyEntry) bool { |
| if len(a) != len(b) { |
| return false |
| } |
| for i := range a { |
| if strings.TrimSpace(a[i].UpstreamAPIKey) != strings.TrimSpace(b[i].UpstreamAPIKey) { |
| return false |
| } |
| if !equalStringSet(a[i].APIKeys, b[i].APIKeys) { |
| return false |
| } |
| } |
| return true |
| } |
|
|
| |
| func equalClaudeAPIKeyEntries(a, b []config.ClaudeAPIKeyEntry) bool { |
| if len(a) != len(b) { |
| return false |
| } |
| for i := range a { |
| if strings.TrimSpace(a[i].APIKey) != strings.TrimSpace(b[i].APIKey) { |
| return false |
| } |
| if strings.TrimSpace(a[i].ProxyURL) != strings.TrimSpace(b[i].ProxyURL) { |
| return false |
| } |
| } |
| return true |
| } |
|
|