mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 13:00:52 +08:00
Introduce formatProxyURL helper to sanitize proxy addresses before logging, stripping credentials and path components while preserving host information. Rework model hash computation to sort and deduplicate name/alias pairs with case normalization, ensuring consistent output regardless of input ordering. Add signature-based identification for anonymous OpenAI-compatible provider entries to maintain stable keys across configuration reloads. Replace direct stdout prints with structured logger calls for file change notifications.
184 lines
5.1 KiB
Go
184 lines
5.1 KiB
Go
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[:])
|
|
}
|