mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
feat(ops): add status test and model fetch
This commit is contained in:
@@ -150,6 +150,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
masterService := service.NewMasterService(db)
|
masterService := service.NewMasterService(db)
|
||||||
healthService := service.NewHealthCheckService(db, rdb)
|
healthService := service.NewHealthCheckService(db, rdb)
|
||||||
|
healthHandler := api.NewHealthHandler(healthService)
|
||||||
|
|
||||||
handler := api.NewHandler(db, logDB, syncService, logWriter, rdb)
|
handler := api.NewHandler(db, logDB, syncService, logWriter, rdb)
|
||||||
adminHandler := api.NewAdminHandler(db, logDB, masterService, syncService)
|
adminHandler := api.NewAdminHandler(db, logDB, masterService, syncService)
|
||||||
@@ -201,6 +202,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
c.JSON(httpStatus, status)
|
c.JSON(httpStatus, status)
|
||||||
})
|
})
|
||||||
|
r.GET("/api/status/test", healthHandler.TestDeps)
|
||||||
|
|
||||||
// Swagger Documentation
|
// Swagger Documentation
|
||||||
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||||
@@ -245,6 +247,7 @@ func main() {
|
|||||||
adminGroup.PUT("/providers/:id", handler.UpdateProvider)
|
adminGroup.PUT("/providers/:id", handler.UpdateProvider)
|
||||||
adminGroup.DELETE("/providers/:id", handler.DeleteProvider)
|
adminGroup.DELETE("/providers/:id", handler.DeleteProvider)
|
||||||
adminGroup.POST("/providers/:id/test", handler.TestProvider)
|
adminGroup.POST("/providers/:id/test", handler.TestProvider)
|
||||||
|
adminGroup.POST("/providers/:id/fetch-models", handler.FetchProviderModels)
|
||||||
adminGroup.POST("/models", handler.CreateModel)
|
adminGroup.POST("/models", handler.CreateModel)
|
||||||
adminGroup.GET("/models", handler.ListModels)
|
adminGroup.GET("/models", handler.ListModels)
|
||||||
adminGroup.PUT("/models/:id", handler.UpdateModel)
|
adminGroup.PUT("/models/:id", handler.UpdateModel)
|
||||||
|
|||||||
37
internal/api/health_handler.go
Normal file
37
internal/api/health_handler.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/ez-api/ez-api/internal/service"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HealthHandler struct {
|
||||||
|
svc *service.HealthCheckService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHealthHandler(svc *service.HealthCheckService) *HealthHandler {
|
||||||
|
return &HealthHandler{svc: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDeps godoc
|
||||||
|
// @Summary Test dependency connectivity
|
||||||
|
// @Description Checks Redis/PostgreSQL connections and reports status
|
||||||
|
// @Tags system
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} service.HealthStatus
|
||||||
|
// @Failure 503 {object} service.HealthStatus
|
||||||
|
// @Router /api/status/test [get]
|
||||||
|
func (h *HealthHandler) TestDeps(c *gin.Context) {
|
||||||
|
if h == nil || h.svc == nil {
|
||||||
|
c.JSON(http.StatusServiceUnavailable, gin.H{"status": "down"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
status := h.svc.Check(c.Request.Context())
|
||||||
|
httpStatus := http.StatusOK
|
||||||
|
if status.Status == "down" {
|
||||||
|
httpStatus = http.StatusServiceUnavailable
|
||||||
|
}
|
||||||
|
c.JSON(httpStatus, status)
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -123,49 +126,16 @@ func (h *Handler) TestProvider(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pt := provider.NormalizeType(p.Type)
|
req, err := buildProviderModelsRequest(&p)
|
||||||
baseURL := strings.TrimRight(strings.TrimSpace(p.BaseURL), "/")
|
if err != nil {
|
||||||
if baseURL == "" {
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
client := &http.Client{Timeout: 10 * time.Second}
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, testProviderResponse{StatusCode: 0, OK: false, URL: url, Body: err.Error()})
|
c.JSON(http.StatusOK, testProviderResponse{StatusCode: 0, OK: false, URL: req.URL.String(), Body: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
@@ -176,7 +146,196 @@ func (h *Handler) TestProvider(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, testProviderResponse{
|
c.JSON(http.StatusOK, testProviderResponse{
|
||||||
StatusCode: resp.StatusCode,
|
StatusCode: resp.StatusCode,
|
||||||
OK: ok,
|
OK: ok,
|
||||||
URL: url,
|
URL: req.URL.String(),
|
||||||
Body: string(body),
|
Body: string(body),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FetchProviderModels godoc
|
||||||
|
// @Summary Fetch models from provider
|
||||||
|
// @Description Calls upstream /models (or /v1/models) and updates provider model list
|
||||||
|
// @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 502 {object} gin.H
|
||||||
|
// @Router /admin/providers/{id}/fetch-models [post]
|
||||||
|
func (h *Handler) FetchProviderModels(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
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := buildProviderModelsRequest(&p)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 15 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadGateway, gin.H{"error": "failed to fetch models", "details": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
c.JSON(http.StatusBadGateway, gin.H{
|
||||||
|
"error": "upstream returned non-2xx",
|
||||||
|
"status_code": resp.StatusCode,
|
||||||
|
"body": string(body),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
models, err := parseProviderModelIDs(body)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadGateway, gin.H{"error": "failed to parse models", "details": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.db.Model(&p).Update("models", strings.Join(models, ",")).Error; err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update provider models", "details": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.db.First(&p, id).Error; err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to reload 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.StatusOK, gin.H{
|
||||||
|
"status": "updated",
|
||||||
|
"count": len(models),
|
||||||
|
"models": models,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildProviderModelsRequest(p *model.Provider) (*http.Request, error) {
|
||||||
|
if p == nil {
|
||||||
|
return nil, fmt.Errorf("provider required")
|
||||||
|
}
|
||||||
|
pt := provider.NormalizeType(p.Type)
|
||||||
|
baseURL := strings.TrimRight(strings.TrimSpace(p.BaseURL), "/")
|
||||||
|
if baseURL == "" {
|
||||||
|
return nil, fmt.Errorf("base_url required for provider models fetch")
|
||||||
|
}
|
||||||
|
|
||||||
|
url := ""
|
||||||
|
switch pt {
|
||||||
|
case provider.TypeOpenAI, provider.TypeCompatible:
|
||||||
|
if strings.HasSuffix(baseURL, "/v1") {
|
||||||
|
url = baseURL + "/models"
|
||||||
|
} else {
|
||||||
|
url = baseURL + "/v1/models"
|
||||||
|
}
|
||||||
|
case provider.TypeAnthropic, provider.TypeClaude:
|
||||||
|
url = baseURL + "/v1/models"
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("provider type not supported for model fetch")
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("build request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
apiKey := strings.TrimSpace(p.APIKey)
|
||||||
|
switch pt {
|
||||||
|
case provider.TypeOpenAI, provider.TypeCompatible:
|
||||||
|
if apiKey != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||||
|
}
|
||||||
|
case provider.TypeAnthropic, provider.TypeClaude:
|
||||||
|
if apiKey != "" {
|
||||||
|
req.Header.Set("x-api-key", apiKey)
|
||||||
|
}
|
||||||
|
req.Header.Set("anthropic-version", "2023-06-01")
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type providerModelsResponse struct {
|
||||||
|
Data []json.RawMessage `json:"data"`
|
||||||
|
Models []string `json:"models"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseProviderModelIDs(payload []byte) ([]string, error) {
|
||||||
|
var resp providerModelsResponse
|
||||||
|
if err := json.Unmarshal(payload, &resp); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
models := make([]string, 0, len(resp.Data)+len(resp.Models))
|
||||||
|
for _, name := range resp.Models {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if name != "" {
|
||||||
|
models = append(models, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, raw := range resp.Data {
|
||||||
|
var item struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &item); err == nil {
|
||||||
|
if item.ID != "" {
|
||||||
|
models = append(models, strings.TrimSpace(item.ID))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if item.Model != "" {
|
||||||
|
models = append(models, strings.TrimSpace(item.Model))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if item.Name != "" {
|
||||||
|
models = append(models, strings.TrimSpace(item.Name))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var name string
|
||||||
|
if err := json.Unmarshal(raw, &name); err == nil {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if name != "" {
|
||||||
|
models = append(models, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unique := make(map[string]struct{}, len(models))
|
||||||
|
out := make([]string, 0, len(models))
|
||||||
|
for _, name := range models {
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := unique[name]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
unique[name] = struct{}{}
|
||||||
|
out = append(out, name)
|
||||||
|
}
|
||||||
|
if len(out) == 0 {
|
||||||
|
return nil, fmt.Errorf("no models found in response")
|
||||||
|
}
|
||||||
|
sort.Strings(out)
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user