package config import ( "errors" "fmt" "os" "strings" "github.com/spf13/viper" ) // Config is the root configuration for vecna. type Config struct { Server ServerConfig `mapstructure:"server"` Metrics MetricsConfig `mapstructure:"metrics"` Forward ForwardConfig `mapstructure:"forward"` Adapter AdapterConfig `mapstructure:"adapter"` } // ServerConfig controls the HTTP listener and inbound auth. type ServerConfig struct { Port int `mapstructure:"port"` Host string `mapstructure:"host"` APIKeys []string `mapstructure:"api_keys"` } // MetricsConfig controls Prometheus metrics exposure. type MetricsConfig struct { Enabled bool `mapstructure:"enabled"` Path string `mapstructure:"path"` APIKey string `mapstructure:"api_key"` } // ForwardConfig holds all named forwarding targets. type ForwardConfig struct { Default string `mapstructure:"default"` Targets map[string]ForwardTarget `mapstructure:"targets"` } // ForwardTarget is a named backing embedding model with one or more endpoints. type ForwardTarget struct { Endpoints []EndpointConfig `mapstructure:"endpoints"` Model string `mapstructure:"model"` APIKey string `mapstructure:"api_key"` APIType string `mapstructure:"api_type"` TimeoutSecs int `mapstructure:"timeout_secs"` CooldownSecs int `mapstructure:"cooldown_secs"` PriorityDecay int `mapstructure:"priority_decay"` PriorityRecovery int `mapstructure:"priority_recovery"` } // EndpointConfig is a single URL within a ForwardTarget. type EndpointConfig struct { URL string `mapstructure:"url"` Priority int `mapstructure:"priority"` APIKey string `mapstructure:"api_key"` } // AdapterConfig selects and tunes the dimension adapter. type AdapterConfig struct { Type string `mapstructure:"type"` SourceDim int `mapstructure:"source_dim"` TargetDim int `mapstructure:"target_dim"` TruncateMode string `mapstructure:"truncate_mode"` PadMode string `mapstructure:"pad_mode"` Seed int64 `mapstructure:"seed"` MatrixFile string `mapstructure:"matrix_file"` } // extensions viper will detect automatically. var extensions = []string{"json", "yaml", "toml"} // ResolveFile returns the config file path that would be used by Load. // If cfgFile is non-empty it is returned as-is. // Otherwise the default search paths are checked; if no existing file is found, // the preferred default (~/.vecna.json) is returned so callers can create it. func ResolveFile(cfgFile string) string { if cfgFile != "" { return cfgFile } home, _ := os.UserHomeDir() dirs := []string{".", home, home + "/.config/vecna"} for _, dir := range dirs { for _, ext := range extensions { path := dir + "/vecna." + ext if _, err := os.Stat(path); err == nil { return path } } } return home + "/vecna.json" } // Load reads configuration from the given file path (empty = search defaults), // environment variables (prefix VECNA_), and applies built-in defaults. func Load(cfgFile string) (*Config, error) { v := viper.New() // Defaults v.SetDefault("server.port", 8080) v.SetDefault("server.host", "0.0.0.0") v.SetDefault("metrics.enabled", false) v.SetDefault("metrics.path", "/metrics") v.SetDefault("adapter.type", "truncate") v.SetDefault("adapter.truncate_mode", "from_end") v.SetDefault("adapter.pad_mode", "at_end") // Environment v.SetEnvPrefix("VECNA") v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.AutomaticEnv() // Config file if cfgFile != "" { v.SetConfigFile(cfgFile) } else { home, _ := os.UserHomeDir() v.SetConfigName("vecna") // No SetConfigType — viper detects format from file extension (json, yaml, toml, etc.) v.AddConfigPath(".") v.AddConfigPath(home) v.AddConfigPath(home + "/.config/vecna") } if err := v.ReadInConfig(); err != nil { // Missing config file is acceptable when all required values come from flags/env var notFound viper.ConfigFileNotFoundError if !errors.As(err, ¬Found) { return nil, fmt.Errorf("load config: %w", err) } } var cfg Config if err := v.Unmarshal(&cfg); err != nil { return nil, fmt.Errorf("unmarshal config: %w", err) } applyForwardDefaults(&cfg) return &cfg, nil } // applyForwardDefaults fills in zero-value fields on ForwardTarget entries. func applyForwardDefaults(cfg *Config) { for name, t := range cfg.Forward.Targets { if t.TimeoutSecs == 0 { t.TimeoutSecs = 30 } if t.CooldownSecs == 0 { t.CooldownSecs = 60 } if t.PriorityDecay == 0 { t.PriorityDecay = 2 } if t.PriorityRecovery == 0 { t.PriorityRecovery = 5 } for i, ep := range t.Endpoints { if ep.Priority == 0 { t.Endpoints[i].Priority = 10 } if ep.APIKey == "" && t.APIKey != "" { t.Endpoints[i].APIKey = t.APIKey } } cfg.Forward.Targets[name] = t } }