✨ feat: enhance model billing aggregation & UI display for unknown quota type
Summary
-------
1. **Backend**
• `controller/model_meta.go`
– For prefix/suffix/contains rules, aggregate endpoints, bound channels, enable groups, and quota types across all matched models.
– When mixed billing types are detected, return `quota_type = -1` (unknown) instead of defaulting to volume-based.
2. **Frontend**
• `web/src/helpers/utils.js`
– `calculateModelPrice` now handles `quota_type = -1`, returning placeholder `'-'`.
• `web/src/components/table/model-pricing/view/card/PricingCardView.jsx`
– Billing tag logic updated: displays “按次计费” (times), “按量计费” (volume), or `'-'` for unknown.
• `web/src/components/table/model-pricing/view/table/PricingTableColumns.js`
– `renderQuotaType` shows “未知” for unknown billing type.
• `web/src/components/table/models/ModelsColumnDefs.js`
– Unified `renderQuotaType` to return `'-'` when type is unknown.
• `web/src/components/table/model-pricing/modal/components/ModelPricingTable.jsx`
– Group price table honors unknown billing type; pricing columns show `'-'` and neutral tag color.
3. **Utilities**
• Added safe fallback colours/tags for unknown billing type across affected components.
Impact
------
• Ensures correct data aggregation for non-exact model matches.
• Prevents UI from implying volume billing when actual type is ambiguous.
• Provides consistent placeholder display (`'-'` or “未知”) across cards, tables and modals.
No breaking API changes; frontend gracefully handles legacy values.
This commit is contained in:
@@ -3,8 +3,10 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
|
"one-api/constant"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -162,17 +164,105 @@ func DeleteModelMeta(c *gin.Context) {
|
|||||||
|
|
||||||
// 辅助函数:填充 Endpoints 和 BoundChannels 和 EnableGroups
|
// 辅助函数:填充 Endpoints 和 BoundChannels 和 EnableGroups
|
||||||
func fillModelExtra(m *model.Model) {
|
func fillModelExtra(m *model.Model) {
|
||||||
if m.Endpoints == "" {
|
// 若为精确匹配,保持原有逻辑
|
||||||
eps := model.GetModelSupportEndpointTypes(m.ModelName)
|
if m.NameRule == model.NameRuleExact {
|
||||||
|
if m.Endpoints == "" {
|
||||||
|
eps := model.GetModelSupportEndpointTypes(m.ModelName)
|
||||||
|
if b, err := json.Marshal(eps); err == nil {
|
||||||
|
m.Endpoints = string(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if channels, err := model.GetBoundChannels(m.ModelName); err == nil {
|
||||||
|
m.BoundChannels = channels
|
||||||
|
}
|
||||||
|
m.EnableGroups = model.GetModelEnableGroups(m.ModelName)
|
||||||
|
m.QuotaType = model.GetModelQuotaType(m.ModelName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非精确匹配:计算并集
|
||||||
|
pricings := model.GetPricing()
|
||||||
|
|
||||||
|
// 端点去重集合
|
||||||
|
endpointSet := make(map[constant.EndpointType]struct{})
|
||||||
|
// 已绑定渠道去重集合
|
||||||
|
channelSet := make(map[string]model.BoundChannel)
|
||||||
|
// 分组去重集合
|
||||||
|
groupSet := make(map[string]struct{})
|
||||||
|
// 计费类型(若有任意模型为 1,则返回 1)
|
||||||
|
quotaTypeSet := make(map[int]struct{})
|
||||||
|
|
||||||
|
for _, p := range pricings {
|
||||||
|
var matched bool
|
||||||
|
switch m.NameRule {
|
||||||
|
case model.NameRulePrefix:
|
||||||
|
matched = strings.HasPrefix(p.ModelName, m.ModelName)
|
||||||
|
case model.NameRuleSuffix:
|
||||||
|
matched = strings.HasSuffix(p.ModelName, m.ModelName)
|
||||||
|
case model.NameRuleContains:
|
||||||
|
matched = strings.Contains(p.ModelName, m.ModelName)
|
||||||
|
}
|
||||||
|
if !matched {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集端点
|
||||||
|
for _, et := range p.SupportedEndpointTypes {
|
||||||
|
endpointSet[et] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集分组
|
||||||
|
for _, g := range p.EnableGroup {
|
||||||
|
groupSet[g] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集计费类型
|
||||||
|
quotaTypeSet[p.QuotaType] = struct{}{}
|
||||||
|
|
||||||
|
// 收集渠道
|
||||||
|
if channels, err := model.GetBoundChannels(p.ModelName); err == nil {
|
||||||
|
for _, ch := range channels {
|
||||||
|
key := ch.Name + "_" + strconv.Itoa(ch.Type)
|
||||||
|
channelSet[key] = ch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 序列化端点
|
||||||
|
if len(endpointSet) > 0 && m.Endpoints == "" {
|
||||||
|
eps := make([]constant.EndpointType, 0, len(endpointSet))
|
||||||
|
for et := range endpointSet {
|
||||||
|
eps = append(eps, et)
|
||||||
|
}
|
||||||
if b, err := json.Marshal(eps); err == nil {
|
if b, err := json.Marshal(eps); err == nil {
|
||||||
m.Endpoints = string(b)
|
m.Endpoints = string(b)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if channels, err := model.GetBoundChannels(m.ModelName); err == nil {
|
|
||||||
|
// 序列化渠道
|
||||||
|
if len(channelSet) > 0 {
|
||||||
|
channels := make([]model.BoundChannel, 0, len(channelSet))
|
||||||
|
for _, ch := range channelSet {
|
||||||
|
channels = append(channels, ch)
|
||||||
|
}
|
||||||
m.BoundChannels = channels
|
m.BoundChannels = channels
|
||||||
}
|
}
|
||||||
// 填充启用分组
|
|
||||||
m.EnableGroups = model.GetModelEnableGroups(m.ModelName)
|
// 序列化分组
|
||||||
// 填充计费类型
|
if len(groupSet) > 0 {
|
||||||
m.QuotaType = model.GetModelQuotaType(m.ModelName)
|
groups := make([]string, 0, len(groupSet))
|
||||||
|
for g := range groupSet {
|
||||||
|
groups = append(groups, g)
|
||||||
|
}
|
||||||
|
m.EnableGroups = groups
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确定计费类型:仅当所有匹配模型计费类型一致时才返回该类型,否则返回 -1 表示未知/不确定
|
||||||
|
if len(quotaTypeSet) == 1 {
|
||||||
|
for k := range quotaTypeSet {
|
||||||
|
m.QuotaType = k
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m.QuotaType = -1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ const ModelPricingTable = ({
|
|||||||
key: group,
|
key: group,
|
||||||
group: group,
|
group: group,
|
||||||
ratio: groupRatioValue,
|
ratio: groupRatioValue,
|
||||||
billingType: modelData?.quota_type === 0 ? t('按量计费') : t('按次计费'),
|
billingType: modelData?.quota_type === 0 ? t('按量计费') : (modelData?.quota_type === 1 ? t('按次计费') : '-'),
|
||||||
inputPrice: modelData?.quota_type === 0 ? priceData.inputPrice : '-',
|
inputPrice: modelData?.quota_type === 0 ? priceData.inputPrice : '-',
|
||||||
outputPrice: modelData?.quota_type === 0 ? (priceData.completionPrice || priceData.outputPrice) : '-',
|
outputPrice: modelData?.quota_type === 0 ? (priceData.completionPrice || priceData.outputPrice) : '-',
|
||||||
fixedPrice: modelData?.quota_type === 1 ? priceData.price : '-',
|
fixedPrice: modelData?.quota_type === 1 ? priceData.price : '-',
|
||||||
@@ -100,11 +100,16 @@ const ModelPricingTable = ({
|
|||||||
columns.push({
|
columns.push({
|
||||||
title: t('计费类型'),
|
title: t('计费类型'),
|
||||||
dataIndex: 'billingType',
|
dataIndex: 'billingType',
|
||||||
render: (text) => (
|
render: (text) => {
|
||||||
<Tag color={text === t('按量计费') ? 'violet' : 'teal'} size="small" shape="circle">
|
let color = 'white';
|
||||||
{text}
|
if (text === t('按量计费')) color = 'violet';
|
||||||
</Tag>
|
else if (text === t('按次计费')) color = 'teal';
|
||||||
),
|
return (
|
||||||
|
<Tag color={color} size="small" shape="circle">
|
||||||
|
{text || '-'}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 根据计费类型添加价格列
|
// 根据计费类型添加价格列
|
||||||
|
|||||||
@@ -144,13 +144,24 @@ const PricingCardView = ({
|
|||||||
// 渲染标签
|
// 渲染标签
|
||||||
const renderTags = (record) => {
|
const renderTags = (record) => {
|
||||||
// 计费类型标签(左边)
|
// 计费类型标签(左边)
|
||||||
const billingType = record.quota_type === 1 ? 'teal' : 'violet';
|
let billingTag = (
|
||||||
const billingText = record.quota_type === 1 ? t('按次计费') : t('按量计费');
|
<Tag key="billing" shape='circle' color='white' size='small'>
|
||||||
const billingTag = (
|
-
|
||||||
<Tag key="billing" shape='circle' color={billingType} size='small'>
|
|
||||||
{billingText}
|
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
|
if (record.quota_type === 1) {
|
||||||
|
billingTag = (
|
||||||
|
<Tag key="billing" shape='circle' color='teal' size='small'>
|
||||||
|
{t('按次计费')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
} else if (record.quota_type === 0) {
|
||||||
|
billingTag = (
|
||||||
|
<Tag key="billing" shape='circle' color='violet' size='small'>
|
||||||
|
{t('按量计费')}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 自定义标签(右边)
|
// 自定义标签(右边)
|
||||||
const customTags = [];
|
const customTags = [];
|
||||||
|
|||||||
@@ -137,7 +137,8 @@ const renderQuotaType = (qt, t) => {
|
|||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return qt ?? '-';
|
// 未知
|
||||||
|
return '-';
|
||||||
};
|
};
|
||||||
|
|
||||||
// Render bound channels
|
// Render bound channels
|
||||||
|
|||||||
@@ -632,12 +632,22 @@ export const calculateModelPrice = ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按次计费
|
if (record.quota_type === 1) {
|
||||||
const priceUSD = parseFloat(record.model_price) * usedGroupRatio;
|
// 按次计费
|
||||||
const displayVal = displayPrice(priceUSD);
|
const priceUSD = parseFloat(record.model_price) * usedGroupRatio;
|
||||||
|
const displayVal = displayPrice(priceUSD);
|
||||||
|
|
||||||
|
return {
|
||||||
|
price: displayVal,
|
||||||
|
isPerToken: false,
|
||||||
|
usedGroup,
|
||||||
|
usedGroupRatio,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未知计费类型,返回占位信息
|
||||||
return {
|
return {
|
||||||
price: displayVal,
|
price: '-',
|
||||||
isPerToken: false,
|
isPerToken: false,
|
||||||
usedGroup,
|
usedGroup,
|
||||||
usedGroupRatio,
|
usedGroupRatio,
|
||||||
|
|||||||
Reference in New Issue
Block a user