193 lines
5.6 KiB
Go
193 lines
5.6 KiB
Go
// 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 := `<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>LLM Gateway — Swagger UI</title>
|
||
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
|
||
</head>
|
||
<body>
|
||
<div id="swagger-ui"></div>
|
||
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
||
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js"></script>
|
||
<script>
|
||
window.onload = () => {
|
||
SwaggerUIBundle({
|
||
url: "/swagger/swagger.json",
|
||
dom_id: "#swagger-ui",
|
||
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
|
||
layout: "StandaloneLayout",
|
||
deepLinking: true,
|
||
})
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>`
|
||
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()
|
||
}
|