Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
392
backend/internal/config/config_normalization.go
Normal file
392
backend/internal/config/config_normalization.go
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
sdkpluginstore "github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginstore"
|
||||
)
|
||||
|
||||
// NormalizePluginsConfig applies default plugin configuration values.
|
||||
func (cfg *Config) NormalizePluginsConfig() {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
cfg.Plugins.Dir = strings.TrimSpace(cfg.Plugins.Dir)
|
||||
if cfg.Plugins.Dir == "" {
|
||||
cfg.Plugins.Dir = defaultPluginsDir
|
||||
}
|
||||
if len(cfg.Plugins.StoreSources) > 0 {
|
||||
sources := make([]string, 0, len(cfg.Plugins.StoreSources))
|
||||
for _, source := range cfg.Plugins.StoreSources {
|
||||
source = strings.TrimSpace(source)
|
||||
if source == "" {
|
||||
continue
|
||||
}
|
||||
sources = append(sources, source)
|
||||
}
|
||||
cfg.Plugins.StoreSources = sources
|
||||
}
|
||||
cfg.Plugins.StoreAuth = sdkpluginstore.NormalizeAuthConfigs(cfg.Plugins.StoreAuth)
|
||||
if cfg.Plugins.Configs == nil {
|
||||
cfg.Plugins.Configs = map[string]PluginInstanceConfig{}
|
||||
}
|
||||
}
|
||||
|
||||
// SanitizeCodexHeaderDefaults trims surrounding whitespace from the
|
||||
// configured Codex header fallback values.
|
||||
func (cfg *Config) SanitizeCodexHeaderDefaults() {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
cfg.CodexHeaderDefaults.UserAgent = strings.TrimSpace(cfg.CodexHeaderDefaults.UserAgent)
|
||||
cfg.CodexHeaderDefaults.BetaFeatures = strings.TrimSpace(cfg.CodexHeaderDefaults.BetaFeatures)
|
||||
}
|
||||
|
||||
// SanitizeClaudeHeaderDefaults trims surrounding whitespace from the
|
||||
// configured Claude fingerprint baseline values.
|
||||
func (cfg *Config) SanitizeClaudeHeaderDefaults() {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
cfg.ClaudeHeaderDefaults.UserAgent = strings.TrimSpace(cfg.ClaudeHeaderDefaults.UserAgent)
|
||||
cfg.ClaudeHeaderDefaults.PackageVersion = strings.TrimSpace(cfg.ClaudeHeaderDefaults.PackageVersion)
|
||||
cfg.ClaudeHeaderDefaults.RuntimeVersion = strings.TrimSpace(cfg.ClaudeHeaderDefaults.RuntimeVersion)
|
||||
cfg.ClaudeHeaderDefaults.OS = strings.TrimSpace(cfg.ClaudeHeaderDefaults.OS)
|
||||
cfg.ClaudeHeaderDefaults.Arch = strings.TrimSpace(cfg.ClaudeHeaderDefaults.Arch)
|
||||
cfg.ClaudeHeaderDefaults.Timeout = strings.TrimSpace(cfg.ClaudeHeaderDefaults.Timeout)
|
||||
cfg.ClaudeHeaderDefaults.Timezone = strings.TrimSpace(cfg.ClaudeHeaderDefaults.Timezone)
|
||||
}
|
||||
|
||||
// SanitizeOAuthModelAlias normalizes and deduplicates global OAuth model name aliases.
|
||||
// It trims whitespace, normalizes channel keys to lower-case, drops empty entries,
|
||||
// allows multiple aliases per upstream name, and ensures aliases are unique within each channel.
|
||||
func (cfg *Config) SanitizeOAuthModelAlias() {
|
||||
if cfg == nil || len(cfg.OAuthModelAlias) == 0 {
|
||||
return
|
||||
}
|
||||
out := make(map[string][]OAuthModelAlias, len(cfg.OAuthModelAlias))
|
||||
for rawChannel, aliases := range cfg.OAuthModelAlias {
|
||||
channel := strings.ToLower(strings.TrimSpace(rawChannel))
|
||||
if channel == "" || len(aliases) == 0 {
|
||||
continue
|
||||
}
|
||||
seenAlias := make(map[string]struct{}, len(aliases))
|
||||
clean := make([]OAuthModelAlias, 0, len(aliases))
|
||||
for _, entry := range aliases {
|
||||
name := strings.TrimSpace(entry.Name)
|
||||
alias := strings.TrimSpace(entry.Alias)
|
||||
if name == "" || alias == "" {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(name, alias) {
|
||||
continue
|
||||
}
|
||||
aliasKey := strings.ToLower(alias)
|
||||
if _, ok := seenAlias[aliasKey]; ok {
|
||||
continue
|
||||
}
|
||||
seenAlias[aliasKey] = struct{}{}
|
||||
clean = append(clean, OAuthModelAlias{
|
||||
Name: name,
|
||||
Alias: alias,
|
||||
Fork: entry.Fork,
|
||||
DisplayName: strings.TrimSpace(entry.DisplayName),
|
||||
ForceMapping: entry.ForceMapping,
|
||||
})
|
||||
}
|
||||
if len(clean) > 0 {
|
||||
out[channel] = clean
|
||||
}
|
||||
}
|
||||
cfg.OAuthModelAlias = out
|
||||
}
|
||||
|
||||
// SanitizeOAuthRequestScopedErrors normalizes and validates global OAuth request-scoped error rules.
|
||||
// It trims whitespace, normalizes channel keys to lower-case, validates status/action, and drops invalid rules.
|
||||
func (cfg *Config) SanitizeOAuthRequestScopedErrors() {
|
||||
if cfg == nil || len(cfg.OAuthRequestScopedErrors) == 0 {
|
||||
return
|
||||
}
|
||||
out := make(map[string][]RequestScopedErrorRule, len(cfg.OAuthRequestScopedErrors))
|
||||
for rawChannel, rules := range cfg.OAuthRequestScopedErrors {
|
||||
channel := strings.ToLower(strings.TrimSpace(rawChannel))
|
||||
if channel == "" || len(rules) == 0 {
|
||||
continue
|
||||
}
|
||||
clean := make([]RequestScopedErrorRule, 0, len(rules))
|
||||
for _, r := range rules {
|
||||
action := strings.ToLower(strings.TrimSpace(r.Action))
|
||||
match := make([]string, 0, len(r.Match))
|
||||
for _, m := range r.Match {
|
||||
if tm := strings.TrimSpace(m); tm != "" {
|
||||
match = append(match, tm)
|
||||
}
|
||||
}
|
||||
matchRegexr := make([]string, 0, len(r.MatchRegexr))
|
||||
for _, re := range r.MatchRegexr {
|
||||
if tre := strings.TrimSpace(re); tre != "" {
|
||||
matchRegexr = append(matchRegexr, tre)
|
||||
}
|
||||
}
|
||||
if r.Status <= 0 || (len(match) == 0 && len(matchRegexr) == 0) || action == "" {
|
||||
continue
|
||||
}
|
||||
clean = append(clean, RequestScopedErrorRule{
|
||||
Status: r.Status,
|
||||
Match: match,
|
||||
MatchRegexr: matchRegexr,
|
||||
Action: action,
|
||||
})
|
||||
}
|
||||
if len(clean) > 0 {
|
||||
out[channel] = clean
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
cfg.OAuthRequestScopedErrors = nil
|
||||
return
|
||||
}
|
||||
cfg.OAuthRequestScopedErrors = out
|
||||
}
|
||||
|
||||
// SanitizeOpenAICompatibility removes OpenAI-compatibility provider entries that are
|
||||
// not actionable, specifically those missing a BaseURL. It trims whitespace before
|
||||
// evaluation and preserves the relative order of remaining entries.
|
||||
func (cfg *Config) SanitizeOpenAICompatibility() {
|
||||
if cfg == nil || len(cfg.OpenAICompatibility) == 0 {
|
||||
return
|
||||
}
|
||||
out := make([]OpenAICompatibility, 0, len(cfg.OpenAICompatibility))
|
||||
for i := range cfg.OpenAICompatibility {
|
||||
e := cfg.OpenAICompatibility[i]
|
||||
e.Name = strings.TrimSpace(e.Name)
|
||||
e.Prefix = normalizeModelPrefix(e.Prefix)
|
||||
e.BaseURL = strings.TrimSpace(e.BaseURL)
|
||||
e.Headers = NormalizeHeaders(e.Headers)
|
||||
if e.BaseURL == "" {
|
||||
// Skip providers with no base-url; treated as removed
|
||||
continue
|
||||
}
|
||||
out = append(out, e)
|
||||
}
|
||||
cfg.OpenAICompatibility = out
|
||||
}
|
||||
|
||||
// SanitizeCodexKeys removes Codex API key entries missing a BaseURL.
|
||||
// It trims whitespace and preserves order for remaining entries.
|
||||
func (cfg *Config) SanitizeCodexKeys() {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
cfg.CodexKey = sanitizeCodexKeyEntries(cfg.CodexKey)
|
||||
}
|
||||
|
||||
// SanitizeXAIKeys removes xAI API key entries missing a BaseURL.
|
||||
// It applies the same normalization rules as codex-api-key.
|
||||
func (cfg *Config) SanitizeXAIKeys() {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
cfg.XAIKey = sanitizeCodexKeyEntries(cfg.XAIKey)
|
||||
for i := range cfg.XAIKey {
|
||||
cfg.XAIKey[i].AlphaSearch = false
|
||||
}
|
||||
}
|
||||
|
||||
func sanitizeCodexKeyEntries(entries []CodexKey) []CodexKey {
|
||||
if len(entries) == 0 {
|
||||
return entries
|
||||
}
|
||||
out := make([]CodexKey, 0, len(entries))
|
||||
for i := range entries {
|
||||
e := entries[i]
|
||||
e.Prefix = normalizeModelPrefix(e.Prefix)
|
||||
e.BaseURL = strings.TrimSpace(e.BaseURL)
|
||||
e.Headers = NormalizeHeaders(e.Headers)
|
||||
e.ExcludedModels = NormalizeExcludedModels(e.ExcludedModels)
|
||||
if e.BaseURL == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, e)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// SanitizeClaudeKeys normalizes headers for Claude credentials.
|
||||
func (cfg *Config) SanitizeClaudeKeys() {
|
||||
if cfg == nil || len(cfg.ClaudeKey) == 0 {
|
||||
return
|
||||
}
|
||||
for i := range cfg.ClaudeKey {
|
||||
entry := &cfg.ClaudeKey[i]
|
||||
entry.Prefix = normalizeModelPrefix(entry.Prefix)
|
||||
entry.Headers = NormalizeHeaders(entry.Headers)
|
||||
entry.ExcludedModels = NormalizeExcludedModels(entry.ExcludedModels)
|
||||
// Only a recognized value is rewritten. An unrecognized one is preserved as
|
||||
// written so sanitizing a config file never destroys operator input; the
|
||||
// request path falls back to the default profile and reports it once.
|
||||
if normalized, ok := NormalizeClaudeFingerprintProfile(entry.FingerprintProfile); ok {
|
||||
entry.FingerprintProfile = normalized
|
||||
} else {
|
||||
entry.FingerprintProfile = strings.TrimSpace(entry.FingerprintProfile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sanitizeGeminiKeyEntries(entries []GeminiKey) []GeminiKey {
|
||||
seen := make(map[string]struct{}, len(entries))
|
||||
out := entries[:0]
|
||||
for i := range entries {
|
||||
entry := entries[i]
|
||||
entry.APIKey = strings.TrimSpace(entry.APIKey)
|
||||
entry.BaseURL = strings.TrimSpace(entry.BaseURL)
|
||||
if entry.APIKey == "" && entry.BaseURL == "" {
|
||||
continue
|
||||
}
|
||||
entry.Prefix = normalizeModelPrefix(entry.Prefix)
|
||||
entry.ProxyURL = strings.TrimSpace(entry.ProxyURL)
|
||||
entry.Headers = NormalizeHeaders(entry.Headers)
|
||||
entry.ExcludedModels = NormalizeExcludedModels(entry.ExcludedModels)
|
||||
uniqueKey := formatGeminiKeyDedupID(entry)
|
||||
if _, exists := seen[uniqueKey]; exists {
|
||||
continue
|
||||
}
|
||||
seen[uniqueKey] = struct{}{}
|
||||
out = append(out, entry)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func formatGeminiKeyDedupID(entry GeminiKey) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(entry.APIKey)
|
||||
b.WriteByte(0)
|
||||
b.WriteString(entry.BaseURL)
|
||||
b.WriteByte(0)
|
||||
b.WriteString(entry.ProxyURL)
|
||||
b.WriteByte(0)
|
||||
b.WriteString(entry.Prefix)
|
||||
b.WriteByte(0)
|
||||
b.WriteString(FormatSortedHeaders(entry.Headers))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// FormatSortedHeaders serializes headers deterministically with null byte separators.
|
||||
func FormatSortedHeaders(headers map[string]string) string {
|
||||
if len(headers) == 0 {
|
||||
return ""
|
||||
}
|
||||
keys := make([]string, 0, len(headers))
|
||||
for k := range headers {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
var b strings.Builder
|
||||
for _, k := range keys {
|
||||
b.WriteString(k)
|
||||
b.WriteByte(0)
|
||||
b.WriteString(headers[k])
|
||||
b.WriteByte(0)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// SanitizeGeminiKeys deduplicates and normalizes Gemini credentials.
|
||||
// It uses API key, base URL, proxy URL, prefix, and custom headers as the uniqueness key.
|
||||
func (cfg *Config) SanitizeGeminiKeys() {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
cfg.GeminiKey = sanitizeGeminiKeyEntries(cfg.GeminiKey)
|
||||
}
|
||||
|
||||
// SanitizeInteractionsKeys deduplicates and normalizes native Interactions credentials.
|
||||
// It uses API key, base URL, proxy URL, prefix, and custom headers as the uniqueness key.
|
||||
func (cfg *Config) SanitizeInteractionsKeys() {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
cfg.InteractionsKey = sanitizeGeminiKeyEntries(cfg.InteractionsKey)
|
||||
}
|
||||
|
||||
func normalizeModelPrefix(prefix string) string {
|
||||
trimmed := strings.TrimSpace(prefix)
|
||||
trimmed = strings.Trim(trimmed, "/")
|
||||
if trimmed == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.Contains(trimmed, "/") {
|
||||
return ""
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
// NormalizeHeaders trims header keys and values and removes empty pairs.
|
||||
func NormalizeHeaders(headers map[string]string) map[string]string {
|
||||
if len(headers) == 0 {
|
||||
return nil
|
||||
}
|
||||
clean := make(map[string]string, len(headers))
|
||||
for k, v := range headers {
|
||||
key := strings.TrimSpace(k)
|
||||
val := strings.TrimSpace(v)
|
||||
if key == "" || val == "" {
|
||||
continue
|
||||
}
|
||||
clean[key] = val
|
||||
}
|
||||
if len(clean) == 0 {
|
||||
return nil
|
||||
}
|
||||
return clean
|
||||
}
|
||||
|
||||
// NormalizeExcludedModels trims, lowercases, and deduplicates model exclusion patterns.
|
||||
// It preserves the order of first occurrences and drops empty entries.
|
||||
func NormalizeExcludedModels(models []string) []string {
|
||||
if len(models) == 0 {
|
||||
return nil
|
||||
}
|
||||
seen := make(map[string]struct{}, len(models))
|
||||
out := make([]string, 0, len(models))
|
||||
for _, raw := range models {
|
||||
trimmed := strings.ToLower(strings.TrimSpace(raw))
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[trimmed]; exists {
|
||||
continue
|
||||
}
|
||||
seen[trimmed] = struct{}{}
|
||||
out = append(out, trimmed)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// NormalizeOAuthExcludedModels cleans provider -> excluded models mappings by normalizing provider keys
|
||||
// and applying model exclusion normalization to each entry.
|
||||
func NormalizeOAuthExcludedModels(entries map[string][]string) map[string][]string {
|
||||
if len(entries) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string][]string, len(entries))
|
||||
for provider, models := range entries {
|
||||
key := strings.ToLower(strings.TrimSpace(provider))
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
normalized := NormalizeExcludedModels(models)
|
||||
if len(normalized) == 0 {
|
||||
continue
|
||||
}
|
||||
out[key] = normalized
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
Loading…
Reference in a new issue