Files
vecna/pkg/config/config.go
Hein 5c070e441e feat(docker): add configurable path for vecna.json
* set default config path to /config
* update docker-compose example for config usage
* modify config resolution to include /config directory
2026-04-11 20:48:21 +02:00

170 lines
4.9 KiB
Go

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{"/config", ".", 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("/config")
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, &notFound) {
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
}
}