package config import ( "fmt" "strings" "github.com/spf13/viper" ) // Config holds all application configuration. // Values are loaded from config.yaml then overridden by env vars prefixed with VEYLANT_. // Example: VEYLANT_SERVER_PORT=9090 overrides server.port. type Config struct { Server ServerConfig `mapstructure:"server"` Database DatabaseConfig `mapstructure:"database"` Redis RedisConfig `mapstructure:"redis"` Auth AuthConfig `mapstructure:"auth"` PII PIIConfig `mapstructure:"pii"` Log LogConfig `mapstructure:"log"` Providers ProvidersConfig `mapstructure:"providers"` RBAC RBACConfig `mapstructure:"rbac"` Metrics MetricsConfig `mapstructure:"metrics"` Routing RoutingConfig `mapstructure:"routing"` ClickHouse ClickHouseConfig `mapstructure:"clickhouse"` Crypto CryptoConfig `mapstructure:"crypto"` RateLimit RateLimitConfig `mapstructure:"rate_limit"` } // RateLimitConfig holds default rate limiting parameters applied to all tenants // that have no explicit per-tenant override in the rate_limit_configs table. type RateLimitConfig struct { // DefaultTenantRPM is the default tenant-wide requests per minute limit. DefaultTenantRPM int `mapstructure:"default_tenant_rpm"` // DefaultTenantBurst is the maximum burst size for a tenant bucket. DefaultTenantBurst int `mapstructure:"default_tenant_burst"` // DefaultUserRPM is the default per-user requests per minute limit within a tenant. DefaultUserRPM int `mapstructure:"default_user_rpm"` // DefaultUserBurst is the maximum burst size for a per-user bucket. DefaultUserBurst int `mapstructure:"default_user_burst"` } // ClickHouseConfig holds ClickHouse connection settings for the audit log. type ClickHouseConfig struct { DSN string `mapstructure:"dsn"` // clickhouse://user:pass@host:9000/db MaxConns int `mapstructure:"max_conns"` DialTimeoutSec int `mapstructure:"dial_timeout_seconds"` } // CryptoConfig holds cryptographic settings. type CryptoConfig struct { // AESKeyBase64 is a base64-encoded 32-byte key for AES-256-GCM prompt encryption. // Set via env var VEYLANT_CRYPTO_AES_KEY_BASE64 — never hardcode. AESKeyBase64 string `mapstructure:"aes_key_base64"` } // RoutingConfig controls the intelligent routing engine behaviour. type RoutingConfig struct { // CacheTTLSeconds is how long routing rules are cached per tenant before // a background refresh. 0 means use the default (30s). CacheTTLSeconds int `mapstructure:"cache_ttl_seconds"` } // ProvidersConfig holds configuration for all LLM provider adapters. type ProvidersConfig struct { OpenAI OpenAIConfig `mapstructure:"openai"` Anthropic AnthropicConfig `mapstructure:"anthropic"` Azure AzureConfig `mapstructure:"azure"` Mistral MistralConfig `mapstructure:"mistral"` Ollama OllamaConfig `mapstructure:"ollama"` } // OpenAIConfig holds OpenAI adapter configuration. type OpenAIConfig struct { APIKey string `mapstructure:"api_key"` BaseURL string `mapstructure:"base_url"` TimeoutSeconds int `mapstructure:"timeout_seconds"` MaxConns int `mapstructure:"max_conns"` } // AnthropicConfig holds Anthropic adapter configuration. type AnthropicConfig struct { APIKey string `mapstructure:"api_key"` BaseURL string `mapstructure:"base_url"` Version string `mapstructure:"version"` // Anthropic API version header, e.g. "2023-06-01" TimeoutSeconds int `mapstructure:"timeout_seconds"` MaxConns int `mapstructure:"max_conns"` } // AzureConfig holds Azure OpenAI adapter configuration. type AzureConfig struct { APIKey string `mapstructure:"api_key"` ResourceName string `mapstructure:"resource_name"` // e.g. "my-azure-resource" DeploymentID string `mapstructure:"deployment_id"` // e.g. "gpt-4o" APIVersion string `mapstructure:"api_version"` // e.g. "2024-02-01" TimeoutSeconds int `mapstructure:"timeout_seconds"` MaxConns int `mapstructure:"max_conns"` } // MistralConfig holds Mistral AI adapter configuration (OpenAI-compatible). type MistralConfig struct { APIKey string `mapstructure:"api_key"` BaseURL string `mapstructure:"base_url"` TimeoutSeconds int `mapstructure:"timeout_seconds"` MaxConns int `mapstructure:"max_conns"` } // OllamaConfig holds Ollama adapter configuration (OpenAI-compatible, local). type OllamaConfig struct { BaseURL string `mapstructure:"base_url"` TimeoutSeconds int `mapstructure:"timeout_seconds"` MaxConns int `mapstructure:"max_conns"` } // RBACConfig holds role-based access control settings for the provider router. type RBACConfig struct { // UserAllowedModels lists models accessible to the "user" role (exact or prefix match). UserAllowedModels []string `mapstructure:"user_allowed_models"` // AuditorCanComplete controls whether auditors can make chat completions. // Defaults to false — auditors receive 403 on POST /v1/chat/completions. AuditorCanComplete bool `mapstructure:"auditor_can_complete"` } // MetricsConfig holds Prometheus metrics configuration. type MetricsConfig struct { Enabled bool `mapstructure:"enabled"` Path string `mapstructure:"path"` } type ServerConfig struct { Port int `mapstructure:"port"` ShutdownTimeout int `mapstructure:"shutdown_timeout_seconds"` Env string `mapstructure:"env"` // development, staging, production TenantName string `mapstructure:"tenant_name"` // display name used in PDF reports AllowedOrigins []string `mapstructure:"allowed_origins"` // CORS allowed origins for the React dashboard } type DatabaseConfig struct { URL string `mapstructure:"url"` MaxOpenConns int `mapstructure:"max_open_conns"` MaxIdleConns int `mapstructure:"max_idle_conns"` MigrationsPath string `mapstructure:"migrations_path"` } type RedisConfig struct { URL string `mapstructure:"url"` } // AuthConfig holds local JWT authentication settings. // Override via VEYLANT_AUTH_JWT_SECRET and VEYLANT_AUTH_JWT_TTL_HOURS. type AuthConfig struct { JWTSecret string `mapstructure:"jwt_secret"` JWTTTLHours int `mapstructure:"jwt_ttl_hours"` } type PIIConfig struct { Enabled bool `mapstructure:"enabled"` ServiceAddr string `mapstructure:"service_addr"` // gRPC address, e.g. localhost:50051 TimeoutMs int `mapstructure:"timeout_ms"` FailOpen bool `mapstructure:"fail_open"` // if true, pass request through on PII service error } type LogConfig struct { Level string `mapstructure:"level"` // debug, info, warn, error Format string `mapstructure:"format"` // json, console } // Load reads configuration from config.yaml (searched in . and ./config) // and overrides with environment variables prefixed VEYLANT_. func Load() (*Config, error) { v := viper.New() v.SetConfigName("config") v.SetConfigType("yaml") v.AddConfigPath(".") v.AddConfigPath("./config") // Env var overrides: VEYLANT_SERVER_PORT → server.port v.SetEnvPrefix("VEYLANT") v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.AutomaticEnv() // Defaults v.SetDefault("server.port", 8090) v.SetDefault("server.shutdown_timeout_seconds", 30) v.SetDefault("server.env", "development") v.SetDefault("server.allowed_origins", []string{"http://localhost:3000"}) v.SetDefault("database.max_open_conns", 25) v.SetDefault("database.max_idle_conns", 5) v.SetDefault("database.migrations_path", "migrations") v.SetDefault("auth.jwt_secret", "change-me-in-production-use-VEYLANT_AUTH_JWT_SECRET") v.SetDefault("auth.jwt_ttl_hours", 24) v.SetDefault("pii.enabled", false) v.SetDefault("pii.service_addr", "localhost:50051") v.SetDefault("pii.timeout_ms", 100) v.SetDefault("pii.fail_open", true) v.SetDefault("log.level", "info") v.SetDefault("log.format", "json") v.SetDefault("providers.openai.base_url", "https://api.openai.com/v1") v.SetDefault("providers.openai.timeout_seconds", 30) v.SetDefault("providers.openai.max_conns", 100) v.SetDefault("providers.anthropic.base_url", "https://api.anthropic.com/v1") v.SetDefault("providers.anthropic.version", "2023-06-01") v.SetDefault("providers.anthropic.timeout_seconds", 30) v.SetDefault("providers.anthropic.max_conns", 100) v.SetDefault("providers.azure.api_version", "2024-02-01") v.SetDefault("providers.azure.timeout_seconds", 30) v.SetDefault("providers.azure.max_conns", 100) v.SetDefault("providers.mistral.base_url", "https://api.mistral.ai/v1") v.SetDefault("providers.mistral.timeout_seconds", 30) v.SetDefault("providers.mistral.max_conns", 100) v.SetDefault("providers.ollama.base_url", "http://localhost:11434/v1") v.SetDefault("providers.ollama.timeout_seconds", 120) v.SetDefault("providers.ollama.max_conns", 10) v.SetDefault("rbac.user_allowed_models", []string{"gpt-4o-mini", "gpt-3.5-turbo", "mistral-small"}) v.SetDefault("rbac.auditor_can_complete", false) v.SetDefault("metrics.enabled", true) v.SetDefault("metrics.path", "/metrics") v.SetDefault("routing.cache_ttl_seconds", 30) v.SetDefault("clickhouse.max_conns", 10) v.SetDefault("clickhouse.dial_timeout_seconds", 5) v.SetDefault("rate_limit.default_tenant_rpm", 1000) v.SetDefault("rate_limit.default_tenant_burst", 200) v.SetDefault("rate_limit.default_user_rpm", 100) v.SetDefault("rate_limit.default_user_burst", 20) if err := v.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); !ok { return nil, fmt.Errorf("reading config: %w", err) } // Config file not found — rely on defaults and env vars only } var cfg Config if err := v.Unmarshal(&cfg); err != nil { return nil, fmt.Errorf("unmarshaling config: %w", err) } return &cfg, nil }