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) } // 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"}) }