package admin import ( "encoding/json" "net/http" "slices" "github.com/gin-gonic/gin" "github.com/rose_cat707/luminary/internal/auth" "github.com/rose_cat707/luminary/internal/model" "github.com/rose_cat707/luminary/internal/prediction" "github.com/rose_cat707/luminary/internal/provider" "github.com/rose_cat707/luminary/internal/store/cache" ) func (h *Handler) DeleteProvider(c *gin.Context) { id := parseUint(c.Param("id")) h.db.DB().Where("provider_id = ?", id).Delete(&model.ProviderKey{}) h.db.DB().Where("provider_id = ?", id).Delete(&model.ModelEntry{}) h.db.DB().Delete(&model.Provider{}, id) h.cache.DeleteProvider(id) c.JSON(http.StatusOK, gin.H{"ok": true}) } func (h *Handler) SyncModels(c *gin.Context) { id := parseUint(c.Param("id")) p, ok := h.cache.GetProvider(id) if !ok { c.JSON(http.StatusNotFound, gin.H{"error": "provider not found"}) return } allowed := provider.ParseAllowedEndpoints(p.AllowedEndpointsJSON, p.Category) if !slices.Contains(allowed, string(provider.EndpointListModels)) { c.JSON(http.StatusBadRequest, gin.H{"error": "list_models endpoint not enabled"}) return } models, err := h.gateway.FetchModels(c.Request.Context(), id) if err != nil { c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) return } existing := h.cache.GetModels(id) existingByModelID := make(map[string]model.ModelEntry, len(existing)) for _, e := range existing { existingByModelID[e.ModelID] = e } upstreamIDs := make(map[string]bool, len(models)) var entries []model.ModelEntry for _, m := range models { upstreamIDs[m.ID] = true if old, ok := existingByModelID[m.ID]; ok { old.DisplayName = m.DisplayName if old.DisplayName == "" { old.DisplayName = m.ID } if err := h.db.DB().Save(&old).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } entries = append(entries, old) continue } displayName := m.DisplayName if displayName == "" { displayName = m.ID } entry := model.ModelEntry{ ProviderID: id, ModelID: m.ID, DisplayName: displayName, Enabled: true, } if err := h.db.DB().Create(&entry).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } entries = append(entries, entry) } for _, e := range existing { if !upstreamIDs[e.ModelID] { entries = append(entries, e) } } h.cache.SetModels(id, entries) c.JSON(http.StatusOK, gin.H{"synced": len(models), "models": entries}) } func (h *Handler) ListProviderKeys(c *gin.Context) { id := parseUint(c.Param("id")) c.JSON(http.StatusOK, gin.H{"data": h.cache.ListProviderKeys(id)}) } func (h *Handler) CreateProviderKey(c *gin.Context) { providerID := parseUint(c.Param("id")) var req struct { Name string `json:"name"` APIKey string `json:"api_key"` Weight float64 `json:"weight"` Models []string `json:"models"` Enabled *bool `json:"enabled"` MaxRequestsPerDay int `json:"max_requests_per_day"` MaxTokensPerDay int64 `json:"max_tokens_per_day"` } if err := c.ShouldBindJSON(&req); err != nil || req.Name == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } if req.APIKey == "" { var p model.Provider h.db.DB().First(&p, providerID) if p.Type != model.ProviderOllama { c.JSON(http.StatusBadRequest, gin.H{"error": "api_key required"}) return } req.APIKey = "ollama" } enc, err := auth.Encrypt(req.APIKey, h.settings.EncryptionKey()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "encrypt failed"}) return } modelsJSON := cache.ModelsToJSON(req.Models) if req.Models == nil { modelsJSON = `["*"]` } k := model.ProviderKey{ ProviderID: providerID, Name: req.Name, APIKeyEnc: enc, Weight: req.Weight, ModelsJSON: modelsJSON, Enabled: true, MaxRequestsPerDay: req.MaxRequestsPerDay, MaxTokensPerDay: req.MaxTokensPerDay, } if req.Weight <= 0 { k.Weight = 1 } if req.Enabled != nil { k.Enabled = *req.Enabled } if err := h.db.DB().Create(&k).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } h.cache.SetProviderKey(k) c.JSON(http.StatusCreated, k) } func (h *Handler) UpdateProviderKey(c *gin.Context) { keyID := parseUint(c.Param("keyId")) k, ok := h.cache.GetProviderKey(keyID) if !ok { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return } var req struct { Name string `json:"name"` APIKey string `json:"api_key"` Weight float64 `json:"weight"` Models []string `json:"models"` Enabled *bool `json:"enabled"` MaxRequestsPerDay int `json:"max_requests_per_day"` MaxTokensPerDay int64 `json:"max_tokens_per_day"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } if req.Name != "" { k.Name = req.Name } if req.APIKey != "" { enc, err := auth.Encrypt(req.APIKey, h.settings.EncryptionKey()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "encrypt failed"}) return } k.APIKeyEnc = enc } if req.Weight > 0 { k.Weight = req.Weight } if req.Models != nil { k.ModelsJSON = cache.ModelsToJSON(req.Models) } if req.Enabled != nil { k.Enabled = *req.Enabled } if req.MaxRequestsPerDay >= 0 { k.MaxRequestsPerDay = req.MaxRequestsPerDay } if req.MaxTokensPerDay >= 0 { k.MaxTokensPerDay = req.MaxTokensPerDay } h.db.DB().Save(&k) h.cache.SetProviderKey(k) c.JSON(http.StatusOK, k) } func (h *Handler) DeleteProviderKey(c *gin.Context) { keyID := parseUint(c.Param("keyId")) h.db.DB().Delete(&model.ProviderKey{}, keyID) h.cache.DeleteProviderKey(keyID) c.JSON(http.StatusOK, gin.H{"ok": true}) } func (h *Handler) ListModels(c *gin.Context) { id := parseUint(c.Param("id")) if _, ok := h.cache.GetProvider(id); !ok { c.JSON(http.StatusNotFound, gin.H{"error": "provider not found"}) return } c.JSON(http.StatusOK, gin.H{"data": h.cache.GetModels(id)}) } func (h *Handler) CreateModel(c *gin.Context) { providerID := parseUint(c.Param("id")) prov, ok := h.cache.GetProvider(providerID) if !ok { c.JSON(http.StatusNotFound, gin.H{"error": "provider not found"}) return } var req struct { ModelID string `json:"model_id"` Alias string `json:"alias"` DisplayName string `json:"display_name"` WorkflowType model.WorkflowType `json:"workflow_type"` WorkflowJSON string `json:"workflow_json"` InputBindingJSON string `json:"input_binding_json"` InputBinding map[string]*prediction.NodeField `json:"input_binding"` InputDefaultsJSON string `json:"input_defaults_json"` InputDefaults map[string]any `json:"input_defaults"` } if err := c.ShouldBindJSON(&req); err != nil || req.ModelID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } for _, existing := range h.cache.GetModels(providerID) { if existing.ModelID == req.ModelID { c.JSON(http.StatusBadRequest, gin.H{"error": "model already exists"}) return } } if msg := validateModelAlias(h, req.Alias, 0); msg != "" { c.JSON(http.StatusBadRequest, gin.H{"error": msg}) return } if prov.Category == model.CategoryImage { if err := validateImageModelCreate(struct { ModelID, DisplayName string WorkflowType model.WorkflowType WorkflowJSON, InputBindingJSON string InputBinding map[string]*prediction.NodeField }{req.ModelID, req.DisplayName, req.WorkflowType, req.WorkflowJSON, req.InputBindingJSON, req.InputBinding}); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } } bindingJSON := req.InputBindingJSON if bindingJSON == "" && req.InputBinding != nil { b, _ := json.Marshal(req.InputBinding) bindingJSON = string(b) } defaultsJSON, err := marshalInputDefaults(req.InputDefaultsJSON, req.InputDefaults) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } wt := req.WorkflowType if wt == "" { wt = model.WorkflowTxt2Img } m := model.ModelEntry{ ProviderID: providerID, ModelID: req.ModelID, Alias: normalizeAlias(req.Alias), DisplayName: req.DisplayName, WorkflowType: wt, WorkflowJSON: req.WorkflowJSON, InputBindingJSON: bindingJSON, InputDefaultsJSON: defaultsJSON, VersionHash: prediction.HashWorkflowJSON(req.WorkflowJSON), Enabled: true, } if prov.Category == model.CategoryImage { m.Alias = "" } if m.DisplayName == "" { m.DisplayName = req.ModelID } if err := h.db.DB().Create(&m).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } models := h.cache.GetModels(providerID) models = append(models, m) h.cache.SetModels(providerID, models) c.JSON(http.StatusCreated, m) } func (h *Handler) UpdateModel(c *gin.Context) { providerID := parseUint(c.Param("id")) modelID := parseUint(c.Param("modelId")) var req struct { Enabled *bool `json:"enabled"` DisplayName string `json:"display_name"` Alias *string `json:"alias"` ModelID string `json:"model_id"` WorkflowType model.WorkflowType `json:"workflow_type"` WorkflowJSON string `json:"workflow_json"` InputBindingJSON string `json:"input_binding_json"` InputBinding map[string]*prediction.NodeField `json:"input_binding"` InputDefaultsJSON string `json:"input_defaults_json"` InputDefaults map[string]any `json:"input_defaults"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) return } m, ok := h.findModelEntry(providerID, modelID, req.ModelID) if !ok { c.JSON(http.StatusNotFound, gin.H{"error": "model not found"}) return } if req.Enabled != nil { m.Enabled = *req.Enabled } if req.DisplayName != "" { m.DisplayName = req.DisplayName } if req.Alias != nil { normalized := normalizeAlias(*req.Alias) if msg := validateModelAlias(h, normalized, m.ID); msg != "" { c.JSON(http.StatusBadRequest, gin.H{"error": msg}) return } m.Alias = normalized } if req.ModelID != "" && req.ModelID != m.ModelID { for _, existing := range h.cache.GetModels(m.ProviderID) { if existing.ID != m.ID && existing.ModelID == req.ModelID { c.JSON(http.StatusBadRequest, gin.H{"error": "model already exists"}) return } } m.ModelID = req.ModelID } if req.WorkflowType != "" { m.WorkflowType = req.WorkflowType } if req.WorkflowJSON != "" { m.WorkflowJSON = req.WorkflowJSON m.VersionHash = prediction.HashWorkflowJSON(req.WorkflowJSON) } if req.InputBindingJSON != "" { m.InputBindingJSON = req.InputBindingJSON } else if req.InputBinding != nil { b, _ := json.Marshal(req.InputBinding) m.InputBindingJSON = string(b) } if req.InputDefaults != nil || req.InputDefaultsJSON != "" { defaultsJSON, err := marshalInputDefaults(req.InputDefaultsJSON, req.InputDefaults) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } m.InputDefaultsJSON = defaultsJSON } prov, _ := h.cache.GetProvider(m.ProviderID) if prov.Category == model.CategoryImage { m.Alias = "" } if prov.Category == model.CategoryImage && m.WorkflowJSON != "" { if err := validateImageModelCreate(struct { ModelID, DisplayName string WorkflowType model.WorkflowType WorkflowJSON, InputBindingJSON string InputBinding map[string]*prediction.NodeField }{m.ModelID, m.DisplayName, m.WorkflowType, m.WorkflowJSON, m.InputBindingJSON, nil}); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } } if err := h.db.DB().Save(&m).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } models := h.cache.GetModels(m.ProviderID) for i := range models { if models[i].ID == m.ID { models[i] = m break } } h.cache.SetModels(m.ProviderID, models) c.JSON(http.StatusOK, m) } func (h *Handler) findModelEntry(providerID, entryID uint, modelID string) (model.ModelEntry, bool) { var m model.ModelEntry if entryID > 0 { if err := h.db.DB().First(&m, entryID).Error; err == nil { if providerID > 0 && m.ProviderID != providerID { return model.ModelEntry{}, false } return m, true } } if providerID > 0 && modelID != "" { if err := h.db.DB().Where("provider_id = ? AND model_id = ?", providerID, modelID).First(&m).Error; err == nil { return m, true } } return model.ModelEntry{}, false } func (h *Handler) DeleteModel(c *gin.Context) { modelID := parseUint(c.Param("modelId")) var m model.ModelEntry if err := h.db.DB().First(&m, modelID).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return } if providerID := parseUint(c.Param("id")); providerID > 0 && m.ProviderID != providerID { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return } if err := h.db.DB().Delete(&m).Error; err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } models := h.cache.GetModels(m.ProviderID) filtered := models[:0] for _, item := range models { if item.ID != modelID { filtered = append(filtered, item) } } h.cache.SetModels(m.ProviderID, filtered) c.JSON(http.StatusOK, gin.H{"ok": true}) } func marshalInputDefaults(raw string, defaults map[string]any) (string, error) { if defaults != nil { b, err := json.Marshal(defaults) if err != nil { return "", err } return string(b), nil } if raw == "" { return "{}", nil } if _, err := prediction.ParseInputDefaults(raw); err != nil { return "", err } return raw, nil }