package config import ( "log/slog" "net/url" "os" "strconv" "strings" ) const ( DefaultLocalGeneratedStorageDir = "data/static/generated" DefaultLocalUploadedStorageDir = "data/static/uploaded" ) type Config struct { AppEnv string HTTPAddr string DatabaseURL string IdentityMode string JWTSecret string ServerMainBaseURL string ServerMainInternalToken string ServerMainInternalKey string ServerMainInternalSecret string OIDCEnabled bool OIDCIssuer string OIDCAudience string OIDCTenantID string OIDCRolePrefix string OIDCRequiredScopes []string OIDCJWKSCacheTTLSeconds int OIDCAcceptLegacyHS256 bool OIDCIntrospectionEnabled bool OIDCIntrospectionClientID string OIDCIntrospectionClientSecret string PublicBaseURL string WebBaseURL string LocalGeneratedStorageDir string LocalUploadedStorageDir string LocalTempAssetTTLHours int TaskProgressCallbackEnabled bool TaskProgressCallbackURL string TaskProgressCallbackTimeoutMS string TaskProgressCallbackMaxAttempts string CORSAllowedOrigin string GlobalHTTPProxy string GlobalHTTPProxySource string LogLevel slog.Level } func Load() Config { globalProxy := LoadGlobalHTTPProxyStatus() return Config{ AppEnv: env("APP_ENV", "development"), HTTPAddr: env("HTTP_ADDR", ":8088"), DatabaseURL: gatewayDatabaseURL(), IdentityMode: env("IDENTITY_MODE", "hybrid"), JWTSecret: env("CONFIG_JWT_SECRET", "this is a very secret secret"), ServerMainBaseURL: strings.TrimRight( env("SERVER_MAIN_BASE_URL", "http://localhost:3000"), "/", ), ServerMainInternalToken: env("SERVER_MAIN_INTERNAL_TOKEN", ""), ServerMainInternalKey: env("SERVER_MAIN_INTERNAL_KEY", "gateway"), ServerMainInternalSecret: env("SERVER_MAIN_INTERNAL_SECRET", env("SERVER_MAIN_INTERNAL_TOKEN", "")), OIDCEnabled: env("OIDC_ENABLED", "false") == "true", OIDCIssuer: strings.TrimRight(env("OIDC_ISSUER", ""), "/"), OIDCAudience: env("OIDC_AUDIENCE", ""), OIDCTenantID: env("OIDC_TENANT_ID", ""), OIDCRolePrefix: env("OIDC_ROLE_PREFIX", "gateway."), OIDCRequiredScopes: splitCSV(env("OIDC_REQUIRED_SCOPES", "gateway.access")), OIDCJWKSCacheTTLSeconds: envInt("OIDC_JWKS_CACHE_TTL_SECONDS", 300), OIDCAcceptLegacyHS256: env("OIDC_ACCEPT_LEGACY_HS256", "true") == "true", OIDCIntrospectionEnabled: env("OIDC_INTROSPECTION_ENABLED", "false") == "true", OIDCIntrospectionClientID: env("OIDC_INTROSPECTION_CLIENT_ID", ""), OIDCIntrospectionClientSecret: env("OIDC_INTROSPECTION_CLIENT_SECRET", ""), PublicBaseURL: strings.TrimRight(env("AI_GATEWAY_PUBLIC_BASE_URL", env("PUBLIC_BASE_URL", "")), "/"), WebBaseURL: strings.TrimRight(env("AI_GATEWAY_WEB_BASE_URL", env("GATEWAY_WEB_BASE_URL", env("PUBLIC_WEB_BASE_URL", ""))), "/"), LocalGeneratedStorageDir: env("AI_GATEWAY_GENERATED_STORAGE_DIR", env("LOCAL_GENERATED_STORAGE_DIR", env("AI_GATEWAY_STATIC_STORAGE_DIR", DefaultLocalGeneratedStorageDir))), LocalUploadedStorageDir: env("AI_GATEWAY_UPLOADED_STORAGE_DIR", env("LOCAL_UPLOADED_STORAGE_DIR", DefaultLocalUploadedStorageDir)), LocalTempAssetTTLHours: envInt("AI_GATEWAY_LOCAL_TEMP_ASSET_TTL_HOURS", 24), TaskProgressCallbackEnabled: env("TASK_PROGRESS_CALLBACK_ENABLED", "true") == "true", TaskProgressCallbackURL: env("TASK_PROGRESS_CALLBACK_URL", strings.TrimRight(env("SERVER_MAIN_BASE_URL", "http://localhost:3000"), "/")+"/internal/platform/task-progress-callbacks", ), TaskProgressCallbackTimeoutMS: env("TASK_PROGRESS_CALLBACK_TIMEOUT_MS", "5000"), TaskProgressCallbackMaxAttempts: env("TASK_PROGRESS_CALLBACK_MAX_ATTEMPTS", "10"), CORSAllowedOrigin: env("CORS_ALLOWED_ORIGIN", "http://localhost:5178,http://127.0.0.1:5178"), GlobalHTTPProxy: globalProxy.HTTPProxy, GlobalHTTPProxySource: globalProxy.Source, LogLevel: logLevel(env("LOG_LEVEL", "info")), } } type GlobalHTTPProxyStatus struct { HTTPProxy string Source string } func LoadGlobalHTTPProxyStatus() GlobalHTTPProxyStatus { for _, key := range []string{ "AI_GATEWAY_GLOBAL_HTTP_PROXY", "GLOBAL_HTTP_PROXY", "HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy", "ALL_PROXY", "all_proxy", } { if value := envValue(key); value != "" { return GlobalHTTPProxyStatus{HTTPProxy: value, Source: key} } } return GlobalHTTPProxyStatus{} } func gatewayDatabaseURL() string { if value := envValue("AI_GATEWAY_DATABASE_URL"); value != "" { return normalizePostgresURL(value) } if value := envValue("DATABASE_URL"); value != "" { return normalizePostgresURL(value) } if memoryURL := envValue("MEMORY_DATABASE_URL"); memoryURL != "" { return normalizePostgresURL(withDatabase(memoryURL, env("AI_GATEWAY_DATABASE_NAME", "easyai_ai_gateway"))) } return normalizePostgresURL("postgresql://easyai:easyai2025@localhost:5432/easyai_ai_gateway?sslmode=disable") } func normalizePostgresURL(raw string) string { parsed, err := url.Parse(raw) if err != nil { return raw } values := parsed.Query() schema := values.Get("schema") if schema == "" { return raw } values.Del("schema") if values.Get("search_path") == "" { values.Set("search_path", schema) } parsed.RawQuery = values.Encode() return parsed.String() } func splitCSV(value string) []string { items := strings.Split(value, ",") result := make([]string, 0, len(items)) for _, item := range items { if trimmed := strings.TrimSpace(item); trimmed != "" { result = append(result, trimmed) } } return result } func withDatabase(raw string, databaseName string) string { parsed, err := url.Parse(raw) if err != nil || databaseName == "" { return raw } parsed.Path = "/" + databaseName return parsed.String() } func envValue(key string) string { return strings.TrimSpace(os.Getenv(key)) } func env(key string, fallback string) string { if value := envValue(key); value != "" { return value } return fallback } func envInt(key string, fallback int) int { value := envValue(key) if value == "" { return fallback } parsed, err := strconv.Atoi(value) if err != nil { return fallback } return parsed } func logLevel(value string) slog.Level { switch strings.ToLower(value) { case "debug": return slog.LevelDebug case "warn", "warning": return slog.LevelWarn case "error": return slog.LevelError default: return slog.LevelInfo } }