mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-09-03 14:39:26 +08:00
Align the remaining measured OAuth wire profiles, including the ordered connection writer in internal/httpwire that reproduces the observed header sequence, and the refresh/profile response shapes in internal/auth/claude. Replay the measured Fast path and keep diagnostic continuity across cloaked and native requests. Preserve the native direct token-counting shape so a caller that reaches count_tokens itself is not reshaped into the cloaked form. Scope cloak dates to the credential's timezone rather than the host's, so currentDate matches what the real client would have sent for that account.
302 lines
8.8 KiB
Go
302 lines
8.8 KiB
Go
package config
|
|
|
|
import (
|
|
"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
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
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)
|
|
uniqueKey := entry.APIKey + "|" + entry.BaseURL
|
|
if _, exists := seen[uniqueKey]; exists {
|
|
continue
|
|
}
|
|
seen[uniqueKey] = struct{}{}
|
|
out = append(out, entry)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// SanitizeGeminiKeys deduplicates and normalizes Gemini credentials.
|
|
// It uses API key + base URL 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 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
|
|
}
|