package api import ( "crypto/rand" "encoding/hex" "net/http" "strings" "github.com/ez-api/ez-api/internal/dto" "github.com/ez-api/ez-api/internal/model" groupx "github.com/ez-api/foundation/group" providerx "github.com/ez-api/foundation/provider" "github.com/gin-gonic/gin" ) // CreateProviderPreset godoc // @Summary Create a preset provider // @Description Create an official OpenAI/Anthropic provider (only api_key is typically required) // @Tags admin // @Accept json // @Produce json // @Security AdminAuth // @Param provider body dto.ProviderPresetCreateDTO true "Provider preset payload" // @Success 201 {object} model.Provider // @Failure 400 {object} gin.H // @Failure 500 {object} gin.H // @Router /admin/providers/preset [post] func (h *Handler) CreateProviderPreset(c *gin.Context) { var req dto.ProviderPresetCreateDTO if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } preset := providerx.NormalizeType(req.Preset) if preset == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "preset required"}) return } var providerType string var baseURL string switch preset { case providerx.TypeOpenAI: providerType = providerx.TypeOpenAI baseURL = "https://api.openai.com/v1" case providerx.TypeAnthropic, providerx.TypeClaude: providerType = providerx.TypeAnthropic baseURL = "https://api.anthropic.com" default: c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported preset: " + preset + " (use /admin/providers/google for Google SDK providers)"}) return } name := strings.TrimSpace(req.Name) if name == "" { name = providerType + "-" + randomSuffix(4) } 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 } googleLocation := providerx.DefaultGoogleLocation(providerType, req.GoogleLocation) p := model.Provider{ Name: name, Type: providerType, BaseURL: baseURL, APIKey: strings.TrimSpace(req.APIKey), GoogleProject: strings.TrimSpace(req.GoogleProject), GoogleLocation: googleLocation, Group: groupx.Normalize(group), Models: strings.Join(req.Models, ","), Status: status, AutoBan: autoBan, } if req.Weight > 0 { p.Weight = req.Weight } if err := h.db.Create(&p).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create provider", "details": err.Error()}) return } if err := h.sync.SyncProvider(&p); 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.StatusCreated, p) } // CreateProviderGoogle godoc // @Summary Create a Google SDK provider // @Description Create a Google SDK provider (Gemini API key or Vertex project/location); base_url is not used // @Tags admin // @Accept json // @Produce json // @Security AdminAuth // @Param provider body dto.ProviderGoogleCreateDTO true "Google provider payload" // @Success 201 {object} model.Provider // @Failure 400 {object} gin.H // @Failure 500 {object} gin.H // @Router /admin/providers/google [post] func (h *Handler) CreateProviderGoogle(c *gin.Context) { var req dto.ProviderGoogleCreateDTO if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } pt := providerx.NormalizeType(req.Type) if pt == "" { pt = providerx.TypeGemini } if !providerx.IsGoogleFamily(pt) { c.JSON(http.StatusBadRequest, gin.H{"error": "type must be google family"}) return } name := strings.TrimSpace(req.Name) if name == "" { name = pt + "-" + randomSuffix(4) } 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 } // Validate fields by type. apiKey := strings.TrimSpace(req.APIKey) googleProject := strings.TrimSpace(req.GoogleProject) googleLocation := providerx.DefaultGoogleLocation(pt, req.GoogleLocation) if providerx.IsVertexFamily(pt) { // Vertex uses ADC and project/location; api_key is not required. if strings.TrimSpace(googleLocation) == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "google_location required"}) return } apiKey = "" } else { // Gemini API requires api_key. if apiKey == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "api_key required for gemini api"}) return } googleProject = "" googleLocation = "" } p := model.Provider{ Name: name, Type: pt, BaseURL: "", // intentionally unused for Google SDK APIKey: apiKey, GoogleProject: googleProject, GoogleLocation: googleLocation, Group: groupx.Normalize(group), Models: strings.Join(req.Models, ","), Status: status, AutoBan: autoBan, } if req.Weight > 0 { p.Weight = req.Weight } if err := h.db.Create(&p).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create provider", "details": err.Error()}) return } if err := h.sync.SyncProvider(&p); 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.StatusCreated, p) } // CreateProviderCustom godoc // @Summary Create a custom provider // @Description Create an OpenAI-compatible provider (base_url + api_key required) // @Tags admin // @Accept json // @Produce json // @Security AdminAuth // @Param provider body dto.ProviderCustomCreateDTO true "Provider custom payload" // @Success 201 {object} model.Provider // @Failure 400 {object} gin.H // @Failure 500 {object} gin.H // @Router /admin/providers/custom [post] func (h *Handler) CreateProviderCustom(c *gin.Context) { var req dto.ProviderCustomCreateDTO 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 } baseURL := strings.TrimSpace(req.BaseURL) if baseURL == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "base_url required"}) 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 } p := model.Provider{ Name: name, Type: providerx.TypeCompatible, BaseURL: baseURL, APIKey: strings.TrimSpace(req.APIKey), Group: groupx.Normalize(group), Models: strings.Join(req.Models, ","), Status: status, AutoBan: autoBan, } if req.Weight > 0 { p.Weight = req.Weight } if err := h.db.Create(&p).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create provider", "details": err.Error()}) return } if err := h.sync.SyncProvider(&p); 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.StatusCreated, p) } func randomSuffix(bytesLen int) string { if bytesLen <= 0 { bytesLen = 4 } b := make([]byte, bytesLen) if _, err := rand.Read(b); err != nil { return "rand" } return hex.EncodeToString(b) }