package api import ( "io" "net/http" "strings" "time" "github.com/ez-api/ez-api/internal/model" "github.com/ez-api/foundation/provider" "github.com/gin-gonic/gin" ) // ListProviders godoc // @Summary List providers // @Description List all configured upstream providers // @Tags admin // @Produce json // @Security AdminAuth // @Success 200 {array} model.Provider // @Failure 500 {object} gin.H // @Router /admin/providers [get] func (h *Handler) ListProviders(c *gin.Context) { var providers []model.Provider if err := h.db.Order("id desc").Find(&providers).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list providers", "details": err.Error()}) return } c.JSON(http.StatusOK, providers) } // GetProvider godoc // @Summary Get provider // @Description Get a provider by id // @Tags admin // @Produce json // @Security AdminAuth // @Param id path int true "Provider ID" // @Success 200 {object} model.Provider // @Failure 400 {object} gin.H // @Failure 404 {object} gin.H // @Failure 500 {object} gin.H // @Router /admin/providers/{id} [get] func (h *Handler) GetProvider(c *gin.Context) { id, ok := parseUintParam(c, "id") if !ok { return } var p model.Provider if err := h.db.First(&p, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "provider not found"}) return } c.JSON(http.StatusOK, p) } // DeleteProvider godoc // @Summary Delete provider // @Description Deletes a provider and triggers a full snapshot sync to avoid stale routing // @Tags admin // @Produce json // @Security AdminAuth // @Param id path int true "Provider ID" // @Success 200 {object} gin.H // @Failure 400 {object} gin.H // @Failure 404 {object} gin.H // @Failure 500 {object} gin.H // @Router /admin/providers/{id} [delete] func (h *Handler) DeleteProvider(c *gin.Context) { id, ok := parseUintParam(c, "id") if !ok { return } var p model.Provider if err := h.db.First(&p, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "provider not found"}) return } if err := h.db.Delete(&p).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete provider", "details": err.Error()}) return } // Full sync is the simplest safe option because provider deletion needs to remove // stale entries in Redis snapshots (config:providers) and refresh bindings. 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": "deleted"}) } type testProviderResponse struct { StatusCode int `json:"status_code"` OK bool `json:"ok"` URL string `json:"url"` Body string `json:"body,omitempty"` } // TestProvider godoc // @Summary Test provider connectivity // @Description Performs a lightweight upstream request to verify the provider configuration // @Tags admin // @Produce json // @Security AdminAuth // @Param id path int true "Provider ID" // @Success 200 {object} testProviderResponse // @Failure 400 {object} gin.H // @Failure 404 {object} gin.H // @Failure 500 {object} gin.H // @Router /admin/providers/{id}/test [post] func (h *Handler) TestProvider(c *gin.Context) { id, ok := parseUintParam(c, "id") if !ok { return } var p model.Provider if err := h.db.First(&p, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "provider not found"}) return } pt := provider.NormalizeType(p.Type) baseURL := strings.TrimRight(strings.TrimSpace(p.BaseURL), "/") if baseURL == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "base_url required for provider test"}) return } url := "" var req *http.Request switch pt { case provider.TypeOpenAI, provider.TypeCompatible: url = baseURL + "/models" r, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to build request", "details": err.Error()}) return } req = r if strings.TrimSpace(p.APIKey) != "" { req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(p.APIKey)) } case provider.TypeAnthropic, provider.TypeClaude: url = baseURL + "/v1/models" r, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to build request", "details": err.Error()}) return } req = r if strings.TrimSpace(p.APIKey) != "" { req.Header.Set("x-api-key", strings.TrimSpace(p.APIKey)) } req.Header.Set("anthropic-version", "2023-06-01") default: c.JSON(http.StatusBadRequest, gin.H{"error": "provider type not supported for test"}) return } client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { c.JSON(http.StatusOK, testProviderResponse{StatusCode: 0, OK: false, URL: url, Body: err.Error()}) return } defer resp.Body.Close() body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048)) ok = resp.StatusCode >= 200 && resp.StatusCode < 300 c.JSON(http.StatusOK, testProviderResponse{ StatusCode: resp.StatusCode, OK: ok, URL: url, Body: string(body), }) }