76ba500417
CI / docker (push) Successful in 2m4s
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>
244 lines
7.0 KiB
Go
244 lines
7.0 KiB
Go
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})
|
|
}
|