| package diff |
|
|
| import ( |
| "crypto/sha256" |
| "encoding/hex" |
| "fmt" |
| "sort" |
| "strings" |
|
|
| "github.com/router-for-me/CLIProxyAPI/v6/internal/config" |
| ) |
|
|
| |
| 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, ",")) |
| } |
| } |
|
|
| |
| 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[:]) |
| } |
|
|