Gin + GORM + pure-Go SQLite. Users/auth (JWT), API key management with quotas, proxy gateway with weighted channel failover and health checks, usage/billing ledger, cross-protocol conversion (Anthropic Messages / OpenAI Chat Completions / OpenAI Responses), and channel/model admin API. Channels declare native API formats and auto-convert the rest. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
144 lines
4.0 KiB
Go
144 lines
4.0 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Config is the runtime configuration, loaded from environment variables.
|
|
type Config struct {
|
|
Env string
|
|
Debug bool
|
|
|
|
HTTPPort string
|
|
PublicBase string // external base URL, used for cookies
|
|
AllowOrigins []string
|
|
|
|
DB struct {
|
|
Driver string // "sqlite" (dev default) or "postgres"
|
|
DSN string
|
|
}
|
|
|
|
Redis struct {
|
|
Addr string
|
|
Password string
|
|
Enabled bool
|
|
}
|
|
|
|
Auth struct {
|
|
AccessTokenTTL time.Duration
|
|
RefreshTokenTTL time.Duration
|
|
JWTSecret string
|
|
RefreshCookieName string
|
|
RefreshCookieSecure bool // false for local http dev; set true behind TLS
|
|
RefreshCookieSameSite string
|
|
}
|
|
|
|
// MasterKey encrypts channel API keys at rest (AES-GCM).
|
|
MasterKey string
|
|
|
|
Proxy struct {
|
|
DefaultMaxTokens int
|
|
DefaultTimeoutMs int
|
|
BillingExactBalance bool // reject when estimated cost > balance
|
|
MaxRetries int // additional channel attempts on transport/5xx failures
|
|
}
|
|
|
|
Registration struct {
|
|
Mode string // "open" | "invite"
|
|
}
|
|
|
|
HealthCheck struct {
|
|
Interval time.Duration
|
|
MaxFailures int
|
|
Cooldown time.Duration
|
|
TimeoutMs int
|
|
TestModel string
|
|
}
|
|
|
|
RateLimit struct {
|
|
RequestsPerMin int // per user global limiter
|
|
Burst int
|
|
}
|
|
|
|
MetricsEnabled bool
|
|
}
|
|
|
|
func Load() *Config {
|
|
c := &Config{}
|
|
c.Env = get("APP_ENV", "development")
|
|
c.Debug = strings.EqualFold(get("DEBUG", "false"), "true")
|
|
c.HTTPPort = get("HTTP_PORT", "8080")
|
|
c.PublicBase = get("PUBLIC_BASE", "http://localhost:8080")
|
|
if o := get("CORS_ORIGINS", "*"); o != "*" {
|
|
c.AllowOrigins = strings.Split(o, ",")
|
|
} else {
|
|
c.AllowOrigins = []string{"*"}
|
|
}
|
|
|
|
c.DB.Driver = get("DB_DRIVER", "sqlite")
|
|
if c.DB.Driver == "postgres" {
|
|
c.DB.DSN = get("DATABASE_URL", "host=localhost user=postgres password=postgres dbname=openteam port=5432 sslmode=disable")
|
|
} else {
|
|
path := get("SQLITE_PATH", "data/openteam.db")
|
|
c.DB.DSN = path
|
|
}
|
|
|
|
c.Redis.Addr = get("REDIS_ADDR", "localhost:6379")
|
|
c.Redis.Password = get("REDIS_PASSWORD", "")
|
|
c.Redis.Enabled = strings.EqualFold(get("REDIS_ENABLED", "false"), "true")
|
|
|
|
c.Auth.AccessTokenTTL = duration(get("ACCESS_TOKEN_TTL", "2h"), 2*time.Hour)
|
|
c.Auth.RefreshTokenTTL = duration(get("REFRESH_TOKEN_TTL", "168h"), 7*24*time.Hour)
|
|
c.Auth.JWTSecret = get("JWT_SECRET", "dev-only-secret-change-me")
|
|
c.Auth.RefreshCookieName = get("REFRESH_COOKIE_NAME", "ot_refresh")
|
|
c.Auth.RefreshCookieSecure = strings.EqualFold(get("REFRESH_COOKIE_SECURE", "false"), "true")
|
|
c.Auth.RefreshCookieSameSite = get("REFRESH_COOKIE_SAMESITE", "lax")
|
|
|
|
c.MasterKey = get("MASTER_KEY", "dev-only-master-key-change-me")
|
|
|
|
c.Proxy.DefaultMaxTokens = intVal(get("DEFAULT_MAX_TOKENS", "4096"), 4096)
|
|
c.Proxy.DefaultTimeoutMs = intVal(get("PROXY_TIMEOUT_MS", "300000"), 300000)
|
|
c.Proxy.BillingExactBalance = strings.EqualFold(get("BILLING_EXACT_BALANCE", "false"), "true")
|
|
c.Proxy.MaxRetries = intVal(get("PROXY_MAX_RETRIES", "1"), 1)
|
|
|
|
c.Registration.Mode = get("REGISTRATION_MODE", "open")
|
|
|
|
c.HealthCheck.Interval = duration(get("HEALTHCHECK_INTERVAL", "60s"), time.Minute)
|
|
c.HealthCheck.MaxFailures = intVal(get("HEALTHCHECK_MAX_FAILURES", "3"), 3)
|
|
c.HealthCheck.Cooldown = duration(get("HEALTHCHECK_COOLDOWN", "300s"), 5*time.Minute)
|
|
c.HealthCheck.TimeoutMs = intVal(get("HEALTHCHECK_TIMEOUT_MS", "15000"), 15000)
|
|
c.HealthCheck.TestModel = get("HEALTHCHECK_TEST_MODEL", "")
|
|
|
|
c.RateLimit.RequestsPerMin = intVal(get("RATE_LIMIT_PER_MIN", "60"), 60)
|
|
c.RateLimit.Burst = intVal(get("RATE_LIMIT_BURST", "120"), 120)
|
|
|
|
c.MetricsEnabled = strings.EqualFold(get("METRICS_ENABLED", "false"), "true")
|
|
return c
|
|
}
|
|
|
|
func get(key, def string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return def
|
|
}
|
|
|
|
func intVal(s string, def int) int {
|
|
n := 0
|
|
if _, err := fmt.Sscanf(s, "%d", &n); err != nil || n <= 0 {
|
|
return def
|
|
}
|
|
return n
|
|
}
|
|
|
|
func duration(s string, def time.Duration) time.Duration {
|
|
d, err := time.ParseDuration(s)
|
|
if err != nil || d <= 0 {
|
|
return def
|
|
}
|
|
return d
|
|
}
|