c56ae7194c
- Updated AnthropicHandler to check for streaming configuration before handling SSE transform. - Modified handleStreaming to use the original request body instead of forcing stream to true. - Adjusted the transformation logic in AnthropicToBifrost to respect the original stream setting. - Added logging for SSE streaming configuration in the main application.
170 lines
5.1 KiB
Go
170 lines
5.1 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"
|
||
"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:
|
||
dialector = postgres.Open(cfg.PostgresDSN)
|
||
}
|
||
|
||
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 err := app.Listen(addr); err != nil {
|
||
log.Fatalf("server error: %v", err)
|
||
os.Exit(1)
|
||
}
|
||
}
|