feat: add cost-safe AI growth configuration

This commit is contained in:
2026-08-21 00:25:59 +03:30
parent 1a00e897ca
commit 6df21d4fd0
2 changed files with 93 additions and 11 deletions

View File

@@ -8,21 +8,32 @@ import (
) )
type Config struct { type Config struct {
HTTPAddr string AppEnv string
DatabaseURL string HTTPAddr string
RedisAddr string DatabaseURL string
RedisPassword string RedisAddr string
ImportPath string RedisPassword string
DemoUsername string ImportPath string
DemoPassword string DemoUsername string
SessionSecret string DemoPassword string
CursorSecret string SessionSecret string
FrontendOrigin string CursorSecret string
CookieSecure bool FrontendOrigin string
CookieSecure bool
AIGrowthEnabled bool
AvalAIAPIKey string
AvalAIBaseURL string
AvalAIModel string
AIDailyRequestBudget int64
AIDailyTokenBudget int64
AIMaxConcurrent int
AICreatePerMinute int64
AIUncachedScopesPerDay int64
} }
func Load() (Config, error) { func Load() (Config, error) {
cfg := Config{ cfg := Config{
AppEnv: value("APP_ENV", "development"),
HTTPAddr: value("HTTP_ADDR", "127.0.0.1:8080"), HTTPAddr: value("HTTP_ADDR", "127.0.0.1:8080"),
DatabaseURL: os.Getenv("DATABASE_URL"), DatabaseURL: os.Getenv("DATABASE_URL"),
RedisAddr: value("REDIS_ADDR", "localhost:6379"), RedisAddr: value("REDIS_ADDR", "localhost:6379"),
@@ -33,6 +44,9 @@ func Load() (Config, error) {
SessionSecret: os.Getenv("SESSION_SECRET"), SessionSecret: os.Getenv("SESSION_SECRET"),
CursorSecret: os.Getenv("CURSOR_SECRET"), CursorSecret: os.Getenv("CURSOR_SECRET"),
FrontendOrigin: value("FRONTEND_ORIGIN", "http://localhost:5173"), FrontendOrigin: value("FRONTEND_ORIGIN", "http://localhost:5173"),
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"),
} }
var err error var err error
@@ -40,6 +54,31 @@ func Load() (Config, error) {
if err != nil { if err != nil {
return Config{}, fmt.Errorf("parsing cookie secure: %w", err) 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.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
}
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 == "" { if cfg.DatabaseURL == "" {
return Config{}, errors.New("config: DATABASE_URL is required") return Config{}, errors.New("config: DATABASE_URL is required")
} }
@@ -56,6 +95,17 @@ func (c Config) ValidateAPI() error {
if c.CursorSecret == c.SessionSecret { if c.CursorSecret == c.SessionSecret {
return errors.New("config: CURSOR_SECRET must differ from SESSION_SECRET") 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.AppEnv == "production" && c.AIGrowthEnabled {
if c.AvalAIAPIKey == "" || c.AvalAIModel == "" {
return errors.New("config: enabled production AI requires AVALAI_API_KEY and AVALAI_MODEL")
}
if c.AIDailyRequestBudget < 1 || c.AIDailyTokenBudget < 1 {
return errors.New("config: enabled production AI requires positive daily budgets")
}
}
return nil return nil
} }
@@ -72,3 +122,12 @@ func value(name, fallback string) string {
} }
return fallback 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
}

View File

@@ -33,3 +33,26 @@ func TestValidateAPIRequiresIndependentCursorSecret(t *testing.T) {
t.Fatal("ValidateAPI() error = nil, want independent secrets error") t.Fatal("ValidateAPI() error = nil, want independent secrets error")
} }
} }
func TestValidateAPIRequiresGrowthBudgetsInProduction(t *testing.T) {
t.Parallel()
cfg := Config{SessionSecret: "session-secret-that-is-at-least-32-bytes", CursorSecret: "cursor-secret-that-is-at-least-32-bytes", AppEnv: "production", AIGrowthEnabled: true, AvalAIAPIKey: "key", AvalAIModel: "model", AIMaxConcurrent: 4, AICreatePerMinute: 10, AIUncachedScopesPerDay: 5}
if err := cfg.ValidateAPI(); err == nil {
t.Fatal("ValidateAPI() error = nil, want AI budget error")
}
cfg.AIDailyRequestBudget, cfg.AIDailyTokenBudget = 10, 10_000
if err := cfg.ValidateAPI(); err != nil {
t.Fatalf("ValidateAPI() error = %v", err)
}
}
func TestLoadGrowthDefaultsAreCostSafe(t *testing.T) {
t.Setenv("DATABASE_URL", "postgresql://example")
cfg, err := Load()
if err != nil {
t.Fatal(err)
}
if cfg.AIGrowthEnabled || cfg.AIDailyRequestBudget != 0 || cfg.AIDailyTokenBudget != 0 || cfg.AIMaxConcurrent != 4 {
t.Fatalf("unsafe AI defaults: %+v", cfg)
}
}