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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
b0c7439c01
commit
d0e31b198f
@@ -0,0 +1,143 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user