Files
Luminary/internal/handler/admin/providers.go
T
renjue 76ba500417
CI / docker (push) Successful in 2m4s
Initial commit: Luminary AI Gateway
OpenAI-compatible AI gateway with Vue admin UI, multi-provider egress,
ingress key governance, monitoring, and security controls.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 21:46:16 +08:00

197 lines
6.2 KiB
Go

package admin
import (
"encoding/json"
"net/http"
"github.com/gin-gonic/gin"
"github.com/rose_cat707/luminary/internal/model"
"github.com/rose_cat707/luminary/internal/provider"
"github.com/rose_cat707/luminary/internal/store/cache"
)
func (h *Handler) ListEndpointMeta(c *gin.Context) {
category := model.ProviderCategory(c.Query("category"))
if category == "" {
category = model.CategoryLLM
}
providerType := model.ProviderType(c.Query("type"))
if providerType == "" {
providerType = provider.DefaultProviderType(category)
}
endpoints := provider.EndpointsForCategory(category)
providerTypes := provider.TypesForCategory(category)
type item struct {
provider.EndpointMeta
DefaultPath string `json:"default_path"`
}
out := make([]item, 0, len(endpoints))
for _, e := range endpoints {
out = append(out, item{
EndpointMeta: e,
DefaultPath: provider.DefaultPath(providerType, e.Key),
})
}
c.JSON(http.StatusOK, gin.H{
"categories": []gin.H{
{"key": model.CategoryLLM, "label": provider.CategoryLabel(model.CategoryLLM), "supported": true},
{"key": model.CategoryEmbedding, "label": provider.CategoryLabel(model.CategoryEmbedding), "supported": true},
{"key": model.CategoryRerank, "label": provider.CategoryLabel(model.CategoryRerank), "supported": true},
{"key": model.CategorySpeech, "label": provider.CategoryLabel(model.CategorySpeech), "supported": false},
{"key": model.CategoryImage, "label": provider.CategoryLabel(model.CategoryImage), "supported": true},
{"key": model.CategoryAudio, "label": provider.CategoryLabel(model.CategoryAudio), "supported": false},
},
"provider_types": providerTypes,
"all_provider_types": provider.AllProviderTypes(),
"endpoints": out,
})
}
func (h *Handler) ListProviders(c *gin.Context) {
category := c.Query("category")
providers := h.cache.ListProviders()
if category != "" {
filtered := providers[:0]
for _, p := range providers {
if string(p.Category) == category {
filtered = append(filtered, p)
}
}
providers = filtered
}
out := make([]model.Provider, 0, len(providers))
for _, p := range providers {
p.Models = h.cache.GetModels(p.ID)
out = append(out, p)
}
c.JSON(http.StatusOK, gin.H{"data": out})
}
func (h *Handler) CreateProvider(c *gin.Context) {
var req struct {
Name string `json:"name"`
Type model.ProviderType `json:"type"`
Category model.ProviderCategory `json:"category"`
BaseURL string `json:"base_url"`
AllowedEndpoints []string `json:"allowed_endpoints"`
EndpointPaths map[string]string `json:"endpoint_paths"`
}
if err := c.ShouldBindJSON(&req); err != nil || req.Name == "" || req.Type == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
return
}
if req.Category == "" {
req.Category = model.CategoryLLM
}
if !model.IsCategorySupported(req.Category) {
c.JSON(http.StatusBadRequest, gin.H{"error": "category not supported"})
return
}
if !provider.IsTypeValidForCategory(req.Category, req.Type) {
c.JSON(http.StatusBadRequest, gin.H{"error": "protocol type not supported for this category"})
return
}
if h.providerNameTaken(req.Name, 0) {
c.JSON(http.StatusBadRequest, gin.H{"error": "provider name already exists"})
return
}
allowed := req.AllowedEndpoints
if len(allowed) == 0 {
allowed = provider.DefaultEndpointsForCategory(req.Category)
}
if err := provider.ValidateCategoryEndpoints(req.Category, allowed); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
pathsJSON, _ := json.Marshal(req.EndpointPaths)
if req.EndpointPaths == nil {
pathsJSON = []byte("{}")
}
p := model.Provider{
Name: req.Name,
Type: req.Type,
Category: req.Category,
BaseURL: req.BaseURL,
AllowedEndpointsJSON: cache.ModelsToJSON(allowed),
EndpointPathsJSON: string(pathsJSON),
Status: model.ProviderUnknown,
}
if err := h.db.DB().Create(&p).Error; err != nil {
if h.providerNameTaken(req.Name, 0) {
c.JSON(http.StatusBadRequest, gin.H{"error": "provider name already exists"})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
h.cache.SetProvider(p)
c.JSON(http.StatusCreated, p)
}
func (h *Handler) GetProvider(c *gin.Context) {
id := parseUint(c.Param("id"))
p, ok := h.cache.GetProvider(id)
if !ok {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
p.Keys = h.cache.ListProviderKeys(id)
p.Models = h.cache.GetModels(id)
c.JSON(http.StatusOK, p)
}
func (h *Handler) UpdateProvider(c *gin.Context) {
id := parseUint(c.Param("id"))
p, ok := h.cache.GetProvider(id)
if !ok {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
var req struct {
Name string `json:"name"`
BaseURL string `json:"base_url"`
Category model.ProviderCategory `json:"category"`
AllowedEndpoints []string `json:"allowed_endpoints"`
EndpointPaths map[string]string `json:"endpoint_paths"`
Enabled *bool `json:"enabled"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
return
}
if req.Name != "" && req.Name != p.Name {
if h.providerNameTaken(req.Name, p.ID) {
c.JSON(http.StatusBadRequest, gin.H{"error": "provider name already exists"})
return
}
p.Name = req.Name
}
if req.BaseURL != "" {
p.BaseURL = req.BaseURL
}
if req.Category != "" {
if !model.IsCategorySupported(req.Category) {
c.JSON(http.StatusBadRequest, gin.H{"error": "category not supported yet"})
return
}
p.Category = req.Category
}
if req.AllowedEndpoints != nil {
if err := provider.ValidateCategoryEndpoints(p.Category, req.AllowedEndpoints); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
p.AllowedEndpointsJSON = cache.ModelsToJSON(req.AllowedEndpoints)
}
if req.EndpointPaths != nil {
b, _ := json.Marshal(req.EndpointPaths)
p.EndpointPathsJSON = string(b)
}
if req.Enabled != nil {
p.Enabled = *req.Enabled
}
h.db.DB().Save(&p)
h.cache.SetProvider(p)
c.JSON(http.StatusOK, p)
}