// Package main is the entry point for the LLM Gateway / Proxy service. // // @title LLM Gateway API // @version 1.0 // @description OpenAI-compatible and Anthropic-compatible LLM proxy/gateway with Bifrost mapping. // @termsOfService http://swagger.io/terms/ // // @contact.name optoant // @contact.email admin@optoant.local // // @license.name MIT // @license.url https://opensource.org/licenses/MIT // // @host localhost:8000 // @BasePath / // // @securityDefinitions.apikey BearerAuth // @in header // @name Authorization package main import ( "fmt" "log" "net/url" "os" "path/filepath" "github.com/gofiber/fiber/v3" "github.com/joho/godotenv" "gorm.io/driver/postgres" "gorm.io/driver/sqlite" "gorm.io/gorm" gormlogger "gorm.io/gorm/logger" "optoant/config" "optoant/handlers" "optoant/internal/logger" "optoant/models" ) func main() { // Load .env file if present (silently ignore if missing) _ = godotenv.Load() cfg := config.Load() // Logger verbosity from LOG_LEVEL env (debug, info, warn) logger.SetLevel(cfg.LogLevel) // ------------------------------------------------------------------ // Database setup (GORM — PostgreSQL or SQLite based on DB_MODE) // ------------------------------------------------------------------ var db *gorm.DB if cfg.PostgresDSN != "" || cfg.DBMode == "sqlite" { var dialector gorm.Dialector dbEngine := "postgres" switch cfg.DBMode { case "sqlite": dsn := cfg.DBPath if dsn == "" { dsn = "data/gateway.db" } if err := os.MkdirAll(filepath.Dir(dsn), 0755); err != nil { log.Printf("⚠️ Failed to create DB directory: %v", err) } dialector = sqlite.Open(dsn) dbEngine = "sqlite" default: dsn := cfg.PostgresDSN if cfg.DBTimezone != "" { dsn = appendQueryParam(dsn, "timezone", cfg.DBTimezone) } dialector = postgres.Open(dsn) } var err error db, err = gorm.Open(dialector, &gorm.Config{ Logger: gormlogger.Default.LogMode(gormlogger.Warn), }) if err != nil { log.Printf("⚠️ DB connection failed (continuing without DB): %v", err) db = nil } else { if err := db.AutoMigrate(&models.RequestLog{}); err != nil { log.Printf("⚠️ AutoMigrate failed: %v", err) } else { log.Printf("✅ Database connected and migrated (%s)", dbEngine) } } } else { log.Println("ℹ️ No POSTGRES_DSN/DATABASE_DSN set — running without database logging") } // ------------------------------------------------------------------ // Fiber v3 application // ------------------------------------------------------------------ app := fiber.New(fiber.Config{ AppName: "LLM Gateway v1.0", ServerHeader: "optoant-gateway", }) // ------------------------------------------------------------------ // Swagger UI at /swagger/* — Fiber v3 native, no v2 dependency // Swagger UI is loaded from CDN; spec is served from /swagger/swagger.json // ------------------------------------------------------------------ app.Get("/swagger/swagger.json", func(c fiber.Ctx) error { return c.SendFile("./docs/swagger.json") }) app.Get("/swagger", func(c fiber.Ctx) error { return c.Redirect().To("/swagger/") }) app.Get("/swagger/*", func(c fiber.Ctx) error { html := ` LLM Gateway — Swagger UI
` return c.Type("html").SendString(html) }) // ------------------------------------------------------------------ // Routes // ------------------------------------------------------------------ // Health check app.Get("/health", handlers.HealthHandler(cfg, db)) // OpenAI-compatible: forward all /v1/* methods to OPENAI_BACKEND app.All("/v1/*", handlers.OpenAIHandler(cfg, db)) // Anthropic-compatible: forward all /anthropic/* to backend or Bifrost app.All("/anthropic", handlers.AnthropicHandler(cfg, db)) app.All("/anthropic/*", handlers.AnthropicHandler(cfg, db)) // ------------------------------------------------------------------ // Start server // ------------------------------------------------------------------ addr := fmt.Sprintf(":%s", cfg.Port) log.Printf("🚀 LLM Gateway starting on %s", addr) log.Printf(" Log level : %s", cfg.LogLevel) log.Printf(" Upstream backend: %s", cfg.OpenAIBackend) log.Printf(" OpenAI endpoint : /v1/* (direct passthrough)") log.Printf(" Anthropic endpoint: /anthropic/* (Anthropic↔OpenAI transform)") if db != nil { log.Printf(" DB logging : enabled") } log.Printf(" SSE streaming : %v", cfg.Streaming) if cfg.DBTimezone != "" { log.Printf(" DB timezone : %s", cfg.DBTimezone) } if err := app.Listen(addr); err != nil { log.Fatalf("server error: %v", err) os.Exit(1) } } func appendQueryParam(dsn, key, value string) string { if value == "" { return dsn } u, err := url.Parse(dsn) if err != nil { log.Printf("⚠️ Failed to parse DSN for timezone: %v", err) return dsn } q := u.Query() q.Set(key, value) u.RawQuery = q.Encode() return u.String() }