OpenAI-compatible AI gateway with Vue admin UI, multi-provider egress, ingress key governance, monitoring, and security controls. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rose_cat707/luminary/internal/model"
|
||||
)
|
||||
|
||||
func (h *Handler) ListIPRules(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"data": h.cache.ListIPRules()})
|
||||
}
|
||||
|
||||
func (h *Handler) CreateIPRule(c *gin.Context) {
|
||||
var req struct {
|
||||
CIDR string `json:"cidr"`
|
||||
Type model.IPRuleType `json:"type"`
|
||||
Scope model.IPRuleScope `json:"scope"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil || req.CIDR == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
|
||||
return
|
||||
}
|
||||
if req.Type == "" {
|
||||
req.Type = model.IPRuleAllow
|
||||
}
|
||||
if req.Scope == "" {
|
||||
req.Scope = model.IPScopeProxy
|
||||
}
|
||||
rule := model.IPRule{CIDR: req.CIDR, Type: req.Type, Scope: req.Scope, Enabled: true}
|
||||
if req.Enabled != nil {
|
||||
rule.Enabled = *req.Enabled
|
||||
}
|
||||
if err := h.db.DB().Create(&rule).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
rules := h.cache.ListIPRules()
|
||||
rules = append(rules, rule)
|
||||
h.cache.SetIPRules(rules)
|
||||
c.JSON(http.StatusCreated, rule)
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateIPRule(c *gin.Context) {
|
||||
id := parseUint(c.Param("id"))
|
||||
var rule model.IPRule
|
||||
if err := h.db.DB().First(&rule, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
CIDR string `json:"cidr"`
|
||||
Type model.IPRuleType `json:"type"`
|
||||
Scope model.IPRuleScope `json:"scope"`
|
||||
Enabled *bool `json:"enabled"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
|
||||
return
|
||||
}
|
||||
if req.CIDR != "" {
|
||||
rule.CIDR = req.CIDR
|
||||
}
|
||||
if req.Type != "" {
|
||||
rule.Type = req.Type
|
||||
}
|
||||
if req.Scope != "" {
|
||||
rule.Scope = req.Scope
|
||||
}
|
||||
if req.Enabled != nil {
|
||||
rule.Enabled = *req.Enabled
|
||||
}
|
||||
h.db.DB().Save(&rule)
|
||||
h.reloadIPRules()
|
||||
c.JSON(http.StatusOK, rule)
|
||||
}
|
||||
|
||||
func (h *Handler) DeleteIPRule(c *gin.Context) {
|
||||
id := parseUint(c.Param("id"))
|
||||
h.db.DB().Delete(&model.IPRule{}, id)
|
||||
h.reloadIPRules()
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
func (h *Handler) reloadIPRules() {
|
||||
var rules []model.IPRule
|
||||
h.db.DB().Find(&rules)
|
||||
h.cache.SetIPRules(rules)
|
||||
}
|
||||
|
||||
func (h *Handler) DashboardOverview(c *gin.Context) {
|
||||
since := time.Now().Add(-24 * time.Hour)
|
||||
var totalRequests int64
|
||||
var totalTokens int64
|
||||
var errorCount int64
|
||||
h.db.DB().Model(&model.UsageRecord{}).Where("created_at >= ?", since).Count(&totalRequests)
|
||||
h.db.DB().Model(&model.UsageRecord{}).Where("created_at >= ?", since).Select("COALESCE(SUM(tokens_in + tokens_out), 0)").Scan(&totalTokens)
|
||||
h.db.DB().Model(&model.UsageRecord{}).Where("created_at >= ? AND status = ?", since, "error").Count(&errorCount)
|
||||
activeKeys := len(h.cache.ListIngressKeys())
|
||||
errorRate := 0.0
|
||||
if totalRequests > 0 {
|
||||
errorRate = float64(errorCount) / float64(totalRequests)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"total_requests_24h": totalRequests,
|
||||
"total_tokens_24h": totalTokens,
|
||||
"error_rate": errorRate,
|
||||
"active_ingress_keys": activeKeys,
|
||||
"providers": len(h.cache.ListProviders()),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) DashboardProviders(c *gin.Context) {
|
||||
providers := h.cache.ListProviders()
|
||||
health := h.cache.GetHealthStatus()
|
||||
type item struct {
|
||||
model.Provider
|
||||
Health model.HealthCheckRecord `json:"health"`
|
||||
}
|
||||
var out []item
|
||||
for _, p := range providers {
|
||||
out = append(out, item{Provider: p, Health: health[p.ID]})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": out})
|
||||
}
|
||||
|
||||
func (h *Handler) DashboardIngressUsage(c *gin.Context) {
|
||||
keys := h.cache.ListIngressKeys()
|
||||
c.JSON(http.StatusOK, gin.H{"data": keys})
|
||||
}
|
||||
|
||||
func (h *Handler) DashboardEgressUsage(c *gin.Context) {
|
||||
keys := h.cache.ListAllProviderKeys()
|
||||
c.JSON(http.StatusOK, gin.H{"data": keys})
|
||||
}
|
||||
|
||||
func (h *Handler) DashboardProviderModelUsage(c *gin.Context) {
|
||||
since := time.Now().Add(-24 * time.Hour)
|
||||
type row struct {
|
||||
ProviderID uint `json:"provider_id"`
|
||||
Model string `json:"model"`
|
||||
RequestCount int64 `json:"request_count"`
|
||||
TokenCount int64 `json:"token_count"`
|
||||
Cost float64 `json:"cost"`
|
||||
}
|
||||
var rows []row
|
||||
h.db.DB().Model(&model.UsageRecord{}).
|
||||
Select("provider_id, model, COUNT(*) as request_count, COALESCE(SUM(tokens_in + tokens_out), 0) as token_count, COALESCE(SUM(cost), 0) as cost").
|
||||
Where("created_at >= ? AND provider_id > 0 AND model != ''", since).
|
||||
Group("provider_id, model").
|
||||
Order("request_count desc").
|
||||
Limit(50).
|
||||
Scan(&rows)
|
||||
|
||||
type item struct {
|
||||
ProviderID uint `json:"provider_id"`
|
||||
ProviderName string `json:"provider_name"`
|
||||
Model string `json:"model"`
|
||||
RequestCount int64 `json:"request_count"`
|
||||
TokenCount int64 `json:"token_count"`
|
||||
Cost float64 `json:"cost"`
|
||||
}
|
||||
out := make([]item, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
name := ""
|
||||
if p, ok := h.cache.GetProvider(r.ProviderID); ok {
|
||||
name = p.Name
|
||||
}
|
||||
out = append(out, item{
|
||||
ProviderID: r.ProviderID,
|
||||
ProviderName: name,
|
||||
Model: r.Model,
|
||||
RequestCount: r.RequestCount,
|
||||
TokenCount: r.TokenCount,
|
||||
Cost: r.Cost,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": out})
|
||||
}
|
||||
|
||||
func (h *Handler) DashboardClientIPUsage(c *gin.Context) {
|
||||
since := time.Now().Add(-24 * time.Hour)
|
||||
type item struct {
|
||||
ClientIP string `json:"client_ip"`
|
||||
RequestCount int64 `json:"request_count"`
|
||||
TokenCount int64 `json:"token_count"`
|
||||
Cost float64 `json:"cost"`
|
||||
}
|
||||
var out []item
|
||||
h.db.DB().Model(&model.UsageRecord{}).
|
||||
Select("client_ip, COUNT(*) as request_count, COALESCE(SUM(tokens_in + tokens_out), 0) as token_count, COALESCE(SUM(cost), 0) as cost").
|
||||
Where("created_at >= ? AND client_ip != ''", since).
|
||||
Group("client_ip").
|
||||
Order("request_count desc").
|
||||
Limit(50).
|
||||
Scan(&out)
|
||||
c.JSON(http.StatusOK, gin.H{"data": out})
|
||||
}
|
||||
|
||||
func (h *Handler) DashboardRequestTrend(c *gin.Context) {
|
||||
since := time.Now().Add(-23 * time.Hour).Truncate(time.Hour)
|
||||
type aggRow struct {
|
||||
Hour string `gorm:"column:hour"`
|
||||
Requests int64 `gorm:"column:requests"`
|
||||
Errors int64 `gorm:"column:errors"`
|
||||
Tokens int64 `gorm:"column:tokens"`
|
||||
}
|
||||
var rows []aggRow
|
||||
h.db.DB().Model(&model.UsageRecord{}).
|
||||
Select(`strftime('%Y-%m-%d %H:00', created_at) as hour,
|
||||
COUNT(*) as requests,
|
||||
SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as errors,
|
||||
COALESCE(SUM(tokens_in + tokens_out), 0) as tokens`).
|
||||
Where("created_at >= ?", since).
|
||||
Group("hour").
|
||||
Order("hour").
|
||||
Scan(&rows)
|
||||
|
||||
byHour := make(map[string]aggRow, len(rows))
|
||||
for _, r := range rows {
|
||||
byHour[r.Hour] = r
|
||||
}
|
||||
|
||||
type bucket struct {
|
||||
Hour string `json:"hour"`
|
||||
Requests int64 `json:"requests"`
|
||||
Errors int64 `json:"errors"`
|
||||
Tokens int64 `json:"tokens"`
|
||||
}
|
||||
out := make([]bucket, 24)
|
||||
for i := 0; i < 24; i++ {
|
||||
t := since.Add(time.Duration(i) * time.Hour)
|
||||
key := t.Format("2006-01-02 15:00")
|
||||
if r, ok := byHour[key]; ok {
|
||||
out[i] = bucket{Hour: key, Requests: r.Requests, Errors: r.Errors, Tokens: r.Tokens}
|
||||
} else {
|
||||
out[i] = bucket{Hour: key}
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": out})
|
||||
}
|
||||
Reference in New Issue
Block a user