Files
SakurasanandClaude Sonnet 5 d0e31b198f feat(server): API relay gateway backend M0-M4
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>
2026-08-15 21:05:02 +08:00

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
}