Spaces:
Paused
Paused
| package diff | |
| import ( | |
| "crypto/sha256" | |
| "encoding/hex" | |
| "fmt" | |
| "sort" | |
| "strings" | |
| "github.com/router-for-me/CLIProxyAPI/v6/internal/config" | |
| ) | |
| // DiffOpenAICompatibility produces human-readable change descriptions. | |
| func DiffOpenAICompatibility(oldList, newList []config.OpenAICompatibility) []string { | |
| changes := make([]string, 0) | |
| oldMap := make(map[string]config.OpenAICompatibility, len(oldList)) | |
| oldLabels := make(map[string]string, len(oldList)) | |
| for idx, entry := range oldList { | |
| key, label := openAICompatKey(entry, idx) | |
| oldMap[key] = entry | |
| oldLabels[key] = label | |
| } | |
| newMap := make(map[string]config.OpenAICompatibility, len(newList)) | |
| newLabels := make(map[string]string, len(newList)) | |
| for idx, entry := range newList { | |
| key, label := openAICompatKey(entry, idx) | |
| newMap[key] = entry | |
| newLabels[key] = label | |
| } | |
| keySet := make(map[string]struct{}, len(oldMap)+len(newMap)) | |
| for key := range oldMap { | |
| keySet[key] = struct{}{} | |
| } | |
| for key := range newMap { | |
| keySet[key] = struct{}{} | |
| } | |
| orderedKeys := make([]string, 0, len(keySet)) | |
| for key := range keySet { | |
| orderedKeys = append(orderedKeys, key) | |
| } | |
| sort.Strings(orderedKeys) | |
| for _, key := range orderedKeys { | |
| oldEntry, oldOk := oldMap[key] | |
| newEntry, newOk := newMap[key] | |
| label := oldLabels[key] | |
| if label == "" { | |
| label = newLabels[key] | |
| } | |
| switch { | |
| case !oldOk: | |
| changes = append(changes, fmt.Sprintf("provider added: %s (api-keys=%d, models=%d)", label, countAPIKeys(newEntry), countOpenAIModels(newEntry.Models))) | |
| case !newOk: | |
| changes = append(changes, fmt.Sprintf("provider removed: %s (api-keys=%d, models=%d)", label, countAPIKeys(oldEntry), countOpenAIModels(oldEntry.Models))) | |
| default: | |
| if detail := describeOpenAICompatibilityUpdate(oldEntry, newEntry); detail != "" { | |
| changes = append(changes, fmt.Sprintf("provider updated: %s %s", label, detail)) | |
| } | |
| } | |
| } | |
| return changes | |
| } | |
| func describeOpenAICompatibilityUpdate(oldEntry, newEntry config.OpenAICompatibility) string { | |
| oldKeyCount := countAPIKeys(oldEntry) | |
| newKeyCount := countAPIKeys(newEntry) | |
| oldModelCount := countOpenAIModels(oldEntry.Models) | |
| newModelCount := countOpenAIModels(newEntry.Models) | |
| details := make([]string, 0, 3) | |
| if oldKeyCount != newKeyCount { | |
| details = append(details, fmt.Sprintf("api-keys %d -> %d", oldKeyCount, newKeyCount)) | |
| } | |
| if oldModelCount != newModelCount { | |
| details = append(details, fmt.Sprintf("models %d -> %d", oldModelCount, newModelCount)) | |
| } | |
| if !equalStringMap(oldEntry.Headers, newEntry.Headers) { | |
| details = append(details, "headers updated") | |
| } | |
| if len(details) == 0 { | |
| return "" | |
| } | |
| return "(" + strings.Join(details, ", ") + ")" | |
| } | |
| func countAPIKeys(entry config.OpenAICompatibility) int { | |
| count := 0 | |
| for _, keyEntry := range entry.APIKeyEntries { | |
| if strings.TrimSpace(keyEntry.APIKey) != "" { | |
| count++ | |
| } | |
| } | |
| return count | |
| } | |
| func countOpenAIModels(models []config.OpenAICompatibilityModel) int { | |
| count := 0 | |
| for _, model := range models { | |
| name := strings.TrimSpace(model.Name) | |
| alias := strings.TrimSpace(model.Alias) | |
| if name == "" && alias == "" { | |
| continue | |
| } | |
| count++ | |
| } | |
| return count | |
| } | |
| func openAICompatKey(entry config.OpenAICompatibility, index int) (string, string) { | |
| name := strings.TrimSpace(entry.Name) | |
| if name != "" { | |
| return "name:" + name, name | |
| } | |
| base := strings.TrimSpace(entry.BaseURL) | |
| if base != "" { | |
| return "base:" + base, base | |
| } | |
| for _, model := range entry.Models { | |
| alias := strings.TrimSpace(model.Alias) | |
| if alias == "" { | |
| alias = strings.TrimSpace(model.Name) | |
| } | |
| if alias != "" { | |
| return "alias:" + alias, alias | |
| } | |
| } | |
| sig := openAICompatSignature(entry) | |
| if sig == "" { | |
| return fmt.Sprintf("index:%d", index), fmt.Sprintf("entry-%d", index+1) | |
| } | |
| short := sig | |
| if len(short) > 8 { | |
| short = short[:8] | |
| } | |
| return "sig:" + sig, "compat-" + short | |
| } | |
| func openAICompatSignature(entry config.OpenAICompatibility) string { | |
| var parts []string | |
| if v := strings.TrimSpace(entry.Name); v != "" { | |
| parts = append(parts, "name="+strings.ToLower(v)) | |
| } | |
| if v := strings.TrimSpace(entry.BaseURL); v != "" { | |
| parts = append(parts, "base="+v) | |
| } | |
| models := make([]string, 0, len(entry.Models)) | |
| for _, model := range entry.Models { | |
| name := strings.TrimSpace(model.Name) | |
| alias := strings.TrimSpace(model.Alias) | |
| if name == "" && alias == "" { | |
| continue | |
| } | |
| models = append(models, strings.ToLower(name)+"|"+strings.ToLower(alias)) | |
| } | |
| if len(models) > 0 { | |
| sort.Strings(models) | |
| parts = append(parts, "models="+strings.Join(models, ",")) | |
| } | |
| if len(entry.Headers) > 0 { | |
| keys := make([]string, 0, len(entry.Headers)) | |
| for k := range entry.Headers { | |
| if trimmed := strings.TrimSpace(k); trimmed != "" { | |
| keys = append(keys, strings.ToLower(trimmed)) | |
| } | |
| } | |
| if len(keys) > 0 { | |
| sort.Strings(keys) | |
| parts = append(parts, "headers="+strings.Join(keys, ",")) | |
| } | |
| } | |
| // Intentionally exclude API key material; only count non-empty entries. | |
| if count := countAPIKeys(entry); count > 0 { | |
| parts = append(parts, fmt.Sprintf("api_keys=%d", count)) | |
| } | |
| if len(parts) == 0 { | |
| return "" | |
| } | |
| sum := sha256.Sum256([]byte(strings.Join(parts, "|"))) | |
| return hex.EncodeToString(sum[:]) | |
| } | |