| package config |
|
|
| import ( |
| "bytes" |
| "encoding/json" |
| "strings" |
|
|
| log "github.com/sirupsen/logrus" |
| "golang.org/x/crypto/bcrypt" |
| ) |
|
|
| |
| func (cfg *Config) SanitizePayloadRules() { |
| if cfg == nil { |
| return |
| } |
| cfg.Payload.DefaultRaw = sanitizePayloadRawRules(cfg.Payload.DefaultRaw, "default-raw") |
| cfg.Payload.OverrideRaw = sanitizePayloadRawRules(cfg.Payload.OverrideRaw, "override-raw") |
| } |
|
|
| func sanitizePayloadRawRules(rules []PayloadRule, section string) []PayloadRule { |
| if len(rules) == 0 { |
| return rules |
| } |
| out := make([]PayloadRule, 0, len(rules)) |
| for i := range rules { |
| rule := rules[i] |
| if len(rule.Params) == 0 { |
| continue |
| } |
| invalid := false |
| for path, value := range rule.Params { |
| raw, ok := payloadRawString(value) |
| if !ok { |
| continue |
| } |
| trimmed := bytes.TrimSpace(raw) |
| if len(trimmed) == 0 || !json.Valid(trimmed) { |
| log.WithFields(log.Fields{ |
| "section": section, |
| "rule_index": i + 1, |
| "param": path, |
| }).Warn("payload rule dropped: invalid raw JSON") |
| invalid = true |
| break |
| } |
| } |
| if invalid { |
| continue |
| } |
| out = append(out, rule) |
| } |
| return out |
| } |
|
|
| func payloadRawString(value any) ([]byte, bool) { |
| switch typed := value.(type) { |
| case string: |
| return []byte(typed), true |
| case []byte: |
| return typed, true |
| default: |
| return nil, false |
| } |
| } |
|
|
| |
| |
| |
| 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}) |
| } |
| if len(clean) > 0 { |
| out[channel] = clean |
| } |
| } |
| cfg.OAuthModelAlias = out |
| } |
|
|
| |
| |
| |
| 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 == "" { |
| |
| continue |
| } |
| out = append(out, e) |
| } |
| cfg.OpenAICompatibility = out |
| } |
|
|
| |
| |
| func (cfg *Config) SanitizeCodexKeys() { |
| if cfg == nil || len(cfg.CodexKey) == 0 { |
| return |
| } |
| out := make([]CodexKey, 0, len(cfg.CodexKey)) |
| for i := range cfg.CodexKey { |
| e := cfg.CodexKey[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) |
| } |
| cfg.CodexKey = out |
| } |
|
|
|
|
| |
| 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) |
| entry.BaseURL = strings.TrimSpace(entry.BaseURL) |
| entry.ProxyURL = strings.TrimSpace(entry.ProxyURL) |
| entry.APIKey = strings.TrimSpace(entry.APIKey) |
| |
| for j := range entry.APIKeyEntries { |
| apiKeyEntry := &entry.APIKeyEntries[j] |
| apiKeyEntry.APIKey = strings.TrimSpace(apiKeyEntry.APIKey) |
| apiKeyEntry.ProxyURL = strings.TrimSpace(apiKeyEntry.ProxyURL) |
| } |
| } |
| } |
|
|
| |
| func (cfg *Config) SanitizeGeminiKeys() { |
| if cfg == nil { |
| return |
| } |
|
|
| seen := make(map[string]struct{}, len(cfg.GeminiKey)) |
| out := cfg.GeminiKey[:0] |
| for i := range cfg.GeminiKey { |
| entry := cfg.GeminiKey[i] |
| entry.APIKey = strings.TrimSpace(entry.APIKey) |
| if entry.APIKey == "" { |
| continue |
| } |
| entry.Prefix = normalizeModelPrefix(entry.Prefix) |
| entry.BaseURL = strings.TrimSpace(entry.BaseURL) |
| entry.ProxyURL = strings.TrimSpace(entry.ProxyURL) |
| entry.Headers = NormalizeHeaders(entry.Headers) |
| entry.ExcludedModels = NormalizeExcludedModels(entry.ExcludedModels) |
| if _, exists := seen[entry.APIKey]; exists { |
| continue |
| } |
| seen[entry.APIKey] = struct{}{} |
| out = append(out, entry) |
| } |
| cfg.GeminiKey = out |
| } |
|
|
| func normalizeModelPrefix(prefix string) string { |
| trimmed := strings.TrimSpace(prefix) |
| trimmed = strings.Trim(trimmed, "/") |
| if trimmed == "" { |
| return "" |
| } |
| if strings.Contains(trimmed, "/") { |
| return "" |
| } |
| return trimmed |
| } |
|
|
| func syncInlineAccessProvider(cfg *Config) { |
| if cfg == nil { |
| return |
| } |
| if len(cfg.APIKeys) == 0 { |
| if provider := cfg.ConfigAPIKeyProvider(); provider != nil && len(provider.APIKeys) > 0 { |
| cfg.APIKeys = append([]string(nil), provider.APIKeys...) |
| } |
| } |
| cfg.Access.Providers = nil |
| } |
|
|
| |
| func looksLikeBcrypt(s string) bool { |
| return len(s) > 4 && (s[:4] == "$2a$" || s[:4] == "$2b$" || s[:4] == "$2y$") |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| |
| 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 |
| } |
|
|
| |
| |
| 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 |
| } |
|
|
| |
| func hashSecret(secret string) (string, error) { |
| |
| hashedBytes, err := bcrypt.GenerateFromPassword([]byte(secret), bcrypt.DefaultCost) |
| if err != nil { |
| return "", err |
| } |
| return string(hashedBytes), nil |
| } |
|
|
| func sanitizeConfigForPersist(cfg *Config) *Config { |
| if cfg == nil { |
| return nil |
| } |
| clone := *cfg |
| clone.SDKConfig = cfg.SDKConfig |
| clone.SDKConfig.Access = AccessConfig{} |
| return &clone |
| } |
|
|