Gin + GORM + pure-Go SQLite. Users/auth (JWT), API key management with quotas, proxy gateway with weighted channel failover and health checks, usage/billing ledger, cross-protocol conversion (Anthropic Messages / OpenAI Chat Completions / OpenAI Responses), and channel/model admin API. Channels declare native API formats and auto-convert the rest. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
172 lines
4.3 KiB
Go
172 lines
4.3 KiB
Go
package admin
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/shopspring/decimal"
|
|
"go.uber.org/zap"
|
|
"gorm.io/gorm"
|
|
|
|
"openteam/server/internal/pkg/httpx"
|
|
"openteam/server/internal/store"
|
|
)
|
|
|
|
type modelHandler struct {
|
|
db *gorm.DB
|
|
log *zap.Logger
|
|
}
|
|
|
|
type modelInput struct {
|
|
Name string `json:"name" binding:"required"`
|
|
DisplayName string `json:"displayName"`
|
|
InputPrice string `json:"inputPrice"`
|
|
OutputPrice string `json:"outputPrice"`
|
|
CacheReadPrice string `json:"cacheReadPrice"`
|
|
Enabled *bool `json:"enabled"`
|
|
Sort *int `json:"sort"`
|
|
}
|
|
|
|
// List handles GET /api/v1/admin/models.
|
|
func (h *modelHandler) List(c *gin.Context) {
|
|
var models []store.Model
|
|
if err := h.db.Order("sort ASC, id ASC").Find(&models).Error; err != nil {
|
|
h.log.Warn("list models failed", zap.Error(err))
|
|
httpx.Fail(c, http.StatusInternalServerError, "list models failed")
|
|
return
|
|
}
|
|
out := make([]gin.H, 0, len(models))
|
|
for i := range models {
|
|
out = append(out, modelDTO(&models[i]))
|
|
}
|
|
httpx.OK(c, out)
|
|
}
|
|
|
|
// Create handles POST /api/v1/admin/models.
|
|
func (h *modelHandler) Create(c *gin.Context) {
|
|
var in modelInput
|
|
if !httpx.Bind(c, &in) {
|
|
return
|
|
}
|
|
m := &store.Model{
|
|
Name: in.Name,
|
|
DisplayName: in.DisplayName,
|
|
InputPrice: parsePrice(in.InputPrice),
|
|
OutputPrice: parsePrice(in.OutputPrice),
|
|
CacheReadPrice: parsePrice(in.CacheReadPrice),
|
|
Enabled: true,
|
|
}
|
|
if in.Enabled != nil {
|
|
m.Enabled = *in.Enabled
|
|
}
|
|
if in.Sort != nil {
|
|
m.Sort = *in.Sort
|
|
}
|
|
if err := h.db.Create(m).Error; err != nil {
|
|
h.log.Warn("create model failed", zap.Error(err))
|
|
httpx.Fail(c, http.StatusInternalServerError, "create model failed")
|
|
return
|
|
}
|
|
httpx.Created(c, modelDTO(m))
|
|
}
|
|
|
|
// Update handles PUT /api/v1/admin/models/:id.
|
|
func (h *modelHandler) Update(c *gin.Context) {
|
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
httpx.Fail(c, http.StatusBadRequest, "invalid model id")
|
|
return
|
|
}
|
|
var m store.Model
|
|
if err := h.db.First(&m, id).Error; err != nil {
|
|
httpx.Fail(c, http.StatusNotFound, "model not found")
|
|
return
|
|
}
|
|
var in modelInput
|
|
if !httpx.Bind(c, &in) {
|
|
return
|
|
}
|
|
updates := map[string]any{
|
|
"name": in.Name,
|
|
}
|
|
if in.DisplayName != "" {
|
|
updates["display_name"] = in.DisplayName
|
|
}
|
|
if in.InputPrice != "" {
|
|
updates["input_price"] = parsePrice(in.InputPrice)
|
|
}
|
|
if in.OutputPrice != "" {
|
|
updates["output_price"] = parsePrice(in.OutputPrice)
|
|
}
|
|
if in.CacheReadPrice != "" {
|
|
updates["cache_read_price"] = parsePrice(in.CacheReadPrice)
|
|
}
|
|
if in.Enabled != nil {
|
|
updates["enabled"] = *in.Enabled
|
|
}
|
|
if in.Sort != nil {
|
|
updates["sort"] = *in.Sort
|
|
}
|
|
if err := h.db.Model(&m).Updates(updates).Error; err != nil {
|
|
h.log.Warn("update model failed", zap.Error(err))
|
|
httpx.Fail(c, http.StatusInternalServerError, "update model failed")
|
|
return
|
|
}
|
|
h.db.First(&m, id)
|
|
httpx.OK(c, modelDTO(&m))
|
|
}
|
|
|
|
// UpdatePrice handles PUT /api/v1/admin/models/:id/price.
|
|
func (h *modelHandler) UpdatePrice(c *gin.Context) {
|
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
httpx.Fail(c, http.StatusBadRequest, "invalid model id")
|
|
return
|
|
}
|
|
var in struct {
|
|
InputPrice string `json:"inputPrice"`
|
|
OutputPrice string `json:"outputPrice"`
|
|
CacheReadPrice string `json:"cacheReadPrice"`
|
|
}
|
|
if !httpx.Bind(c, &in) {
|
|
return
|
|
}
|
|
updates := map[string]any{}
|
|
if in.InputPrice != "" {
|
|
updates["input_price"] = parsePrice(in.InputPrice)
|
|
}
|
|
if in.OutputPrice != "" {
|
|
updates["output_price"] = parsePrice(in.OutputPrice)
|
|
}
|
|
if in.CacheReadPrice != "" {
|
|
updates["cache_read_price"] = parsePrice(in.CacheReadPrice)
|
|
}
|
|
if len(updates) == 0 {
|
|
httpx.Fail(c, http.StatusBadRequest, "no price fields provided")
|
|
return
|
|
}
|
|
res := h.db.Model(&store.Model{}).Where("id = ?", id).Updates(updates)
|
|
if res.RowsAffected == 0 {
|
|
httpx.Fail(c, http.StatusNotFound, "model not found")
|
|
return
|
|
}
|
|
httpx.OK(c, gin.H{"ok": true})
|
|
}
|
|
|
|
func parsePrice(s string) decimal.Decimal {
|
|
d, err := decimal.NewFromString(s)
|
|
if err != nil {
|
|
return decimal.Zero
|
|
}
|
|
return d.Round(8)
|
|
}
|
|
|
|
func modelDTO(m *store.Model) gin.H {
|
|
return gin.H{
|
|
"id": m.ID, "name": m.Name, "displayName": m.DisplayName,
|
|
"inputPrice": m.InputPrice.String(), "outputPrice": m.OutputPrice.String(),
|
|
"cacheReadPrice": m.CacheReadPrice.String(), "enabled": m.Enabled, "sort": m.Sort,
|
|
}
|
|
}
|