mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
Add `PUT /admin/providers/{id}` endpoint to allow updating provider
configurations, including status and ban details. Update synchronization
logic to exclude inactive or banned providers from routing tables to
ensure traffic is not routed to them.
344 lines
9.9 KiB
Go
344 lines
9.9 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/ez-api/ez-api/internal/dto"
|
|
"github.com/ez-api/ez-api/internal/model"
|
|
"github.com/ez-api/ez-api/internal/service"
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type Handler struct {
|
|
db *gorm.DB
|
|
sync *service.SyncService
|
|
logger *service.LogWriter
|
|
}
|
|
|
|
func NewHandler(db *gorm.DB, sync *service.SyncService, logger *service.LogWriter) *Handler {
|
|
return &Handler{db: db, sync: sync, logger: logger}
|
|
}
|
|
|
|
// CreateKey is now handled by MasterHandler
|
|
|
|
// CreateProvider godoc
|
|
// @Summary Create a new provider
|
|
// @Description Register a new upstream AI provider
|
|
// @Tags admin
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security AdminAuth
|
|
// @Param provider body dto.ProviderDTO true "Provider Info"
|
|
// @Success 201 {object} model.Provider
|
|
// @Failure 400 {object} gin.H
|
|
// @Failure 500 {object} gin.H
|
|
// @Router /admin/providers [post]
|
|
func (h *Handler) CreateProvider(c *gin.Context) {
|
|
var req dto.ProviderDTO
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
group := strings.TrimSpace(req.Group)
|
|
if group == "" {
|
|
group = "default"
|
|
}
|
|
|
|
status := strings.TrimSpace(req.Status)
|
|
if status == "" {
|
|
status = "active"
|
|
}
|
|
autoBan := true
|
|
if req.AutoBan != nil {
|
|
autoBan = *req.AutoBan
|
|
}
|
|
|
|
provider := model.Provider{
|
|
Name: req.Name,
|
|
Type: req.Type,
|
|
BaseURL: req.BaseURL,
|
|
APIKey: req.APIKey,
|
|
Group: group,
|
|
Models: strings.Join(req.Models, ","),
|
|
Status: status,
|
|
AutoBan: autoBan,
|
|
BanReason: req.BanReason,
|
|
}
|
|
if !req.BanUntil.IsZero() {
|
|
tu := req.BanUntil.UTC()
|
|
provider.BanUntil = &tu
|
|
}
|
|
|
|
if err := h.db.Create(&provider).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create provider", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
if err := h.sync.SyncProvider(&provider); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync provider", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, provider)
|
|
}
|
|
|
|
// UpdateProvider godoc
|
|
// @Summary Update a provider
|
|
// @Description Update provider attributes including status/auto-ban flags
|
|
// @Tags admin
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security AdminAuth
|
|
// @Param id path int true "Provider ID"
|
|
// @Param provider body dto.ProviderDTO true "Provider Info"
|
|
// @Success 200 {object} model.Provider
|
|
// @Failure 400 {object} gin.H
|
|
// @Failure 404 {object} gin.H
|
|
// @Failure 500 {object} gin.H
|
|
// @Router /admin/providers/{id} [put]
|
|
func (h *Handler) UpdateProvider(c *gin.Context) {
|
|
idParam := c.Param("id")
|
|
id, err := strconv.Atoi(idParam)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
|
return
|
|
}
|
|
|
|
var existing model.Provider
|
|
if err := h.db.First(&existing, id).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "provider not found"})
|
|
return
|
|
}
|
|
|
|
var req dto.ProviderDTO
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
update := map[string]any{}
|
|
if strings.TrimSpace(req.Name) != "" {
|
|
update["name"] = req.Name
|
|
}
|
|
if strings.TrimSpace(req.Type) != "" {
|
|
update["type"] = req.Type
|
|
}
|
|
if strings.TrimSpace(req.BaseURL) != "" {
|
|
update["base_url"] = req.BaseURL
|
|
}
|
|
if req.APIKey != "" {
|
|
update["api_key"] = req.APIKey
|
|
}
|
|
if req.Models != nil {
|
|
update["models"] = strings.Join(req.Models, ",")
|
|
}
|
|
if strings.TrimSpace(req.Group) != "" {
|
|
update["group"] = normalizeGroup(req.Group)
|
|
}
|
|
if req.AutoBan != nil {
|
|
update["auto_ban"] = *req.AutoBan
|
|
}
|
|
if strings.TrimSpace(req.Status) != "" {
|
|
update["status"] = req.Status
|
|
}
|
|
if req.BanReason != "" || strings.TrimSpace(req.Status) == "active" {
|
|
update["ban_reason"] = req.BanReason
|
|
}
|
|
if !req.BanUntil.IsZero() {
|
|
tu := req.BanUntil.UTC()
|
|
update["ban_until"] = &tu
|
|
}
|
|
if req.BanUntil.IsZero() && strings.TrimSpace(req.Status) == "active" {
|
|
update["ban_until"] = nil
|
|
}
|
|
|
|
if len(update) == 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"})
|
|
return
|
|
}
|
|
|
|
if err := h.db.Model(&existing).Updates(update).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update provider", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
if err := h.db.First(&existing, id).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reload provider", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
if err := h.sync.SyncProvider(&existing); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync provider", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, existing)
|
|
}
|
|
|
|
// CreateModel godoc
|
|
// @Summary Register a new model
|
|
// @Description Register a supported model with its capabilities
|
|
// @Tags admin
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security AdminAuth
|
|
// @Param model body dto.ModelDTO true "Model Info"
|
|
// @Success 201 {object} model.Model
|
|
// @Failure 400 {object} gin.H
|
|
// @Failure 500 {object} gin.H
|
|
// @Router /admin/models [post]
|
|
func (h *Handler) CreateModel(c *gin.Context) {
|
|
var req dto.ModelDTO
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
modelReq := model.Model{
|
|
Name: req.Name,
|
|
ContextWindow: req.ContextWindow,
|
|
CostPerToken: req.CostPerToken,
|
|
SupportsVision: req.SupportsVision,
|
|
SupportsFunctions: req.SupportsFunctions,
|
|
SupportsToolChoice: req.SupportsToolChoice,
|
|
SupportsFIM: req.SupportsFIM,
|
|
MaxOutputTokens: req.MaxOutputTokens,
|
|
}
|
|
|
|
if err := h.db.Create(&modelReq).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create model", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
if err := h.sync.SyncModel(&modelReq); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync model", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, modelReq)
|
|
}
|
|
|
|
// ListModels godoc
|
|
// @Summary List all models
|
|
// @Description Get a list of all registered models
|
|
// @Tags admin
|
|
// @Produce json
|
|
// @Security AdminAuth
|
|
// @Success 200 {array} model.Model
|
|
// @Failure 500 {object} gin.H
|
|
// @Router /admin/models [get]
|
|
func (h *Handler) ListModels(c *gin.Context) {
|
|
var models []model.Model
|
|
if err := h.db.Find(&models).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list models", "details": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, models)
|
|
}
|
|
|
|
// UpdateModel godoc
|
|
// @Summary Update a model
|
|
// @Description Update an existing model's configuration
|
|
// @Tags admin
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security AdminAuth
|
|
// @Param id path int true "Model ID"
|
|
// @Param model body dto.ModelDTO true "Model Info"
|
|
// @Success 200 {object} model.Model
|
|
// @Failure 400 {object} gin.H
|
|
// @Failure 404 {object} gin.H
|
|
// @Failure 500 {object} gin.H
|
|
// @Router /admin/models/{id} [put]
|
|
func (h *Handler) UpdateModel(c *gin.Context) {
|
|
idParam := c.Param("id")
|
|
id, err := strconv.Atoi(idParam)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
|
return
|
|
}
|
|
|
|
var req dto.ModelDTO
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
var existing model.Model
|
|
if err := h.db.First(&existing, id).Error; err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "model not found"})
|
|
return
|
|
}
|
|
|
|
existing.Name = req.Name
|
|
existing.ContextWindow = req.ContextWindow
|
|
existing.CostPerToken = req.CostPerToken
|
|
existing.SupportsVision = req.SupportsVision
|
|
existing.SupportsFunctions = req.SupportsFunctions
|
|
existing.SupportsToolChoice = req.SupportsToolChoice
|
|
existing.SupportsFIM = req.SupportsFIM
|
|
existing.MaxOutputTokens = req.MaxOutputTokens
|
|
|
|
if err := h.db.Save(&existing).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update model", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
if err := h.sync.SyncModel(&existing); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync model", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, existing)
|
|
}
|
|
|
|
// SyncSnapshot godoc
|
|
// @Summary Force sync snapshot
|
|
// @Description Force full synchronization of DB state to Redis
|
|
// @Tags admin
|
|
// @Produce json
|
|
// @Security AdminAuth
|
|
// @Success 200 {object} gin.H
|
|
// @Failure 500 {object} gin.H
|
|
// @Router /admin/sync/snapshot [post]
|
|
func (h *Handler) SyncSnapshot(c *gin.Context) {
|
|
if err := h.sync.SyncAll(h.db); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync snapshots", "details": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"status": "synced"})
|
|
}
|
|
|
|
// IngestLog accepts log records from data plane or other services.
|
|
// @Summary Ingest logs
|
|
// @Description Internal endpoint for ingesting logs from Balancer
|
|
// @Tags system
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param log body model.LogRecord true "Log Record"
|
|
// @Success 202 {object} gin.H
|
|
// @Failure 400 {object} gin.H
|
|
// @Router /logs [post]
|
|
func (h *Handler) IngestLog(c *gin.Context) {
|
|
var rec model.LogRecord
|
|
if err := c.ShouldBindJSON(&rec); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// By default, only metadata is expected; payload fields may be empty.
|
|
h.logger.Write(rec)
|
|
c.JSON(http.StatusAccepted, gin.H{"status": "queued"})
|
|
}
|
|
|
|
func normalizeGroup(group string) string {
|
|
if strings.TrimSpace(group) == "" {
|
|
return "default"
|
|
}
|
|
return group
|
|
}
|