feat(ops): add status test and model fetch

This commit is contained in:
zenfun
2025-12-21 20:22:56 +08:00
parent 6e6c669ea0
commit f7baa0f08f
3 changed files with 237 additions and 38 deletions

View File

@@ -1,8 +1,11 @@
package api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"sort"
"strings"
"time"
@@ -123,49 +126,16 @@ func (h *Handler) TestProvider(c *gin.Context) {
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"})
req, err := buildProviderModelsRequest(&p)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
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()})
c.JSON(http.StatusOK, testProviderResponse{StatusCode: 0, OK: false, URL: req.URL.String(), Body: err.Error()})
return
}
defer resp.Body.Close()
@@ -176,7 +146,196 @@ func (h *Handler) TestProvider(c *gin.Context) {
c.JSON(http.StatusOK, testProviderResponse{
StatusCode: resp.StatusCode,
OK: ok,
URL: url,
URL: req.URL.String(),
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
}