feat(billing): 域名计费系数升级为二维(域名 x 命中分组)

DomainRatio 结构 {域名:{分组:系数, "*":默认}}; 按 auto 熔断后实际命中分组取系数,
回退 "*", 再无则 1。不限 auto, 灵活到 域名x分组 粒度。
price.go 用 relayInfo.UsingGroup(命中分组) 查 GetDomainRatioForGroup。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 03:21:46 +08:00
parent 380753d13f
commit 538122d36b
2 changed files with 34 additions and 21 deletions

View File

@@ -61,9 +61,8 @@ func HandleGroupRatio(ctx *gin.Context, relayInfo *relaycommon.RelayInfo) types.
groupRatioInfo.GroupRatio = ratio_setting.GetGroupRatio(relayInfo.UsingGroup)
}
// 域名计费系数:同一个令牌(含 auto 分组)按请求所用的对外域名再乘一个系数。
// 用于"同一个 key、不同域名、不同价"的纯价差场景,完全不影响 auto 的分组选择/熔断
// 未配置任何域名系数时直接跳过,对现有计费零影响。
// 域名计费系数(二维: 域名 × 实际命中分组):同一个令牌(含 auto)按 "对外域名 + 命中分组" 再乘系数。
// 此处 relayInfo.UsingGroup 已是 auto 熔断后实际命中的分组。未配置则跳过, 对现有计费零影响
if ratio_setting.HasDomainRatioConfigured() {
host := ctx.GetHeader("X-Forwarded-Host")
if host == "" {
@@ -73,12 +72,12 @@ func HandleGroupRatio(ctx *gin.Context, relayInfo *relaycommon.RelayInfo) types.
if i := strings.IndexByte(host, ','); i != -1 {
host = host[:i]
}
if dr := ratio_setting.GetDomainRatio(host); dr != 1 {
if dr := ratio_setting.GetDomainRatioForGroup(host, relayInfo.UsingGroup); dr != 1 {
groupRatioInfo.GroupRatio *= dr
if groupRatioInfo.HasSpecialRatio {
groupRatioInfo.GroupSpecialRatio *= dr
}
logger.LogDebug(ctx, fmt.Sprintf("domain ratio applied: host=%s ratio=%f finalGroupRatio=%f", host, dr, groupRatioInfo.GroupRatio))
logger.LogDebug(ctx, fmt.Sprintf("domain ratio applied: host=%s group=%s ratio=%f finalGroupRatio=%f", host, relayInfo.UsingGroup, dr, groupRatioInfo.GroupRatio))
}
}

View File

@@ -7,18 +7,26 @@ import (
"github.com/QuantumNous/new-api/types"
)
// 域名计费系数:同一个令牌(可为 auto 分组),按请求所用的对外域名再乘一个额外系数。
// 最终倍率 = auto命中分组倍率 × domainRatio。
// 用于"同一个 key、不同域名、不同价"的纯价差场景(同上游, 不破坏 auto 的选择/熔断)
// key 为对外域名(小写, 不含端口), value 为系数。例:
// 域名计费系数(二维):同一个令牌(可为 auto 分组),按 "请求对外域名 × 实际命中分组" 再乘一个系数。
// 最终倍率 = 模型倍率 × 命中分组倍率 × DomainRatio[域名][命中分组 | "*"]
// 用于"同一个 key、不同域名、不同价"的纯价差场景(同上游, 不破坏 auto 的选择/熔断)
// 且不限 auto——按 auto 熔断后实际命中的分组取系数,灵活到"域名×分组"粒度。
//
// { "api1.example.com": 1.2, "api2.example.com": 1.5 }
var defaultDomainRatio = map[string]float64{}
// 结构: { 域名(小写,不含端口): { 分组名: 系数, "*": 该域名默认系数 } }
// 例:
//
// {
// "api1.example.com": { "*": 1.2 },
// "api2.example.com": { "*": 1.5, "Claude-3折": 1.8 }
// }
//
// 查找规则: 先按命中分组精确匹配; 未命中则回退该域名的 "*"; 再没有则系数=1(不影响)。
var defaultDomainRatio = map[string]map[string]float64{}
var domainRatioMap = types.NewRWMap[string, float64]()
var domainRatioMap = types.NewRWMap[string, map[string]float64]()
type DomainRatioSetting struct {
DomainRatio *types.RWMap[string, float64] `json:"domain_ratio"`
DomainRatio *types.RWMap[string, map[string]float64] `json:"domain_ratio"`
}
var domainRatioSetting DomainRatioSetting
@@ -31,18 +39,24 @@ func init() {
config.GlobalConfig.Register("domain_ratio_setting", &domainRatioSetting)
}
// GetDomainRatio 按域名返回系数;未配置时返回 1(不影响计费)
// host 会被规整为小写并去端口。
func GetDomainRatio(host string) float64 {
// GetDomainRatioForGroup 按 "域名 + 命中分组" 返回系数
// host 会被规整为小写并去端口;未配置/匹配不到时返回 1(不影响计费)
func GetDomainRatioForGroup(host string, group string) float64 {
h := normalizeDomainHost(host)
if h == "" {
return 1
}
ratio, ok := domainRatioMap.Get(h)
if !ok {
groupMap, ok := domainRatioMap.Get(h)
if !ok || groupMap == nil {
return 1
}
return ratio
if r, ok := groupMap[group]; ok {
return r
}
if r, ok := groupMap["*"]; ok {
return r
}
return 1
}
// HasDomainRatioConfigured 是否配置了任何域名系数(用于跳过无谓计算)。
@@ -56,7 +70,7 @@ func normalizeDomainHost(host string) string {
if h == "" {
return ""
}
// 去端口host:port → host。IPv6 形如 [::1]:8080 → 去掉 ]:8080 之后的端口。
// IPv6 形如 [::1]:8080 → 保留 [::1]
if strings.HasPrefix(h, "[") {
if end := strings.Index(h, "]"); end != -1 {
return h[:end+1]
@@ -77,6 +91,6 @@ func UpdateDomainRatioByJSONString(jsonStr string) error {
return types.LoadFromJsonString(domainRatioMap, jsonStr)
}
func GetDomainRatioCopy() map[string]float64 {
func GetDomainRatioCopy() map[string]map[string]float64 {
return domainRatioMap.ReadAll()
}