185 lines
6 KiB
Go
185 lines
6 KiB
Go
package config
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"syscall"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// LoadConfig reads a YAML configuration file from the given path,
|
|
// unmarshals it into a Config struct, applies environment variable overrides,
|
|
// and returns it.
|
|
//
|
|
// Parameters:
|
|
// - configFile: The path to the YAML configuration file
|
|
//
|
|
// Returns:
|
|
// - *Config: The loaded configuration
|
|
// - error: An error if the configuration could not be loaded
|
|
func LoadConfig(configFile string) (*Config, error) {
|
|
return LoadConfigOptional(configFile, false)
|
|
}
|
|
|
|
// LoadConfigOptional reads YAML from configFile.
|
|
// If optional is true and the file is missing, it returns an empty Config.
|
|
// If optional is true and the file is empty or invalid, it returns an empty Config.
|
|
func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
|
|
// Read the entire configuration file into memory.
|
|
data, err := os.ReadFile(configFile)
|
|
if err != nil {
|
|
if optional {
|
|
if os.IsNotExist(err) || errors.Is(err, syscall.EISDIR) {
|
|
// Missing and optional: return empty config (cloud deploy standby).
|
|
cfg := &Config{CredentialInFlight: DefaultCredentialInFlightConfig()}
|
|
cfg.NormalizePluginsConfig()
|
|
return cfg, nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("failed to read config file: %w", err)
|
|
}
|
|
|
|
// In cloud deploy mode (optional=true), if file is empty or contains only whitespace, return empty config.
|
|
if optional && len(bytes.TrimSpace(data)) == 0 {
|
|
cfg := &Config{CredentialInFlight: DefaultCredentialInFlightConfig()}
|
|
cfg.NormalizePluginsConfig()
|
|
return cfg, nil
|
|
}
|
|
|
|
if errValidate := validateCredentialWeightYAML(data); errValidate != nil {
|
|
if optional {
|
|
cfgOptional := &Config{CredentialInFlight: DefaultCredentialInFlightConfig()}
|
|
cfgOptional.NormalizePluginsConfig()
|
|
return cfgOptional, nil
|
|
}
|
|
return nil, errValidate
|
|
}
|
|
|
|
// Unmarshal the YAML data into the Config struct.
|
|
var cfg Config
|
|
// Set defaults before unmarshal so that absent keys keep defaults.
|
|
cfg.Host = "" // Default empty: binds to all interfaces (IPv4 + IPv6)
|
|
cfg.LoggingToFile = false
|
|
cfg.LogsMaxTotalSizeMB = 0
|
|
cfg.ErrorLogsMaxFiles = 10
|
|
cfg.UsageStatisticsEnabled = false
|
|
cfg.RedisUsageQueueRetentionSeconds = 60
|
|
cfg.DisableCooling = false
|
|
cfg.SaveCooldownStatus = false
|
|
cfg.TransientErrorCooldownSeconds = 0
|
|
cfg.DisableImageGeneration = DisableImageGenerationOff
|
|
cfg.WebsocketAuth = true
|
|
cfg.Pprof.Enable = false
|
|
cfg.Pprof.Addr = DefaultPprofAddr
|
|
cfg.CredentialInFlight = DefaultCredentialInFlightConfig()
|
|
if err = yaml.Unmarshal(data, &cfg); err != nil {
|
|
if optional {
|
|
// In cloud deploy mode, if YAML parsing fails, return empty config instead of error.
|
|
cfgOptional := &Config{CredentialInFlight: DefaultCredentialInFlightConfig()}
|
|
cfgOptional.NormalizePluginsConfig()
|
|
return cfgOptional, nil
|
|
}
|
|
return nil, fmt.Errorf("failed to parse config file: %w", err)
|
|
}
|
|
|
|
cfg.CredentialConcurrency = cfg.CredentialConcurrency.WithDefaults()
|
|
if errValidate := cfg.CredentialInFlight.Validate(); errValidate != nil {
|
|
return nil, errValidate
|
|
}
|
|
if errValidate := cfg.Codex.LiveMediaRelay.Validate(); errValidate != nil {
|
|
return nil, errValidate
|
|
}
|
|
if errValidate := cfg.ValidateCredentialWeights(); errValidate != nil {
|
|
return nil, errValidate
|
|
}
|
|
|
|
// Hash remote management key if plaintext is detected (nested)
|
|
// We consider a value to be already hashed if it looks like a bcrypt hash ($2a$, $2b$, or $2y$ prefix).
|
|
if cfg.RemoteManagement.SecretKey != "" && !looksLikeBcrypt(cfg.RemoteManagement.SecretKey) {
|
|
hashed, errHash := hashSecret(cfg.RemoteManagement.SecretKey)
|
|
if errHash != nil {
|
|
return nil, fmt.Errorf("failed to hash remote management key: %w", errHash)
|
|
}
|
|
cfg.RemoteManagement.SecretKey = hashed
|
|
|
|
// Persist the hashed value back to the config file to avoid re-hashing on next startup.
|
|
// Preserve YAML comments and ordering; update only the nested key.
|
|
_ = SaveConfigPreserveCommentsUpdateNestedScalar(configFile, []string{"remote-management", "secret-key"}, hashed)
|
|
}
|
|
|
|
cfg.Pprof.Addr = strings.TrimSpace(cfg.Pprof.Addr)
|
|
if cfg.Pprof.Addr == "" {
|
|
cfg.Pprof.Addr = DefaultPprofAddr
|
|
}
|
|
|
|
if cfg.LogsMaxTotalSizeMB < 0 {
|
|
cfg.LogsMaxTotalSizeMB = 0
|
|
}
|
|
|
|
if cfg.ErrorLogsMaxFiles < 0 {
|
|
cfg.ErrorLogsMaxFiles = 10
|
|
}
|
|
|
|
if cfg.RedisUsageQueueRetentionSeconds <= 0 {
|
|
cfg.RedisUsageQueueRetentionSeconds = 60
|
|
} else if cfg.RedisUsageQueueRetentionSeconds > 3600 {
|
|
log.WithField("value", cfg.RedisUsageQueueRetentionSeconds).Warn("redis-usage-queue-retention-seconds too large; clamping to 3600")
|
|
cfg.RedisUsageQueueRetentionSeconds = 3600
|
|
}
|
|
|
|
if cfg.MaxRetryCredentials < 0 {
|
|
cfg.MaxRetryCredentials = 0
|
|
}
|
|
|
|
cfg.NormalizePluginsConfig()
|
|
if errResolvePluginsDir := cfg.ResolvePluginsDir(); errResolvePluginsDir != nil && cfg.Plugins.Enabled {
|
|
return nil, errResolvePluginsDir
|
|
}
|
|
|
|
// Sanitize Gemini API key configuration and migrate legacy entries.
|
|
cfg.SanitizeGeminiKeys()
|
|
|
|
// Sanitize native Interactions API key configuration.
|
|
cfg.SanitizeInteractionsKeys()
|
|
|
|
// Sanitize Vertex-compatible API keys.
|
|
cfg.SanitizeVertexCompatKeys()
|
|
|
|
// Sanitize Codex keys: drop entries without base-url
|
|
cfg.SanitizeCodexKeys()
|
|
|
|
// Sanitize xAI keys: drop entries without base-url
|
|
cfg.SanitizeXAIKeys()
|
|
|
|
// Sanitize Codex header defaults.
|
|
cfg.SanitizeCodexHeaderDefaults()
|
|
|
|
// Sanitize Claude header defaults.
|
|
cfg.SanitizeClaudeHeaderDefaults()
|
|
|
|
// Sanitize Claude key headers
|
|
cfg.SanitizeClaudeKeys()
|
|
|
|
// Sanitize OpenAI compatibility providers: drop entries without base-url
|
|
cfg.SanitizeOpenAICompatibility()
|
|
|
|
// Normalize OAuth provider model exclusion map.
|
|
cfg.OAuthExcludedModels = NormalizeOAuthExcludedModels(cfg.OAuthExcludedModels)
|
|
|
|
// Normalize global OAuth model name aliases.
|
|
cfg.SanitizeOAuthModelAlias()
|
|
|
|
// Normalize global OAuth request-scoped error rules.
|
|
cfg.SanitizeOAuthRequestScopedErrors()
|
|
|
|
// Validate raw payload rules and drop invalid entries.
|
|
cfg.SanitizePayloadRules()
|
|
|
|
// Return the populated configuration struct.
|
|
return &cfg, nil
|
|
}
|