204 lines
7.3 KiB
Go
204 lines
7.3 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
)
|
|
|
|
const (
|
|
AIProviderAvalAI = "avalai"
|
|
AIProviderAgentRouter = "agentrouter"
|
|
)
|
|
|
|
type Provider struct {
|
|
Name string
|
|
APIKey string
|
|
BaseURL string
|
|
Model string
|
|
}
|
|
|
|
type Config struct {
|
|
AppEnv string
|
|
HTTPAddr string
|
|
DatabaseURL string
|
|
RedisAddr string
|
|
RedisPassword string
|
|
ImportPath string
|
|
DemoUsername string
|
|
DemoPassword string
|
|
SessionSecret string
|
|
CursorSecret string
|
|
FrontendOrigin string
|
|
CookieSecure bool
|
|
AIGrowthEnabled bool
|
|
AICoachEnabled bool
|
|
AIPageInsightsEnabled bool
|
|
AIProvider string
|
|
AvalAIAPIKey string
|
|
AvalAIBaseURL string
|
|
AvalAIModel string
|
|
AgentRouterAPIKey string
|
|
AgentRouterBaseURL string
|
|
AgentRouterModel string
|
|
AIDailyRequestBudget int64
|
|
AIDailyTokenBudget int64
|
|
AIMaxConcurrent int
|
|
AICreatePerMinute int64
|
|
AIUncachedScopesPerDay int64
|
|
AICoachDailyRequestBudget int64
|
|
AICoachDailyTokenBudget int64
|
|
AIPageInsightsDailyRequestBudget int64
|
|
AIPageInsightsDailyTokenBudget int64
|
|
}
|
|
|
|
func Load() (Config, error) {
|
|
cfg := Config{
|
|
AppEnv: value("APP_ENV", "development"),
|
|
HTTPAddr: value("HTTP_ADDR", "127.0.0.1:8080"),
|
|
DatabaseURL: os.Getenv("DATABASE_URL"),
|
|
RedisAddr: value("REDIS_ADDR", "localhost:6379"),
|
|
RedisPassword: os.Getenv("REDIS_PASSWORD"),
|
|
ImportPath: value("IMPORT_PATH", "data/incoming/challenge_data.csv.gz"),
|
|
DemoUsername: value("DEMO_USERNAME", "merchant-0001"),
|
|
DemoPassword: os.Getenv("DEMO_PASSWORD"),
|
|
SessionSecret: os.Getenv("SESSION_SECRET"),
|
|
CursorSecret: os.Getenv("CURSOR_SECRET"),
|
|
FrontendOrigin: value("FRONTEND_ORIGIN", "http://localhost:5173"),
|
|
AIProvider: value("AI_PROVIDER", AIProviderAvalAI),
|
|
AvalAIAPIKey: os.Getenv("AVALAI_API_KEY"),
|
|
AvalAIBaseURL: value("AVALAI_BASE_URL", "https://api.avalai.ir"),
|
|
AvalAIModel: value("AVALAI_MODEL", "gpt-5-mini-2025-08-07"),
|
|
AgentRouterAPIKey: os.Getenv("AGENTROUTER_API_KEY"),
|
|
AgentRouterBaseURL: value("AGENTROUTER_BASE_URL", "https://agentrouter.org/v1"),
|
|
AgentRouterModel: value("AGENTROUTER_MODEL", "gpt-5.6-sol"),
|
|
}
|
|
if cfg.AIProvider != AIProviderAvalAI && cfg.AIProvider != AIProviderAgentRouter {
|
|
return Config{}, fmt.Errorf("config: unsupported AI_PROVIDER %q", cfg.AIProvider)
|
|
}
|
|
|
|
var err error
|
|
cfg.CookieSecure, err = strconv.ParseBool(value("COOKIE_SECURE", "false"))
|
|
if err != nil {
|
|
return Config{}, fmt.Errorf("parsing cookie secure: %w", err)
|
|
}
|
|
cfg.AIGrowthEnabled, err = strconv.ParseBool(value("AI_GROWTH_ENABLED", "false"))
|
|
if err != nil {
|
|
return Config{}, fmt.Errorf("parsing AI growth enabled: %w", err)
|
|
}
|
|
cfg.AICoachEnabled, err = strconv.ParseBool(value("AI_COACH_ENABLED", strconv.FormatBool(cfg.AIGrowthEnabled)))
|
|
if err != nil {
|
|
return Config{}, fmt.Errorf("parsing AI coach enabled: %w", err)
|
|
}
|
|
cfg.AIPageInsightsEnabled, err = strconv.ParseBool(value("AI_PAGE_INSIGHTS_ENABLED", strconv.FormatBool(cfg.AIGrowthEnabled)))
|
|
if err != nil {
|
|
return Config{}, fmt.Errorf("parsing AI page insights enabled: %w", err)
|
|
}
|
|
cfg.AIDailyRequestBudget, err = parseInt64("AI_DAILY_REQUEST_BUDGET", 0)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.AIDailyTokenBudget, err = parseInt64("AI_DAILY_TOKEN_BUDGET", 0)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.AICoachDailyRequestBudget, err = parseInt64("AI_COACH_DAILY_REQUEST_BUDGET", cfg.AIDailyRequestBudget)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.AICoachDailyTokenBudget, err = parseInt64("AI_COACH_DAILY_TOKEN_BUDGET", cfg.AIDailyTokenBudget)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.AIPageInsightsDailyRequestBudget, err = parseInt64("AI_PAGE_INSIGHTS_DAILY_REQUEST_BUDGET", cfg.AIDailyRequestBudget)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.AIPageInsightsDailyTokenBudget, err = parseInt64("AI_PAGE_INSIGHTS_DAILY_TOKEN_BUDGET", cfg.AIDailyTokenBudget)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
maxConcurrent, err := parseInt64("AI_MAX_CONCURRENT", 4)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.AIMaxConcurrent = int(maxConcurrent)
|
|
cfg.AICreatePerMinute, err = parseInt64("AI_CREATE_PER_MINUTE", 10)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.AIUncachedScopesPerDay, err = parseInt64("AI_UNCACHED_SCOPES_PER_DAY", 5)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
if cfg.DatabaseURL == "" {
|
|
return Config{}, errors.New("config: DATABASE_URL is required")
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func (c Config) ValidateAPI() error {
|
|
if len(c.SessionSecret) < 32 {
|
|
return errors.New("config: SESSION_SECRET must contain at least 32 characters")
|
|
}
|
|
if len(c.CursorSecret) < 32 {
|
|
return errors.New("config: CURSOR_SECRET must contain at least 32 characters")
|
|
}
|
|
if c.CursorSecret == c.SessionSecret {
|
|
return errors.New("config: CURSOR_SECRET must differ from SESSION_SECRET")
|
|
}
|
|
if c.AIMaxConcurrent < 1 || c.AICreatePerMinute < 1 || c.AIUncachedScopesPerDay < 1 {
|
|
return errors.New("config: AI limits must be positive")
|
|
}
|
|
if c.AIProvider != "" && c.AIProvider != AIProviderAvalAI && c.AIProvider != AIProviderAgentRouter {
|
|
return errors.New("config: unsupported AI provider")
|
|
}
|
|
if c.AppEnv == "production" && (c.AIGrowthEnabled || c.AICoachEnabled || c.AIPageInsightsEnabled) {
|
|
provider := c.SelectedAIProvider()
|
|
if provider.APIKey == "" || provider.Model == "" {
|
|
return errors.New("config: enabled production AI requires provider API key and model")
|
|
}
|
|
if c.AIGrowthEnabled && (c.AIDailyRequestBudget < 1 || c.AIDailyTokenBudget < 1) {
|
|
return errors.New("config: enabled production AI requires positive daily budgets")
|
|
}
|
|
if c.AICoachEnabled && (c.AICoachDailyRequestBudget < 1 || c.AICoachDailyTokenBudget < 1) {
|
|
return errors.New("config: enabled production coach requires positive coach budgets")
|
|
}
|
|
if c.AIPageInsightsEnabled && (c.AIPageInsightsDailyRequestBudget < 1 || c.AIPageInsightsDailyTokenBudget < 1) {
|
|
return errors.New("config: enabled production page insights require positive page-insight budgets")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c Config) SelectedAIProvider() Provider {
|
|
if c.AIProvider == AIProviderAgentRouter {
|
|
return Provider{Name: AIProviderAgentRouter, APIKey: c.AgentRouterAPIKey, BaseURL: c.AgentRouterBaseURL, Model: c.AgentRouterModel}
|
|
}
|
|
return Provider{Name: AIProviderAvalAI, APIKey: c.AvalAIAPIKey, BaseURL: c.AvalAIBaseURL, Model: c.AvalAIModel}
|
|
}
|
|
|
|
func (c Config) ValidateSeed() error {
|
|
if len(c.DemoPassword) < 12 {
|
|
return errors.New("config: DEMO_PASSWORD must contain at least 12 characters")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func value(name, fallback string) string {
|
|
if value := os.Getenv(name); value != "" {
|
|
return value
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func parseInt64(name string, fallback int64) (int64, error) {
|
|
raw := value(name, strconv.FormatInt(fallback, 10))
|
|
parsed, err := strconv.ParseInt(raw, 10, 64)
|
|
if err != nil || parsed < 0 {
|
|
return 0, fmt.Errorf("config: %s must be a non-negative integer", name)
|
|
}
|
|
return parsed, nil
|
|
}
|