|
|
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[:]) |
|
|
} |
|
|
|