Split the monolithic config.go (~120KB) into focused modules: - auth.go: JWT, TOTP, Turnstile, RateLimit configs - billing.go: Billing and Pricing configs - database.go: Database and Redis configs - gateway.go: Gateway and Upstream configs - gateway_sub.go: Gateway sub-configurations - ops_and_cache.go: Ops and Cache configs - platforms.go: Platform-specific configs - security.go: Security-related configs - server.go: Server configuration - config_defaults.go: Default values - config_defaults_detail.go: Detailed defaults - config_helpers.go: Helper functions - config_validate.go: Validation logic - config_validate_gateway.go: Gateway validation This improves: - Code maintainability and readability - Faster compilation (smaller files) - Easier navigation and debugging - Better separation of concerns
160 lines
9.0 KiB
Go
160 lines
9.0 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"strings"
|
|
)
|
|
|
|
func validateGateway(g *GatewayConfig) error {
|
|
if g.MaxBodySize <= 0 { return fmt.Errorf("gateway.max_body_size must be positive") }
|
|
if g.UpstreamResponseReadMaxBytes <= 0 { return fmt.Errorf("upstream_response_read_max_bytes must be positive") }
|
|
if g.ProxyProbeResponseReadMaxBytes <= 0 { return fmt.Errorf("proxy_probe_response_read_max_bytes must be positive") }
|
|
|
|
if strings.TrimSpace(g.ConnectionPoolIsolation) != "" {
|
|
switch g.ConnectionPoolIsolation {
|
|
case ConnectionPoolIsolationProxy, ConnectionPoolIsolationAccount, ConnectionPoolIsolationAccountProxy:
|
|
default: return fmt.Errorf("invalid connection_pool_isolation")
|
|
}
|
|
}
|
|
if g.MaxIdleConns <= 0 || g.MaxIdleConnsPerHost <= 0 || g.MaxConnsPerHost < 0 {
|
|
return fmt.Errorf("gateway connection pool fields invalid")
|
|
}
|
|
if g.IdleConnTimeoutSeconds <= 0 { return fmt.Errorf("idle_conn_timeout_seconds must be positive") }
|
|
if g.IdleConnTimeoutSeconds > 180 { slog.Warn("idle_conn_timeout_seconds is high; consider 60-120") }
|
|
if g.MaxUpstreamClients <= 0 { return fmt.Errorf("max_upstream_clients must be positive") }
|
|
if g.ClientIdleTTLSeconds <= 0 { return fmt.Errorf("client_idle_ttl_seconds must be positive") }
|
|
if g.ConcurrencySlotTTLMinutes <= 0 { return fmt.Errorf("concurrency_slot_ttl_minutes must be positive") }
|
|
|
|
if err := validateGatewayStream(g); err != nil { return err }
|
|
if err := validateGatewayOpenAIWS(&g.OpenAIWS); err != nil { return err }
|
|
if err := validateGatewayUsageRecord(&g.UsageRecord); err != nil { return err }
|
|
if err := validateGatewayScheduling(&g.Scheduling); err != nil { return err }
|
|
if g.UserGroupRateCacheTTLSeconds <= 0 { return fmt.Errorf("user_group_rate_cache_ttl_seconds must be positive") }
|
|
if g.ModelsListCacheTTLSeconds < 10 || g.ModelsListCacheTTLSeconds > 30 {
|
|
return fmt.Errorf("models_list_cache_ttl_seconds must be between 10-30")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateGatewayStream(g *GatewayConfig) error {
|
|
if g.StreamDataIntervalTimeout < 0 { return fmt.Errorf("stream_data_interval_timeout must be non-negative") }
|
|
if g.StreamDataIntervalTimeout != 0 && (g.StreamDataIntervalTimeout < 30 || g.StreamDataIntervalTimeout > 300) {
|
|
return fmt.Errorf("stream_data_interval_timeout must be 0 or between 30-300 seconds")
|
|
}
|
|
if g.StreamKeepaliveInterval < 0 { return fmt.Errorf("stream_keepalive_interval must be non-negative") }
|
|
if g.StreamKeepaliveInterval != 0 && (g.StreamKeepaliveInterval < 5 || g.StreamKeepaliveInterval > 30) {
|
|
return fmt.Errorf("stream_keepalive_interval must be 0 or between 5-30 seconds")
|
|
}
|
|
if g.MaxLineSize < 0 { return fmt.Errorf("max_line_size must be non-negative") }
|
|
if g.MaxLineSize != 0 && g.MaxLineSize < 1024*1024 { return fmt.Errorf("max_line_size must be at least 1MB") }
|
|
return nil
|
|
}
|
|
|
|
func validateGatewayOpenAIWS(ws *GatewayOpenAIWSConfig) error {
|
|
// Basic numeric checks
|
|
checks := []struct{ name string; val int64 }{
|
|
{"max_conns_per_account", int64(ws.MaxConnsPerAccount)},
|
|
{"dial_timeout_seconds", int64(ws.DialTimeoutSeconds)},
|
|
{"read_timeout_seconds", int64(ws.ReadTimeoutSeconds)},
|
|
{"write_timeout_seconds", int64(ws.WriteTimeoutSeconds)},
|
|
{"pool_target_utilization", int64(ws.PoolTargetUtilization * 100)}, // scale for comparison
|
|
{"queue_limit_per_conn", int64(ws.QueueLimitPerConn)},
|
|
{"event_flush_batch_size", int64(ws.EventFlushBatchSize)},
|
|
{"lb_top_k", int64(ws.LBTopK)},
|
|
{"sticky_session_ttl_seconds", int64(ws.StickySessionTTLSeconds)},
|
|
{"sticky_response_id_ttl_seconds", int64(ws.StickyResponseIDTTLSeconds)},
|
|
{"max_conns_per_account", int64(ws.MaxConnsPerAccount)},
|
|
}
|
|
for _, c := range checks {
|
|
if c.val <= 0 { return fmt.Errorf("openai_ws.%s must be positive", c.name) }
|
|
}
|
|
if ws.MinIdlePerAccount < 0 || ws.MaxIdlePerAccount < 0 {
|
|
return fmt.Errorf("openai_ws idle per-account fields must be non-negative")
|
|
}
|
|
if ws.MinIdlePerAccount > ws.MaxIdlePerAccount { return fmt.Errorf("min_idle_per_account must be <= max_idle_per_account") }
|
|
if ws.MaxIdlePerAccount > ws.MaxConnsPerAccount { return fmt.Errorf("max_idle_per_account must be <= max_conns_per_account") }
|
|
if ws.OAuthMaxConnsFactor <= 0 || ws.APIKeyMaxConnsFactor <= 0 {
|
|
return fmt.Errorf("openai_ws conns factor must be positive")
|
|
}
|
|
if ws.PoolTargetUtilization <= 0 || ws.PoolTargetUtilization > 1 {
|
|
return fmt.Errorf("pool_target_utilization must be within (0,1]")
|
|
}
|
|
if ws.EventFlushIntervalMS < 0 || ws.PrewarmCooldownMS < 0 ||
|
|
ws.FallbackCooldownSeconds < 0 || ws.RetryBackoffInitialMS < 0 ||
|
|
ws.RetryBackoffMaxMS < 0 || ws.RetryTotalBudgetMS < 0 {
|
|
return fmt.Errorf("openai_ws timeout/retry fields must be non-negative")
|
|
}
|
|
if ws.RetryBackoffInitialMS > 0 && ws.RetryBackoffMaxMS > 0 && ws.RetryBackoffMaxMS < ws.RetryBackoffInitialMS {
|
|
return fmt.Errorf("retry_backoff_max_ms >= retry_backoff_initial_ms")
|
|
}
|
|
if ws.RetryJitterRatio < 0 || ws.RetryJitterRatio > 1 { return fmt.Errorf("retry_jitter_ratio within [0,1]") }
|
|
if ws.PayloadLogSampleRate < 0 || ws.PayloadLogSampleRate > 1 { return fmt.Errorf("payload_log_sample_rate within [0,1]") }
|
|
if ws.StickyPreviousResponseTTLSeconds < 0 { return fmt.Errorf("sticky_previous_response_ttl_seconds must be non-negative") }
|
|
if ws.SchedulerScoreWeights.Priority < 0 || ws.SchedulerScoreWeights.Load < 0 ||
|
|
ws.SchedulerScoreWeights.Queue < 0 || ws.SchedulerScoreWeights.ErrorRate < 0 || ws.SchedulerScoreWeights.TTFT < 0 {
|
|
return fmt.Errorf("scheduler_score_weights must be non-negative")
|
|
}
|
|
weightSum := ws.SchedulerScoreWeights.Priority + ws.SchedulerScoreWeights.Load + ws.SchedulerScoreWeights.Queue +
|
|
ws.SchedulerScoreWeights.ErrorRate + ws.SchedulerScoreWeights.TTFT
|
|
if weightSum <= 0 { return fmt.Errorf("scheduler_score_weights must not all be zero") }
|
|
// Ingress mode
|
|
if mode := strings.ToLower(strings.TrimSpace(ws.IngressModeDefault)); mode != "" {
|
|
switch mode { case "off", "ctx_pool", "passthrough": default: return fmt.Errorf("ingress_mode_default must be off|ctx_pool|passthrough") }
|
|
}
|
|
if mode := strings.ToLower(strings.TrimSpace(ws.StoreDisabledConnMode)); mode != "" {
|
|
switch mode { case "strict", "adaptive", "off": default: return fmt.Errorf("store_disabled_conn_mode must be strict|adaptive|off") }
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateGatewayUsageRecord(ur *GatewayUsageRecordConfig) error {
|
|
if ur.WorkerCount <= 0 || ur.QueueSize <= 0 || ur.TaskTimeoutSeconds <= 0 {
|
|
return fmt.Errorf("usage_record worker/queue/timeout must be positive")
|
|
}
|
|
switch ur.OverflowPolicy {
|
|
case UsageRecordOverflowPolicyDrop, UsageRecordOverflowPolicySample, UsageRecordOverflowPolicySync:
|
|
default: return fmt.Errorf("invalid overflow_policy")
|
|
}
|
|
if ur.OverflowSamplePercent < 0 || ur.OverflowSamplePercent > 100 {
|
|
return fmt.Errorf("overflow_sample_percent must be 0-100")
|
|
}
|
|
if strings.EqualFold(ur.OverflowPolicy, UsageRecordOverflowPolicySample) && ur.OverflowSamplePercent <= 0 {
|
|
return fmt.Errorf("overflow_sample_percent must be positive when policy=sample")
|
|
}
|
|
if !ur.AutoScaleEnabled { return nil }
|
|
if ur.AutoScaleMinWorkers <= 0 || ur.AutoScaleMaxWorkers <= 0 { return fmt.Errorf("auto_scale workers must be positive") }
|
|
if ur.AutoScaleMaxWorkers < ur.AutoScaleMinWorkers { return fmt.Errorf("auto_scale_max >= auto_scale_min") }
|
|
if ur.WorkerCount < ur.AutoScaleMinWorkers || ur.WorkerCount > ur.AutoScaleMaxWorkers {
|
|
return fmt.Errorf("worker_count between auto_scale_min and max")
|
|
}
|
|
if ur.AutoScaleUpQueuePercent <= 0 || ur.AutoScaleUpQueuePercent > 100 { return fmt.Errorf("auto_scale_up_queue_percent 1-100") }
|
|
if ur.AutoScaleDownQueuePercent < 0 || ur.AutoScaleDownQueuePercent >= 100 { return fmt.Errorf("auto_scale_down_queue_percent 0-99") }
|
|
if ur.AutoScaleDownQueuePercent >= ur.AutoScaleUpQueuePercent { return fmt.Errorf("down_queue_percent < up_queue_percent") }
|
|
if ur.AutoScaleUpStep <= 0 || ur.AutoScaleDownStep <= 0 { return fmt.Errorf("auto_scale steps must be positive") }
|
|
if ur.AutoScaleCheckIntervalSeconds <= 0 { return fmt.Errorf("auto_scale_check_interval_seconds must be positive") }
|
|
if ur.AutoScaleCooldownSeconds < 0 { return fmt.Errorf("auto_scale_cooldown_seconds must be non-negative") }
|
|
return nil
|
|
}
|
|
|
|
func validateGatewayScheduling(s *GatewaySchedulingConfig) error {
|
|
if s.StickySessionMaxWaiting <= 0 || s.StickySessionWaitTimeout <= 0 ||
|
|
s.FallbackWaitTimeout <= 0 || s.FallbackMaxWaiting <= 0 ||
|
|
s.SnapshotMGetChunkSize <= 0 || s.SnapshotWriteChunkSize <= 0 {
|
|
return fmt.Errorf("scheduling core fields must be positive")
|
|
}
|
|
if s.SlotCleanupInterval < 0 || s.DbFallbackTimeoutSeconds < 0 || s.DbFallbackMaxQPS < 0 {
|
|
return fmt.Errorf("scheduling optional fields must be non-negative")
|
|
}
|
|
if s.OutboxPollIntervalSeconds <= 0 || s.OutboxLagRebuildFailures <= 0 || s.OutboxBacklogRebuildRows < 0 {
|
|
return fmt.Errorf("outbox fields must be non-negative or positive as documented")
|
|
}
|
|
if s.OutboxLagWarnSeconds < 0 || s.OutboxLagRebuildSeconds < 0 || s.FullRebuildIntervalSeconds < 0 {
|
|
return fmt.Errorf("outbox timing fields must be non-negative")
|
|
}
|
|
if s.OutboxLagWarnSeconds > 0 && s.OutboxLagRebuildSeconds > 0 && s.OutboxLagRebuildSeconds < s.OutboxLagWarnSeconds {
|
|
return fmt.Errorf("outbox_lag_rebuild_seconds >= outbox_lag_warn_seconds")
|
|
}
|
|
return nil
|
|
}
|