feat: migrate backend from rust to go
Some checks failed
CI / Check (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Release Build (push) Has been cancelled

This commit replaces the Axum/Rust backend with a Gin/Go implementation. The original Rust code has been archived in the 'rust' branch.
This commit is contained in:
2026-03-19 10:30:05 -04:00
parent 649371154f
commit 6b10d4249c
69 changed files with 3460 additions and 15096 deletions

View File

@@ -0,0 +1,675 @@
package server
import (
"fmt"
"net/http"
"strings"
"time"
"llm-proxy/internal/db"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
)
type ApiResponse struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
func SuccessResponse(data interface{}) ApiResponse {
return ApiResponse{Success: true, Data: data}
}
func ErrorResponse(err string) ApiResponse {
return ApiResponse{Success: false, Error: err}
}
func (s *Server) adminAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ")
if token == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrorResponse("Not authenticated"))
return
}
session, _, err := s.sessions.ValidateSession(token)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrorResponse("Session expired or invalid"))
return
}
if session.Role != "admin" {
c.AbortWithStatusJSON(http.StatusForbidden, ErrorResponse("Admin access required"))
return
}
c.Set("session", session)
c.Next()
}
}
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
func (s *Server) handleLogin(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse("Invalid request"))
return
}
var user db.User
err := s.database.Get(&user, "SELECT username, password_hash, display_name, role, must_change_password FROM users WHERE username = ?", req.Username)
if err != nil {
c.JSON(http.StatusUnauthorized, ErrorResponse("Invalid username or password"))
return
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
c.JSON(http.StatusUnauthorized, ErrorResponse("Invalid username or password"))
return
}
token, err := s.sessions.CreateSession(user.Username, user.Role)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse("Failed to create session"))
return
}
displayName := user.Username
if user.DisplayName != nil {
displayName = *user.DisplayName
}
c.JSON(http.StatusOK, SuccessResponse(gin.H{
"token": token,
"must_change_password": user.MustChangePassword,
"user": gin.H{
"username": user.Username,
"name": displayName,
"role": user.Role,
},
}))
}
func (s *Server) handleAuthStatus(c *gin.Context) {
token := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ")
session, _, err := s.sessions.ValidateSession(token)
if err != nil {
c.JSON(http.StatusUnauthorized, ErrorResponse("Not authenticated"))
return
}
c.JSON(http.StatusOK, SuccessResponse(gin.H{
"authenticated": true,
"user": gin.H{
"username": session.Username,
"role": session.Role,
},
}))
}
func (s *Server) handleLogout(c *gin.Context) {
token := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ")
s.sessions.RevokeSession(token)
c.JSON(http.StatusOK, SuccessResponse(gin.H{"message": "Logged out"}))
}
type UsagePeriodFilter struct {
Period string `form:"period"`
From string `form:"from"`
To string `form:"to"`
}
func (f *UsagePeriodFilter) ToSQL() (string, []interface{}) {
period := f.Period
if period == "" {
period = "all"
}
if period == "custom" {
var clauses []string
var binds []interface{}
if f.From != "" {
clauses = append(clauses, "timestamp >= ?")
binds = append(binds, f.From)
}
if f.To != "" {
clauses = append(clauses, "timestamp <= ?")
binds = append(binds, f.To)
}
if len(clauses) > 0 {
return " AND " + strings.Join(clauses, " AND "), binds
}
return "", nil
}
now := time.Now().UTC()
var cutoff time.Time
switch period {
case "today":
cutoff = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
case "24h":
cutoff = now.Add(-24 * time.Hour)
case "7d":
cutoff = now.Add(-7 * 24 * time.Hour)
case "30d":
cutoff = now.Add(-30 * 24 * time.Hour)
default:
return "", nil
}
return " AND timestamp >= ?", []interface{}{cutoff.Format(time.RFC3339)}
}
func (s *Server) handleUsageSummary(c *gin.Context) {
var filter UsagePeriodFilter
if err := c.ShouldBindQuery(&filter); err != nil {
// ignore
}
clause, binds := filter.ToSQL()
query := fmt.Sprintf(`
SELECT
COUNT(*) as total_requests,
COALESCE(SUM(total_tokens), 0) as total_tokens,
COALESCE(SUM(cost), 0.0) as total_cost,
COUNT(DISTINCT client_id) as active_clients
FROM llm_requests
WHERE 1=1 %s
`, clause)
var stats struct {
TotalRequests int `db:"total_requests"`
TotalTokens int `db:"total_tokens"`
TotalCost float64 `db:"total_cost"`
ActiveClients int `db:"active_clients"`
}
err := s.database.Get(&stats, query, binds...)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
return
}
c.JSON(http.StatusOK, SuccessResponse(stats))
}
func (s *Server) handleTimeSeries(c *gin.Context) {
var filter UsagePeriodFilter
if err := c.ShouldBindQuery(&filter); err != nil {
// ignore
}
clause, binds := filter.ToSQL()
if clause == "" {
cutoff := time.Now().UTC().Add(-30 * 24 * time.Hour)
clause = " AND timestamp >= ?"
binds = []interface{}{cutoff.Format(time.RFC3339)}
}
query := fmt.Sprintf(`
SELECT
strftime('%%Y-%%m-%%d', timestamp) as bucket,
COUNT(*) as requests,
COALESCE(SUM(total_tokens), 0) as tokens,
COALESCE(SUM(cost), 0.0) as cost
FROM llm_requests
WHERE 1=1 %s
GROUP BY bucket
ORDER BY bucket
`, clause)
var rows []struct {
Bucket string `db:"bucket"`
Requests int `db:"requests"`
Tokens int `db:"tokens"`
Cost float64 `db:"cost"`
}
err := s.database.Select(&rows, query, binds...)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
return
}
series := make([]gin.H, len(rows))
for i, r := range rows {
series[i] = gin.H{
"time": r.Bucket,
"requests": r.Requests,
"tokens": r.Tokens,
"cost": r.Cost,
}
}
c.JSON(http.StatusOK, SuccessResponse(gin.H{
"series": series,
}))
}
func (s *Server) handleAnalyticsBreakdown(c *gin.Context) {
var filter UsagePeriodFilter
if err := c.ShouldBindQuery(&filter); err != nil {
// ignore
}
clause, binds := filter.ToSQL()
var models []struct {
Label string `db:"label"`
Value int `db:"value"`
}
err := s.database.Select(&models, fmt.Sprintf("SELECT model as label, COUNT(*) as value FROM llm_requests WHERE 1=1 %s GROUP BY model ORDER BY value DESC", clause), binds...)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
return
}
var clients []struct {
Label string `db:"label"`
Value int `db:"value"`
}
err = s.database.Select(&clients, fmt.Sprintf("SELECT client_id as label, COUNT(*) as value FROM llm_requests WHERE 1=1 %s GROUP BY client_id ORDER BY value DESC", clause), binds...)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
return
}
c.JSON(http.StatusOK, SuccessResponse(gin.H{
"models": models,
"clients": clients,
}))
}
func (s *Server) handleGetClients(c *gin.Context) {
var clients []db.Client
err := s.database.Select(&clients, "SELECT * FROM clients ORDER BY created_at DESC")
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
return
}
c.JSON(http.StatusOK, SuccessResponse(clients))
}
type CreateClientRequest struct {
Name string `json:"name" binding:"required"`
ClientID *string `json:"client_id"`
}
func (s *Server) handleCreateClient(c *gin.Context) {
var req CreateClientRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse("Invalid request"))
return
}
clientID := ""
if req.ClientID != nil {
clientID = *req.ClientID
} else {
clientID = "client-" + uuid.New().String()[:8]
}
_, err := s.database.Exec("INSERT INTO clients (client_id, name, is_active) VALUES (?, ?, 1)", clientID, req.Name)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
return
}
token := "sk-" + uuid.New().String() + uuid.New().String()
token = token[:51]
_, err = s.database.Exec("INSERT INTO client_tokens (client_id, token, name) VALUES (?, ?, 'default')", clientID, token)
if err != nil {
// Log error
}
c.JSON(http.StatusOK, SuccessResponse(gin.H{
"id": clientID,
"name": req.Name,
"status": "active",
"token": token,
"created_at": time.Now(),
}))
}
func (s *Server) handleDeleteClient(c *gin.Context) {
id := c.Param("id")
if id == "default" {
c.JSON(http.StatusBadRequest, ErrorResponse("Cannot delete default client"))
return
}
_, err := s.database.Exec("DELETE FROM clients WHERE client_id = ?", id)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
return
}
c.JSON(http.StatusOK, SuccessResponse(gin.H{"message": "Client deleted"}))
}
func (s *Server) handleGetClientTokens(c *gin.Context) {
id := c.Param("id")
var tokens []db.ClientToken
err := s.database.Select(&tokens, "SELECT * FROM client_tokens WHERE client_id = ? ORDER BY created_at DESC", id)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
return
}
type MaskedToken struct {
ID int `json:"id"`
TokenMasked string `json:"token_masked"`
Name string `json:"name"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
LastUsedAt *time.Time `json:"last_used_at"`
}
masked := make([]MaskedToken, len(tokens))
for i, t := range tokens {
maskedToken := "••••"
if len(t.Token) > 8 {
maskedToken = t.Token[:3] + "••••" + t.Token[len(t.Token)-8:]
}
masked[i] = MaskedToken{
ID: t.ID,
TokenMasked: maskedToken,
Name: t.Name,
IsActive: t.IsActive,
CreatedAt: t.CreatedAt,
LastUsedAt: t.LastUsedAt,
}
}
c.JSON(http.StatusOK, SuccessResponse(masked))
}
type CreateTokenRequest struct {
Name string `json:"name"`
}
func (s *Server) handleCreateClientToken(c *gin.Context) {
clientID := c.Param("id")
var req CreateTokenRequest
if err := c.ShouldBindJSON(&req); err != nil {
// optional name
}
name := "default"
if req.Name != "" {
name = req.Name
}
token := "sk-" + uuid.New().String() + uuid.New().String()
token = token[:51]
_, err := s.database.Exec("INSERT INTO client_tokens (client_id, token, name) VALUES (?, ?, ?)", clientID, token, name)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
return
}
c.JSON(http.StatusOK, SuccessResponse(gin.H{
"token": token,
"name": name,
"created_at": time.Now(),
}))
}
func (s *Server) handleDeleteClientToken(c *gin.Context) {
tokenID := c.Param("token_id")
_, err := s.database.Exec("DELETE FROM client_tokens WHERE id = ?", tokenID)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
return
}
c.JSON(http.StatusOK, SuccessResponse(gin.H{"message": "Token revoked"}))
}
func (s *Server) handleGetProviders(c *gin.Context) {
var dbConfigs []db.ProviderConfig
err := s.database.Select(&dbConfigs, "SELECT id, enabled, base_url, credit_balance, low_credit_threshold, billing_mode FROM provider_configs")
if err != nil {
// Log error
}
dbMap := make(map[string]db.ProviderConfig)
for _, cfg := range dbConfigs {
dbMap[cfg.ID] = cfg
}
providerIDs := []string{"openai", "gemini", "deepseek", "grok", "ollama"}
var result []gin.H
for _, id := range providerIDs {
var name string
var enabled bool
var baseURL string
switch id {
case "openai":
name = "OpenAI"
enabled = s.cfg.Providers.OpenAI.Enabled
baseURL = s.cfg.Providers.OpenAI.BaseURL
case "gemini":
name = "Google Gemini"
enabled = s.cfg.Providers.Gemini.Enabled
baseURL = s.cfg.Providers.Gemini.BaseURL
case "deepseek":
name = "DeepSeek"
enabled = s.cfg.Providers.DeepSeek.Enabled
baseURL = s.cfg.Providers.DeepSeek.BaseURL
case "grok":
name = "xAI Grok"
enabled = s.cfg.Providers.Grok.Enabled
baseURL = s.cfg.Providers.Grok.BaseURL
case "ollama":
name = "Ollama"
enabled = s.cfg.Providers.Ollama.Enabled
baseURL = s.cfg.Providers.Ollama.BaseURL
}
var balance float64
var threshold float64 = 5.0
var billingMode string
if dbCfg, ok := dbMap[id]; ok {
enabled = dbCfg.Enabled
if dbCfg.BaseURL != nil {
baseURL = *dbCfg.BaseURL
}
balance = dbCfg.CreditBalance
threshold = dbCfg.LowCreditThreshold
if dbCfg.BillingMode != nil {
billingMode = *dbCfg.BillingMode
}
}
status := "disabled"
if enabled {
if _, ok := s.providers[id]; ok {
status = "online"
} else {
status = "error"
}
}
result = append(result, gin.H{
"id": id,
"name": name,
"enabled": enabled,
"status": status,
"base_url": baseURL,
"credit_balance": balance,
"low_credit_threshold": threshold,
"billing_mode": billingMode,
})
}
c.JSON(http.StatusOK, SuccessResponse(result))
}
type UpdateProviderRequest struct {
Enabled bool `json:"enabled"`
BaseURL *string `json:"base_url"`
APIKey *string `json:"api_key"`
CreditBalance *float64 `json:"credit_balance"`
LowCreditThreshold *float64 `json:"low_credit_threshold"`
BillingMode *string `json:"billing_mode"`
}
func (s *Server) handleUpdateProvider(c *gin.Context) {
name := c.Param("name")
var req UpdateProviderRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse("Invalid request"))
return
}
_, err := s.database.Exec(`
INSERT INTO provider_configs (id, display_name, enabled, base_url, api_key, credit_balance, low_credit_threshold, billing_mode)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
enabled = excluded.enabled,
base_url = COALESCE(excluded.base_url, provider_configs.base_url),
api_key = COALESCE(excluded.api_key, provider_configs.api_key),
credit_balance = COALESCE(excluded.credit_balance, provider_configs.credit_balance),
low_credit_threshold = COALESCE(excluded.low_credit_threshold, provider_configs.low_credit_threshold),
billing_mode = COALESCE(excluded.billing_mode, provider_configs.billing_mode),
updated_at = CURRENT_TIMESTAMP
`, name, strings.ToUpper(name), req.Enabled, req.BaseURL, req.APIKey, req.CreditBalance, req.LowCreditThreshold, req.BillingMode)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
return
}
c.JSON(http.StatusOK, SuccessResponse(gin.H{"message": "Provider updated"}))
}
func (s *Server) handleGetModels(c *gin.Context) {
var models []db.ModelConfig
err := s.database.Select(&models, "SELECT * FROM model_configs")
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
return
}
c.JSON(http.StatusOK, SuccessResponse(models))
}
func (s *Server) handleGetUsers(c *gin.Context) {
var users []db.User
err := s.database.Select(&users, "SELECT id, username, display_name, role, must_change_password, created_at FROM users")
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
return
}
c.JSON(http.StatusOK, SuccessResponse(users))
}
type CreateUserRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
DisplayName *string `json:"display_name"`
Role *string `json:"role"`
}
func (s *Server) handleCreateUser(c *gin.Context) {
var req CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse("Invalid request"))
return
}
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 12)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse("Failed to hash password"))
return
}
role := "viewer"
if req.Role != nil {
role = *req.Role
}
_, err = s.database.Exec("INSERT INTO users (username, password_hash, display_name, role, must_change_password) VALUES (?, ?, ?, ?, 1)",
req.Username, string(hash), req.DisplayName, role)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
return
}
c.JSON(http.StatusOK, SuccessResponse(gin.H{"message": "User created"}))
}
type UpdateUserRequest struct {
DisplayName *string `json:"display_name"`
Role *string `json:"role"`
Password *string `json:"password"`
MustChangePassword *bool `json:"must_change_password"`
}
func (s *Server) handleUpdateUser(c *gin.Context) {
id := c.Param("id")
var req UpdateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse("Invalid request"))
return
}
if req.DisplayName != nil {
s.database.Exec("UPDATE users SET display_name = ? WHERE id = ?", req.DisplayName, id)
}
if req.Role != nil {
s.database.Exec("UPDATE users SET role = ? WHERE id = ?", req.Role, id)
}
if req.MustChangePassword != nil {
s.database.Exec("UPDATE users SET must_change_password = ? WHERE id = ?", req.MustChangePassword, id)
}
if req.Password != nil {
hash, _ := bcrypt.GenerateFromPassword([]byte(*req.Password), 12)
s.database.Exec("UPDATE users SET password_hash = ? WHERE id = ?", string(hash), id)
}
c.JSON(http.StatusOK, SuccessResponse(gin.H{"message": "User updated"}))
}
func (s *Server) handleDeleteUser(c *gin.Context) {
id := c.Param("id")
session, _ := c.Get("session")
if sess, ok := session.(*Session); ok {
var username string
s.database.Get(&username, "SELECT username FROM users WHERE id = ?", id)
if username == sess.Username {
c.JSON(http.StatusBadRequest, ErrorResponse("Cannot delete your own account"))
return
}
}
_, err := s.database.Exec("DELETE FROM users WHERE id = ?", id)
if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error()))
return
}
c.JSON(http.StatusOK, SuccessResponse(gin.H{"message": "User deleted"}))
}
func (s *Server) handleSystemHealth(c *gin.Context) {
c.JSON(http.StatusOK, SuccessResponse(gin.H{
"status": "ok",
"db": "connected",
}))
}

113
internal/server/logging.go Normal file
View File

@@ -0,0 +1,113 @@
package server
import (
"log"
"time"
"llm-proxy/internal/db"
)
type RequestLog struct {
Timestamp time.Time `json:"timestamp"`
ClientID string `json:"client_id"`
Provider string `json:"provider"`
Model string `json:"model"`
PromptTokens uint32 `json:"prompt_tokens"`
CompletionTokens uint32 `json:"completion_tokens"`
ReasoningTokens uint32 `json:"reasoning_tokens"`
TotalTokens uint32 `json:"total_tokens"`
CacheReadTokens uint32 `json:"cache_read_tokens"`
CacheWriteTokens uint32 `json:"cache_write_tokens"`
Cost float64 `json:"cost"`
HasImages bool `json:"has_images"`
Status string `json:"status"`
ErrorMessage string `json:"error_message,omitempty"`
DurationMS int64 `json:"duration_ms"`
}
type RequestLogger struct {
database *db.DB
hub *Hub
logChan chan RequestLog
}
func NewRequestLogger(database *db.DB, hub *Hub) *RequestLogger {
return &RequestLogger{
database: database,
hub: hub,
logChan: make(chan RequestLog, 100),
}
}
func (l *RequestLogger) Start() {
go func() {
for entry := range l.logChan {
l.processLog(entry)
}
}()
}
func (l *RequestLogger) LogRequest(entry RequestLog) {
select {
case l.logChan <- entry:
default:
log.Println("Request log channel full, dropping log entry")
}
}
func (l *RequestLogger) processLog(entry RequestLog) {
// Broadcast to dashboard
l.hub.broadcast <- map[string]interface{}{
"type": "request",
"channel": "requests",
"payload": entry,
}
// Insert into DB
tx, err := l.database.Begin()
if err != nil {
log.Printf("Failed to begin transaction for logging: %v", err)
return
}
defer tx.Rollback()
// Ensure client exists
_, _ = tx.Exec("INSERT OR IGNORE INTO clients (client_id, name, description) VALUES (?, ?, 'Auto-created from request')",
entry.ClientID, entry.ClientID)
// Insert log
_, err = tx.Exec(`
INSERT INTO llm_requests
(timestamp, client_id, provider, model, prompt_tokens, completion_tokens, reasoning_tokens, total_tokens, cache_read_tokens, cache_write_tokens, cost, has_images, status, error_message, duration_ms)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, entry.Timestamp, entry.ClientID, entry.Provider, entry.Model,
entry.PromptTokens, entry.CompletionTokens, entry.ReasoningTokens, entry.TotalTokens,
entry.CacheReadTokens, entry.CacheWriteTokens, entry.Cost, entry.HasImages,
entry.Status, entry.ErrorMessage, entry.DurationMS)
if err != nil {
log.Printf("Failed to insert request log: %v", err)
return
}
// Update client stats
_, _ = tx.Exec(`
UPDATE clients SET
total_requests = total_requests + 1,
total_tokens = total_tokens + ?,
total_cost = total_cost + ?,
updated_at = CURRENT_TIMESTAMP
WHERE client_id = ?
`, entry.TotalTokens, entry.Cost, entry.ClientID)
// Update provider balance
if entry.Cost > 0 {
_, _ = tx.Exec("UPDATE provider_configs SET credit_balance = credit_balance - ? WHERE id = ? AND (billing_mode IS NULL OR billing_mode != 'postpaid')",
entry.Cost, entry.Provider)
}
err = tx.Commit()
if err != nil {
log.Printf("Failed to commit logging transaction: %v", err)
}
}

326
internal/server/server.go Normal file
View File

@@ -0,0 +1,326 @@
package server
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"llm-proxy/internal/config"
"llm-proxy/internal/db"
"llm-proxy/internal/middleware"
"llm-proxy/internal/models"
"llm-proxy/internal/providers"
"llm-proxy/internal/utils"
"github.com/gin-gonic/gin"
)
type Server struct {
router *gin.Engine
cfg *config.Config
database *db.DB
providers map[string]providers.Provider
sessions *SessionManager
hub *Hub
logger *RequestLogger
}
func NewServer(cfg *config.Config, database *db.DB) *Server {
router := gin.Default()
hub := NewHub()
s := &Server{
router: router,
cfg: cfg,
database: database,
providers: make(map[string]providers.Provider),
sessions: NewSessionManager(cfg.KeyBytes, 24*time.Hour),
hub: hub,
logger: NewRequestLogger(database, hub),
}
// Initialize providers
if cfg.Providers.OpenAI.Enabled {
apiKey, _ := cfg.GetAPIKey("openai")
s.providers["openai"] = providers.NewOpenAIProvider(cfg.Providers.OpenAI, apiKey)
}
if cfg.Providers.Gemini.Enabled {
apiKey, _ := cfg.GetAPIKey("gemini")
s.providers["gemini"] = providers.NewGeminiProvider(cfg.Providers.Gemini, apiKey)
}
if cfg.Providers.DeepSeek.Enabled {
apiKey, _ := cfg.GetAPIKey("deepseek")
s.providers["deepseek"] = providers.NewDeepSeekProvider(cfg.Providers.DeepSeek, apiKey)
}
if cfg.Providers.Grok.Enabled {
apiKey, _ := cfg.GetAPIKey("grok")
s.providers["grok"] = providers.NewGrokProvider(cfg.Providers.Grok, apiKey)
}
s.setupRoutes()
return s
}
func (s *Server) setupRoutes() {
s.router.Use(middleware.AuthMiddleware(s.database))
// Static files
s.router.Static("/static", "./static")
s.router.StaticFile("/", "./static/index.html")
s.router.StaticFile("/favicon.ico", "./static/favicon.ico")
// WebSocket
s.router.GET("/ws", s.handleWebSocket)
v1 := s.router.Group("/v1")
{
v1.POST("/chat/completions", s.handleChatCompletions)
}
// Dashboard API Group
api := s.router.Group("/api")
{
api.POST("/auth/login", s.handleLogin)
api.GET("/auth/status", s.handleAuthStatus)
api.POST("/auth/logout", s.handleLogout)
// Protected dashboard routes (need admin session)
admin := api.Group("/")
admin.Use(s.adminAuthMiddleware())
{
admin.GET("/usage/summary", s.handleUsageSummary)
admin.GET("/usage/time-series", s.handleTimeSeries)
admin.GET("/analytics/breakdown", s.handleAnalyticsBreakdown)
admin.GET("/clients", s.handleGetClients)
admin.POST("/clients", s.handleCreateClient)
admin.DELETE("/clients/:id", s.handleDeleteClient)
admin.GET("/clients/:id/tokens", s.handleGetClientTokens)
admin.POST("/clients/:id/tokens", s.handleCreateClientToken)
admin.DELETE("/clients/:id/tokens/:token_id", s.handleDeleteClientToken)
admin.GET("/providers", s.handleGetProviders)
admin.PUT("/providers/:name", s.handleUpdateProvider)
admin.GET("/models", s.handleGetModels)
admin.GET("/users", s.handleGetUsers)
admin.POST("/users", s.handleCreateUser)
admin.PUT("/users/:id", s.handleUpdateUser)
admin.DELETE("/users/:id", s.handleDeleteUser)
admin.GET("/system/health", s.handleSystemHealth)
}
}
s.router.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
}
func (s *Server) handleChatCompletions(c *gin.Context) {
startTime := time.Now()
var req models.ChatCompletionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Select provider based on model name
providerName := "openai" // default
if strings.Contains(req.Model, "gemini") {
providerName = "gemini"
} else if strings.Contains(req.Model, "deepseek") {
providerName = "deepseek"
} else if strings.Contains(req.Model, "grok") {
providerName = "grok"
}
provider, ok := s.providers[providerName]
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Provider %s not enabled or supported", providerName)})
return
}
// Convert ChatCompletionRequest to UnifiedRequest
unifiedReq := &models.UnifiedRequest{
Model: req.Model,
Messages: []models.UnifiedMessage{},
Temperature: req.Temperature,
TopP: req.TopP,
TopK: req.TopK,
N: req.N,
MaxTokens: req.MaxTokens,
PresencePenalty: req.PresencePenalty,
FrequencyPenalty: req.FrequencyPenalty,
Stream: req.Stream != nil && *req.Stream,
Tools: req.Tools,
ToolChoice: req.ToolChoice,
}
// Handle Stop sequences
if req.Stop != nil {
var stop []string
if err := json.Unmarshal(req.Stop, &stop); err == nil {
unifiedReq.Stop = stop
} else {
var singleStop string
if err := json.Unmarshal(req.Stop, &singleStop); err == nil {
unifiedReq.Stop = []string{singleStop}
}
}
}
// Convert messages
for _, msg := range req.Messages {
unifiedMsg := models.UnifiedMessage{
Role: msg.Role,
Content: []models.UnifiedContentPart{},
ReasoningContent: msg.ReasoningContent,
ToolCalls: msg.ToolCalls,
Name: msg.Name,
ToolCallID: msg.ToolCallID,
}
// Handle multimodal content
if strContent, ok := msg.Content.(string); ok {
unifiedMsg.Content = append(unifiedMsg.Content, models.UnifiedContentPart{
Type: "text",
Text: strContent,
})
} else if parts, ok := msg.Content.([]interface{}); ok {
for _, part := range parts {
if partMap, ok := part.(map[string]interface{}); ok {
partType, _ := partMap["type"].(string)
if partType == "text" {
text, _ := partMap["text"].(string)
unifiedMsg.Content = append(unifiedMsg.Content, models.UnifiedContentPart{
Type: "text",
Text: text,
})
} else if partType == "image_url" {
if imgURLMap, ok := partMap["image_url"].(map[string]interface{}); ok {
url, _ := imgURLMap["url"].(string)
imageInput := &models.ImageInput{}
if strings.HasPrefix(url, "data:") {
mime, data, err := utils.ParseDataURL(url)
if err == nil {
imageInput.Base64 = data
imageInput.MimeType = mime
}
} else {
imageInput.URL = url
}
unifiedMsg.Content = append(unifiedMsg.Content, models.UnifiedContentPart{
Type: "image",
Image: imageInput,
})
unifiedReq.HasImages = true
}
}
}
}
}
unifiedReq.Messages = append(unifiedReq.Messages, unifiedMsg)
}
clientID := "default"
if auth, ok := c.Get("auth"); ok {
if authInfo, ok := auth.(models.AuthInfo); ok {
unifiedReq.ClientID = authInfo.ClientID
clientID = authInfo.ClientID
}
} else {
unifiedReq.ClientID = clientID
}
if unifiedReq.Stream {
ch, err := provider.ChatCompletionStream(c.Request.Context(), unifiedReq)
if err != nil {
s.logRequest(startTime, clientID, providerName, req.Model, nil, err, unifiedReq.HasImages)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
var lastUsage *models.Usage
c.Stream(func(w io.Writer) bool {
chunk, ok := <-ch
if !ok {
fmt.Fprintf(w, "data: [DONE]\n\n")
s.logRequest(startTime, clientID, providerName, req.Model, lastUsage, nil, unifiedReq.HasImages)
return false
}
if chunk.Usage != nil {
lastUsage = chunk.Usage
}
data, err := json.Marshal(chunk)
if err != nil {
return false
}
fmt.Fprintf(w, "data: %s\n\n", data)
return true
})
return
}
resp, err := provider.ChatCompletion(c.Request.Context(), unifiedReq)
if err != nil {
s.logRequest(startTime, clientID, providerName, req.Model, nil, err, unifiedReq.HasImages)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
s.logRequest(startTime, clientID, providerName, req.Model, resp.Usage, nil, unifiedReq.HasImages)
c.JSON(http.StatusOK, resp)
}
func (s *Server) logRequest(start time.Time, clientID, provider, model string, usage *models.Usage, err error, hasImages bool) {
entry := RequestLog{
Timestamp: start,
ClientID: clientID,
Provider: provider,
Model: model,
Status: "success",
DurationMS: time.Since(start).Milliseconds(),
HasImages: hasImages,
}
if err != nil {
entry.Status = "error"
entry.ErrorMessage = err.Error()
}
if usage != nil {
entry.PromptTokens = usage.PromptTokens
entry.CompletionTokens = usage.CompletionTokens
entry.TotalTokens = usage.TotalTokens
if usage.ReasoningTokens != nil {
entry.ReasoningTokens = *usage.ReasoningTokens
}
if usage.CacheReadTokens != nil {
entry.CacheReadTokens = *usage.CacheReadTokens
}
if usage.CacheWriteTokens != nil {
entry.CacheWriteTokens = *usage.CacheWriteTokens
}
// TODO: Calculate cost properly based on pricing
entry.Cost = 0.0
}
s.logger.LogRequest(entry)
}
func (s *Server) Run() error {
go s.hub.Run()
s.logger.Start()
addr := fmt.Sprintf("%s:%d", s.cfg.Server.Host, s.cfg.Server.Port)
return s.router.Run(addr)
}

151
internal/server/sessions.go Normal file
View File

@@ -0,0 +1,151 @@
package server
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"sync"
"time"
"github.com/google/uuid"
)
type Session struct {
Username string `json:"username"`
Role string `json:"role"`
CreatedAt time.Time `json:"created_at"`
ExpiresAt time.Time `json:"expires_at"`
SessionID string `json:"session_id"`
}
type SessionManager struct {
sessions map[string]Session
mu sync.RWMutex
secret []byte
ttl time.Duration
}
type sessionPayload struct {
SessionID string `json:"session_id"`
Username string `json:"username"`
Role string `json:"role"`
Exp int64 `json:"exp"`
}
func NewSessionManager(secret []byte, ttl time.Duration) *SessionManager {
return &SessionManager{
sessions: make(map[string]Session),
secret: secret,
ttl: ttl,
}
}
func (m *SessionManager) CreateSession(username, role string) (string, error) {
sessionID := uuid.New().String()
now := time.Now()
expiresAt := now.Add(m.ttl)
m.mu.Lock()
m.sessions[sessionID] = Session{
Username: username,
Role: role,
CreatedAt: now,
ExpiresAt: expiresAt,
SessionID: sessionID,
}
m.mu.Unlock()
return m.createSignedToken(sessionID, username, role, expiresAt.Unix())
}
func (m *SessionManager) createSignedToken(sessionID, username, role string, exp int64) (string, error) {
payload := sessionPayload{
SessionID: sessionID,
Username: username,
Role: role,
Exp: exp,
}
payloadJSON, err := json.Marshal(payload)
if err != nil {
return "", err
}
payloadB64 := base64.RawURLEncoding.EncodeToString(payloadJSON)
h := hmac.New(sha256.New, m.secret)
h.Write(payloadJSON)
signature := h.Sum(nil)
signatureB64 := base64.RawURLEncoding.EncodeToString(signature)
return fmt.Sprintf("%s.%s", payloadB64, signatureB64), nil
}
func (m *SessionManager) ValidateSession(token string) (*Session, string, error) {
parts := strings.Split(token, ".")
if len(parts) != 2 {
return nil, "", fmt.Errorf("invalid token format")
}
payloadB64 := parts[0]
signatureB64 := parts[1]
payloadJSON, err := base64.RawURLEncoding.DecodeString(payloadB64)
if err != nil {
return nil, "", err
}
signature, err := base64.RawURLEncoding.DecodeString(signatureB64)
if err != nil {
return nil, "", err
}
h := hmac.New(sha256.New, m.secret)
h.Write(payloadJSON)
if !hmac.Equal(signature, h.Sum(nil)) {
return nil, "", fmt.Errorf("invalid signature")
}
var payload sessionPayload
if err := json.Unmarshal(payloadJSON, &payload); err != nil {
return nil, "", err
}
if time.Now().Unix() > payload.Exp {
return nil, "", fmt.Errorf("token expired")
}
m.mu.RLock()
session, ok := m.sessions[payload.SessionID]
m.mu.RUnlock()
if !ok {
return nil, "", fmt.Errorf("session not found")
}
return &session, "", nil
}
func (m *SessionManager) RevokeSession(token string) {
parts := strings.Split(token, ".")
if len(parts) != 2 {
return
}
payloadJSON, err := base64.RawURLEncoding.DecodeString(parts[0])
if err != nil {
return
}
var payload sessionPayload
if err := json.Unmarshal(payloadJSON, &payload); err != nil {
return
}
m.mu.Lock()
delete(m.sessions, payload.SessionID)
m.mu.Unlock()
}

View File

@@ -0,0 +1,98 @@
package server
import (
"log"
"net/http"
"sync"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true // In production, refine this
},
}
type Hub struct {
clients map[*websocket.Conn]bool
broadcast chan interface{}
register chan *websocket.Conn
unregister chan *websocket.Conn
mu sync.Mutex
}
func NewHub() *Hub {
return &Hub{
clients: make(map[*websocket.Conn]bool),
broadcast: make(chan interface{}),
register: make(chan *websocket.Conn),
unregister: make(chan *websocket.Conn),
}
}
func (h *Hub) Run() {
for {
select {
case client := <-h.register:
h.mu.Lock()
h.clients[client] = true
h.mu.Unlock()
log.Println("WebSocket client registered")
case client := <-h.unregister:
h.mu.Lock()
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
client.Close()
}
h.mu.Unlock()
log.Println("WebSocket client unregistered")
case message := <-h.broadcast:
h.mu.Lock()
for client := range h.clients {
err := client.WriteJSON(message)
if err != nil {
log.Printf("WebSocket error: %v", err)
client.Close()
delete(h.clients, client)
}
}
h.mu.Unlock()
}
}
}
func (s *Server) handleWebSocket(c *gin.Context) {
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Printf("Failed to set websocket upgrade: %v", err)
return
}
s.hub.register <- conn
defer func() {
s.hub.unregister <- conn
}()
// Initial message
conn.WriteJSON(gin.H{
"type": "connected",
"message": "Connected to LLM Proxy Dashboard",
})
for {
var msg map[string]interface{}
err := conn.ReadJSON(&msg)
if err != nil {
break
}
if msg["type"] == "ping" {
conn.WriteJSON(gin.H{"type": "pong", "payload": gin.H{}})
}
}
}