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,6 +8,7 @@ import (
) )
type Config struct { type Config struct {
AppEnv string
HTTPAddr string HTTPAddr string
DatabaseURL string DatabaseURL string
RedisAddr string RedisAddr string
@@ -19,10 +20,20 @@ type Config struct {
CursorSecret string CursorSecret string
FrontendOrigin string FrontendOrigin string
CookieSecure bool 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)
}
}