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/foundation/provider" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // CreateProviderGroup godoc // @Summary Create a provider group // @Description Create a provider group definition // @Tags admin // @Accept json // @Produce json // @Security AdminAuth // @Param group body dto.ProviderGroupDTO true "Provider group payload" // @Success 201 {object} model.ProviderGroup // @Failure 400 {object} gin.H // @Failure 500 {object} gin.H // @Router /admin/provider-groups [post] func (h *Handler) CreateProviderGroup(c *gin.Context) { var req dto.ProviderGroupDTO if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } name := strings.TrimSpace(req.Name) if name == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "name required"}) return } ptype := provider.NormalizeType(req.Type) if ptype == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "type required"}) return } baseURL := strings.TrimSpace(req.BaseURL) googleLocation := provider.DefaultGoogleLocation(ptype, req.GoogleLocation) switch ptype { case provider.TypeOpenAI: if baseURL == "" { baseURL = "https://api.openai.com/v1" } case provider.TypeAnthropic, provider.TypeClaude: if baseURL == "" { baseURL = "https://api.anthropic.com" } case provider.TypeCompatible: if baseURL == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "base_url required for compatible providers"}) return } default: if provider.IsVertexFamily(ptype) && strings.TrimSpace(googleLocation) == "" { googleLocation = provider.DefaultGoogleLocation(ptype, "") } } status := strings.TrimSpace(req.Status) if status == "" { status = "active" } group := model.ProviderGroup{ Name: name, Type: strings.TrimSpace(req.Type), BaseURL: baseURL, GoogleProject: strings.TrimSpace(req.GoogleProject), GoogleLocation: googleLocation, Models: strings.Join(req.Models, ","), Status: status, } if err := h.db.Create(&group).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create provider group", "details": err.Error()}) return } if err := h.sync.SyncProviders(h.db); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync providers", "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.StatusCreated, group) } // ListProviderGroups godoc // @Summary List provider groups // @Description List all provider groups // @Tags admin // @Produce json // @Security AdminAuth // @Param page query int false "page (1-based)" // @Param limit query int false "limit (default 50, max 200)" // @Param search query string false "search by name/type" // @Success 200 {array} model.ProviderGroup // @Failure 500 {object} gin.H // @Router /admin/provider-groups [get] func (h *Handler) ListProviderGroups(c *gin.Context) { var groups []model.ProviderGroup q := h.db.Model(&model.ProviderGroup{}).Order("id desc") query := parseListQuery(c) q = applyListSearch(q, query.Search, "name", "type") q = applyListPagination(q, query) if err := q.Find(&groups).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list provider groups", "details": err.Error()}) return } c.JSON(http.StatusOK, groups) } // GetProviderGroup godoc // @Summary Get provider group // @Description Get a provider group by id // @Tags admin // @Produce json // @Security AdminAuth // @Param id path int true "ProviderGroup ID" // @Success 200 {object} model.ProviderGroup // @Failure 400 {object} gin.H // @Failure 404 {object} gin.H // @Failure 500 {object} gin.H // @Router /admin/provider-groups/{id} [get] func (h *Handler) GetProviderGroup(c *gin.Context) { id, ok := parseUintParam(c, "id") if !ok { return } var group model.ProviderGroup if err := h.db.First(&group, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "provider group not found"}) return } c.JSON(http.StatusOK, group) } // UpdateProviderGroup godoc // @Summary Update provider group // @Description Update a provider group // @Tags admin // @Accept json // @Produce json // @Security AdminAuth // @Param id path int true "ProviderGroup ID" // @Param group body dto.ProviderGroupDTO true "Provider group payload" // @Success 200 {object} model.ProviderGroup // @Failure 400 {object} gin.H // @Failure 404 {object} gin.H // @Failure 500 {object} gin.H // @Router /admin/provider-groups/{id} [put] func (h *Handler) UpdateProviderGroup(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 group model.ProviderGroup if err := h.db.First(&group, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "provider group not found"}) return } var req dto.ProviderGroupDTO if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } nextType := strings.TrimSpace(group.Type) if t := strings.TrimSpace(req.Type); t != "" { nextType = t } nextTypeLower := provider.NormalizeType(nextType) nextBaseURL := strings.TrimSpace(group.BaseURL) if strings.TrimSpace(req.BaseURL) != "" { nextBaseURL = strings.TrimSpace(req.BaseURL) } update := map[string]any{} if strings.TrimSpace(req.Name) != "" { update["name"] = strings.TrimSpace(req.Name) } if strings.TrimSpace(req.Type) != "" { update["type"] = strings.TrimSpace(req.Type) } if strings.TrimSpace(req.BaseURL) != "" { update["base_url"] = strings.TrimSpace(req.BaseURL) } 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(group.GoogleLocation) == "" { update["google_location"] = provider.DefaultGoogleLocation(nextTypeLower, "") } if req.Models != nil { update["models"] = strings.Join(req.Models, ",") } if strings.TrimSpace(req.Status) != "" { update["status"] = strings.TrimSpace(req.Status) } switch nextTypeLower { case provider.TypeOpenAI: if nextBaseURL == "" { update["base_url"] = "https://api.openai.com/v1" } case provider.TypeAnthropic, provider.TypeClaude: if nextBaseURL == "" { update["base_url"] = "https://api.anthropic.com" } case provider.TypeCompatible: if nextBaseURL == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "base_url required for compatible providers"}) return } } if err := h.db.Model(&group).Updates(update).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update provider group", "details": err.Error()}) return } if err := h.db.First(&group, id).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reload provider group", "details": err.Error()}) return } if err := h.sync.SyncProviders(h.db); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync providers", "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, group) } // DeleteProviderGroup godoc // @Summary Delete provider group // @Description Delete a provider group and its api keys/bindings // @Tags admin // @Produce json // @Security AdminAuth // @Param id path int true "ProviderGroup ID" // @Success 200 {object} gin.H // @Failure 400 {object} gin.H // @Failure 404 {object} gin.H // @Failure 500 {object} gin.H // @Router /admin/provider-groups/{id} [delete] func (h *Handler) DeleteProviderGroup(c *gin.Context) { id, ok := parseUintParam(c, "id") if !ok { return } var group model.ProviderGroup if err := h.db.First(&group, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "provider group not found"}) return } if err := h.db.Transaction(func(tx *gorm.DB) error { if err := tx.Where("group_id = ?", group.ID).Delete(&model.APIKey{}).Error; err != nil { return err } if err := tx.Where("group_id = ?", group.ID).Delete(&model.Binding{}).Error; err != nil { return err } return tx.Delete(&group).Error }); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete provider group", "details": err.Error()}) return } if err := h.sync.SyncProviders(h.db); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync providers", "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, gin.H{"status": "deleted"}) }