Files
SakurasanandClaude Sonnet 5 d0e31b198f feat(server): API relay gateway backend M0-M4
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>
2026-08-15 21:05:02 +08:00

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,
}
}