b1394a2817
CI / docker (push) Successful in 3m49s
Text keys use OpenAI routes; image keys use predictions with ingress Model ID, OpenAPI input schemas, owner/name or short name resolution, protocol-aware admin whitelist, and in-app API docs aligned to UI conventions. Co-authored-by: Cursor <cursoragent@cursor.com>
182 lines
5.3 KiB
Go
182 lines
5.3 KiB
Go
package admin
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/rose_cat707/luminary/internal/auth"
|
|
"github.com/rose_cat707/luminary/internal/model"
|
|
"github.com/rose_cat707/luminary/internal/store/cache"
|
|
)
|
|
|
|
func (h *Handler) ListIngressKeys(c *gin.Context) {
|
|
keys := h.cache.ListIngressKeys()
|
|
c.JSON(http.StatusOK, gin.H{"data": keys})
|
|
}
|
|
|
|
func (h *Handler) CreateIngressKey(c *gin.Context) {
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
Protocol model.IngressKeyProtocol `json:"protocol"`
|
|
BudgetLimit float64 `json:"budget_limit"`
|
|
RateLimitRPM int `json:"rate_limit_rpm"`
|
|
RateLimitTPM int `json:"rate_limit_tpm"`
|
|
AllowedProviders []string `json:"allowed_providers"`
|
|
AllowedModels []string `json:"allowed_models"`
|
|
ExpiresAt *time.Time `json:"expires_at"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil || req.Name == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
|
|
return
|
|
}
|
|
protocol := req.Protocol
|
|
if protocol == "" {
|
|
protocol = model.IngressProtocolText
|
|
}
|
|
if !protocol.Valid() {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "protocol must be text or image"})
|
|
return
|
|
}
|
|
plainKey, err := generateIngressKey()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "key generation failed"})
|
|
return
|
|
}
|
|
hash := auth.HashIngressKey(plainKey)
|
|
providersJSON := `["*"]`
|
|
modelsJSON := `["*"]`
|
|
if req.AllowedProviders != nil {
|
|
if len(req.AllowedProviders) == 0 {
|
|
providersJSON = `["*"]`
|
|
} else {
|
|
providersJSON = cache.ModelsToJSON(req.AllowedProviders)
|
|
}
|
|
}
|
|
if req.AllowedModels != nil {
|
|
if len(req.AllowedModels) == 0 {
|
|
modelsJSON = `["*"]`
|
|
} else {
|
|
modelsJSON = cache.ModelsToJSON(req.AllowedModels)
|
|
}
|
|
}
|
|
k := model.IngressKey{
|
|
Name: req.Name,
|
|
KeyHash: hash,
|
|
Prefix: plainKey[:12] + "...",
|
|
Protocol: protocol,
|
|
Enabled: true,
|
|
BudgetLimit: req.BudgetLimit,
|
|
RateLimitRPM: req.RateLimitRPM,
|
|
RateLimitTPM: req.RateLimitTPM,
|
|
AllowedProvidersJSON: providersJSON,
|
|
AllowedModelsJSON: modelsJSON,
|
|
ExpiresAt: req.ExpiresAt,
|
|
}
|
|
if err := h.db.DB().Create(&k).Error; err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
h.cache.SetIngressKey(k)
|
|
c.JSON(http.StatusCreated, gin.H{"key": k, "plain_key": plainKey})
|
|
}
|
|
|
|
func (h *Handler) GetIngressKey(c *gin.Context) {
|
|
id := parseUint(c.Param("id"))
|
|
k, ok := h.cache.GetIngressKey(id)
|
|
if !ok {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, k)
|
|
}
|
|
|
|
func (h *Handler) UpdateIngressKey(c *gin.Context) {
|
|
id := parseUint(c.Param("id"))
|
|
k, ok := h.cache.GetIngressKey(id)
|
|
if !ok {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
|
return
|
|
}
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
Protocol *model.IngressKeyProtocol `json:"protocol"`
|
|
Enabled *bool `json:"enabled"`
|
|
BudgetLimit *float64 `json:"budget_limit"`
|
|
RateLimitRPM *int `json:"rate_limit_rpm"`
|
|
RateLimitTPM *int `json:"rate_limit_tpm"`
|
|
AllowedProviders []string `json:"allowed_providers"`
|
|
AllowedModels []string `json:"allowed_models"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
|
|
return
|
|
}
|
|
if req.Protocol != nil {
|
|
if !req.Protocol.Valid() {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "protocol must be text or image"})
|
|
return
|
|
}
|
|
k.Protocol = *req.Protocol
|
|
}
|
|
if req.Name != "" {
|
|
k.Name = req.Name
|
|
}
|
|
if req.Enabled != nil {
|
|
k.Enabled = *req.Enabled
|
|
}
|
|
if req.BudgetLimit != nil {
|
|
k.BudgetLimit = *req.BudgetLimit
|
|
}
|
|
if req.RateLimitRPM != nil {
|
|
k.RateLimitRPM = *req.RateLimitRPM
|
|
}
|
|
if req.RateLimitTPM != nil {
|
|
k.RateLimitTPM = *req.RateLimitTPM
|
|
}
|
|
if req.AllowedProviders != nil {
|
|
if len(req.AllowedProviders) == 0 {
|
|
k.AllowedProvidersJSON = `["*"]`
|
|
} else {
|
|
k.AllowedProvidersJSON = cache.ModelsToJSON(req.AllowedProviders)
|
|
}
|
|
}
|
|
if req.AllowedModels != nil {
|
|
if len(req.AllowedModels) == 0 {
|
|
k.AllowedModelsJSON = `["*"]`
|
|
} else {
|
|
k.AllowedModelsJSON = cache.ModelsToJSON(req.AllowedModels)
|
|
}
|
|
}
|
|
h.db.DB().Save(&k)
|
|
h.cache.SetIngressKey(k)
|
|
c.JSON(http.StatusOK, k)
|
|
}
|
|
|
|
func (h *Handler) DeleteIngressKey(c *gin.Context) {
|
|
id := parseUint(c.Param("id"))
|
|
h.db.DB().Where("ingress_key_id = ?", id).Delete(&model.RoutingRule{})
|
|
h.db.DB().Delete(&model.IngressKey{}, id)
|
|
h.cache.DeleteIngressKey(id)
|
|
h.cache.DeleteRoutingRulesForIngress(id)
|
|
c.JSON(http.StatusOK, gin.H{"ok": true})
|
|
}
|
|
|
|
func (h *Handler) IngressUsage(c *gin.Context) {
|
|
id := parseUint(c.Param("id"))
|
|
var records []model.UsageRecord
|
|
h.db.DB().Where("ingress_key_id = ?", id).Order("created_at desc").Limit(50).Find(&records)
|
|
k, _ := h.cache.GetIngressKey(id)
|
|
c.JSON(http.StatusOK, gin.H{"key": k, "recent": records})
|
|
}
|
|
|
|
func generateIngressKey() (string, error) {
|
|
b := make([]byte, 24)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", err
|
|
}
|
|
return "sk-lum-" + hex.EncodeToString(b), nil
|
|
}
|