模型定价: 一致性检查与提示(渠道可用模型应全部纳入定价)

- /admin/models 返回每模型 used/needs_pricing/denied + summary(total/unpriced/missing)
- missing 检测孤儿绑定(渠道提供但目录缺失)
- 模型定价页: 缺失警告、在用未定价提示、已禁止/未定价徽章

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Sakurasan
2026-08-16 01:34:22 +08:00
co-authored by Claude
parent b365779188
commit 40c1ae43e9
3 changed files with 101 additions and 4 deletions
+73 -2
View File
@@ -1,6 +1,7 @@
package api
import (
"encoding/json"
"errors"
"net/http"
"strconv"
@@ -11,13 +12,17 @@ import (
"gorm.io/gorm"
)
// AdminModels GET /api/v1/admin/models — 模型列表(含价格与渠道绑定)。
// AdminModels GET /api/v1/admin/models — 模型列表(含价格、渠道绑定、定价/禁止状态)。
func (h *Handler) AdminModels(c *gin.Context) {
var ms []store.Model
if err := h.a.DB.Order("sort ASC, id ASC").Find(&ms).Error; err != nil {
resp.Fail(c, http.StatusInternalServerError, "failed to load models")
return
}
// 全局模型限制策略
allow, deny := h.modelPolicyConfig()
out := make([]gin.H, 0, len(ms))
for _, m := range ms {
var bindings []store.ChannelModelBinding
@@ -29,13 +34,79 @@ func (h *Handler) AdminModels(c *gin.Context) {
"upstream_model": b.UpstreamModel, "weight": b.Weight,
})
}
used := len(bindings) > 0
needsPricing := used && m.InputPrice == 0 && m.OutputPrice == 0 && m.CacheReadPrice == 0
denied := containsStr(deny, m.Name) || (len(allow) > 0 && !containsStr(allow, m.Name))
out = append(out, gin.H{
"id": m.ID, "name": m.Name, "display_name": m.DisplayName,
"input_price": m.InputPrice, "output_price": m.OutputPrice, "cache_read_price": m.CacheReadPrice,
"enabled": m.Enabled, "sort": m.Sort, "channels": chs,
"used": used, "needs_pricing": needsPricing, "denied": denied,
})
}
resp.OK(c, gin.H{"items": out})
// 渠道提供了但目录中缺失的模型(孤儿绑定,数据不一致时出现)
var orphans []struct {
ChannelName string
ModelID uint64
}
h.a.DB.Raw(`SELECT c.name as channel_name, b.model_id
FROM channel_model_bindings b
LEFT JOIN models m ON m.id = b.model_id
LEFT JOIN channels c ON c.id = b.channel_id
WHERE m.id IS NULL`).Scan(&orphans)
missing := make([]gin.H, 0, len(orphans))
for _, o := range orphans {
missing = append(missing, gin.H{"channel": o.ChannelName, "model_id": o.ModelID})
}
// 在用但未定价的模型数(渠道已提供、需定价)
unpriced := 0
{
var usedBindings []struct {
ModelID uint64
}
h.a.DB.Model(&store.ChannelModelBinding{}).Distinct("model_id").Scan(&usedBindings)
usedIDs := map[uint64]bool{}
for _, u := range usedBindings {
usedIDs[u.ModelID] = true
}
for _, m := range ms {
if usedIDs[m.ID] && m.InputPrice == 0 && m.OutputPrice == 0 && m.CacheReadPrice == 0 {
unpriced++
}
}
}
resp.OK(c, gin.H{
"items": out,
"summary": gin.H{
"total": len(ms),
"unpriced": unpriced,
"missing": missing,
"denied_count": len(deny),
},
})
}
// modelPolicyConfig 读取全局模型允许/禁止列表。
func (h *Handler) modelPolicyConfig() (allow, deny []string) {
var raw string
h.a.DB.Model(&store.SystemConfig{}).Where("key = ?", "model_allowlist").Pluck("value", &raw)
_ = json.Unmarshal([]byte(raw), &allow)
raw = ""
h.a.DB.Model(&store.SystemConfig{}).Where("key = ?", "model_denylist").Pluck("value", &raw)
_ = json.Unmarshal([]byte(raw), &deny)
return
}
func containsStr(list []string, s string) bool {
for _, v := range list {
if v == s {
return true
}
}
return false
}
// AdminCreateModel POST /api/v1/admin/models