mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
Implement handlers for creating, listing, and updating model bindings. Register new routes in the admin server group and add DTO definitions. Update provider handlers to trigger binding synchronization on changes to ensure upstream mappings remain current.
367 lines
11 KiB
Go
367 lines
11 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"
|
|
groupx "github.com/ez-api/foundation/group"
|
|
"github.com/ez-api/foundation/provider"
|
|
"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
|
|
}
|
|
|
|
providerType := provider.NormalizeType(req.Type)
|
|
googleLocation := provider.DefaultGoogleLocation(providerType, req.GoogleLocation)
|
|
|
|
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: strings.TrimSpace(req.Type),
|
|
BaseURL: req.BaseURL,
|
|
APIKey: req.APIKey,
|
|
GoogleProject: strings.TrimSpace(req.GoogleProject),
|
|
GoogleLocation: googleLocation,
|
|
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
|
|
}
|
|
// Provider model list changes can affect binding upstream mappings; rebuild bindings snapshot.
|
|
if err := h.sync.SyncBindings(h.db); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync bindings", "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
|
|
}
|
|
|
|
nextType := strings.TrimSpace(existing.Type)
|
|
if t := strings.TrimSpace(req.Type); t != "" {
|
|
nextType = t
|
|
}
|
|
nextTypeLower := provider.NormalizeType(nextType)
|
|
|
|
update := map[string]any{}
|
|
if strings.TrimSpace(req.Name) != "" {
|
|
update["name"] = req.Name
|
|
}
|
|
if strings.TrimSpace(req.Type) != "" {
|
|
update["type"] = strings.TrimSpace(req.Type)
|
|
}
|
|
if strings.TrimSpace(req.BaseURL) != "" {
|
|
update["base_url"] = req.BaseURL
|
|
}
|
|
if req.APIKey != "" {
|
|
update["api_key"] = req.APIKey
|
|
}
|
|
if strings.TrimSpace(req.GoogleProject) != "" {
|
|
update["google_project"] = strings.TrimSpace(req.GoogleProject)
|
|
}
|
|
if strings.TrimSpace(req.GoogleLocation) != "" {
|
|
update["google_location"] = strings.TrimSpace(req.GoogleLocation)
|
|
} else if provider.IsVertexFamily(nextTypeLower) && strings.TrimSpace(existing.GoogleLocation) == "" {
|
|
update["google_location"] = provider.DefaultGoogleLocation(nextTypeLower, "")
|
|
}
|
|
if req.Models != nil {
|
|
update["models"] = strings.Join(req.Models, ",")
|
|
}
|
|
if strings.TrimSpace(req.Group) != "" {
|
|
update["group"] = groupx.Normalize(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
|
|
}
|
|
if err := h.sync.SyncBindings(h.db); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync bindings", "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"})
|
|
}
|