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>
221 lines
6.1 KiB
Go
221 lines
6.1 KiB
Go
package admin
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/rose_cat707/luminary/internal/model"
|
|
"github.com/rose_cat707/luminary/internal/pricing"
|
|
)
|
|
|
|
func (h *Handler) ListRoutingRules(c *gin.Context) {
|
|
ingressID := parseUint(c.Param("id"))
|
|
c.JSON(http.StatusOK, gin.H{"data": h.cache.ListRoutingRules(ingressID)})
|
|
}
|
|
|
|
func (h *Handler) CreateRoutingRule(c *gin.Context) {
|
|
ingressID := parseUint(c.Param("id"))
|
|
var req struct {
|
|
ModelPattern string `json:"model_pattern"`
|
|
ProviderID uint `json:"provider_id"`
|
|
ProviderKeyID uint `json:"provider_key_id"`
|
|
Priority int `json:"priority"`
|
|
Enabled *bool `json:"enabled"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil || req.ModelPattern == "" || req.ProviderID == 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
|
|
return
|
|
}
|
|
rule := model.RoutingRule{
|
|
IngressKeyID: ingressID,
|
|
ModelPattern: req.ModelPattern,
|
|
ProviderID: req.ProviderID,
|
|
ProviderKeyID: req.ProviderKeyID,
|
|
Priority: req.Priority,
|
|
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.ListRoutingRules(ingressID)
|
|
rules = append(rules, rule)
|
|
h.cache.SetRoutingRules(ingressID, rules)
|
|
c.JSON(http.StatusCreated, rule)
|
|
}
|
|
|
|
func (h *Handler) UpdateRoutingRule(c *gin.Context) {
|
|
ruleID := parseUint(c.Param("ruleId"))
|
|
var rule model.RoutingRule
|
|
if err := h.db.DB().First(&rule, ruleID).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
|
return
|
|
}
|
|
var req struct {
|
|
ModelPattern string `json:"model_pattern"`
|
|
ProviderID uint `json:"provider_id"`
|
|
ProviderKeyID uint `json:"provider_key_id"`
|
|
Priority int `json:"priority"`
|
|
Enabled *bool `json:"enabled"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
|
|
return
|
|
}
|
|
if req.ModelPattern != "" {
|
|
rule.ModelPattern = req.ModelPattern
|
|
}
|
|
if req.ProviderID > 0 {
|
|
rule.ProviderID = req.ProviderID
|
|
}
|
|
if req.ProviderKeyID >= 0 {
|
|
rule.ProviderKeyID = req.ProviderKeyID
|
|
}
|
|
rule.Priority = req.Priority
|
|
if req.Enabled != nil {
|
|
rule.Enabled = *req.Enabled
|
|
}
|
|
h.db.DB().Save(&rule)
|
|
h.reloadRoutingRules(rule.IngressKeyID)
|
|
c.JSON(http.StatusOK, rule)
|
|
}
|
|
|
|
func (h *Handler) DeleteRoutingRule(c *gin.Context) {
|
|
ruleID := parseUint(c.Param("ruleId"))
|
|
var rule model.RoutingRule
|
|
if err := h.db.DB().First(&rule, ruleID).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
|
return
|
|
}
|
|
h.db.DB().Delete(&rule)
|
|
h.reloadRoutingRules(rule.IngressKeyID)
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
}
|
|
|
|
func (h *Handler) reloadRoutingRules(ingressID uint) {
|
|
var rules []model.RoutingRule
|
|
h.db.DB().Where("ingress_key_id = ?", ingressID).Find(&rules)
|
|
h.cache.SetRoutingRules(ingressID, rules)
|
|
}
|
|
|
|
func (h *Handler) ListRequestLogs(c *gin.Context) {
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if pageSize <= 0 || pageSize > 50 {
|
|
pageSize = 20
|
|
}
|
|
|
|
q := h.db.DB().Model(&model.UsageRecord{})
|
|
if ingressID := c.Query("ingress_key_id"); ingressID != "" {
|
|
q = q.Where("ingress_key_id = ?", ingressID)
|
|
}
|
|
if status := c.Query("status"); status != "" {
|
|
q = q.Where("status = ?", status)
|
|
}
|
|
if endpoint := c.Query("endpoint"); endpoint != "" {
|
|
q = q.Where("endpoint = ?", endpoint)
|
|
}
|
|
if clientIP := c.Query("client_ip"); clientIP != "" {
|
|
q = q.Where("client_ip LIKE ?", "%"+clientIP+"%")
|
|
}
|
|
if modelName := c.Query("model"); modelName != "" {
|
|
q = q.Where("model LIKE ?", "%"+modelName+"%")
|
|
}
|
|
|
|
var total int64
|
|
q.Count(&total)
|
|
|
|
var logs []model.UsageRecord
|
|
q.Order("created_at desc").
|
|
Offset((page - 1) * pageSize).
|
|
Limit(pageSize).
|
|
Find(&logs)
|
|
|
|
type item struct {
|
|
ID uint `json:"id"`
|
|
IngressKeyID uint `json:"ingress_key_id"`
|
|
IngressName string `json:"ingress_name"`
|
|
ClientIP string `json:"client_ip"`
|
|
Endpoint string `json:"endpoint"`
|
|
Model string `json:"model"`
|
|
LatencyMs int64 `json:"latency_ms"`
|
|
Status string `json:"status"`
|
|
Stream bool `json:"stream"`
|
|
ErrorMsg string `json:"error_msg"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
out := make([]item, 0, len(logs))
|
|
for _, log := range logs {
|
|
name := ""
|
|
if log.Endpoint == model.EndpointAdminLogin {
|
|
name = "管理台"
|
|
} else if k, ok := h.cache.GetIngressKey(log.IngressKeyID); ok {
|
|
name = k.Name
|
|
}
|
|
out = append(out, item{
|
|
ID: log.ID,
|
|
IngressKeyID: log.IngressKeyID,
|
|
IngressName: name,
|
|
ClientIP: log.ClientIP,
|
|
Endpoint: log.Endpoint,
|
|
Model: log.Model,
|
|
LatencyMs: log.LatencyMs,
|
|
Status: log.Status,
|
|
Stream: log.Stream,
|
|
ErrorMsg: log.ErrorMsg,
|
|
CreatedAt: log.CreatedAt,
|
|
})
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"data": out,
|
|
"total": total,
|
|
"page": page,
|
|
"page_size": pageSize,
|
|
"max_kept": model.MaxUsageLogRecords,
|
|
})
|
|
}
|
|
|
|
func (h *Handler) GetRequestLog(c *gin.Context) {
|
|
id := parseUint(c.Param("id"))
|
|
var log model.UsageRecord
|
|
if err := h.db.DB().First(&log, id).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
|
return
|
|
}
|
|
name := ""
|
|
if log.Endpoint == model.EndpointAdminLogin {
|
|
name = "管理台"
|
|
} else if k, ok := h.cache.GetIngressKey(log.IngressKeyID); ok {
|
|
name = k.Name
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"data": gin.H{
|
|
"id": log.ID,
|
|
"ingress_key_id": log.IngressKeyID,
|
|
"ingress_name": name,
|
|
"client_ip": log.ClientIP,
|
|
"endpoint": log.Endpoint,
|
|
"model": log.Model,
|
|
"latency_ms": log.LatencyMs,
|
|
"status": log.Status,
|
|
"stream": log.Stream,
|
|
"error_msg": log.ErrorMsg,
|
|
"request_body": log.RequestBody,
|
|
"response_body": log.ResponseBody,
|
|
"created_at": log.CreatedAt,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (h *Handler) PricingCatalog(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{"data": pricing.ListCatalog()})
|
|
}
|