Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions constant/context_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const (
ContextKeyTokenCrossGroupRetry ContextKey = "token_cross_group_retry"

/* channel related keys */
ContextKeyChannelRatio ContextKey = "channel_ratio"
ContextKeyChannelId ContextKey = "channel_id"
ContextKeyChannelName ContextKey = "channel_name"
ContextKeyChannelCreateTime ContextKey = "channel_create_time"
Expand Down
1 change: 1 addition & 0 deletions controller/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,7 @@ func RelayTask(c *gin.Context) {
task.PrivateData.BillingContext = &model.TaskBillingContext{
ModelPrice: relayInfo.PriceData.ModelPrice,
GroupRatio: relayInfo.PriceData.GroupRatioInfo.GroupRatio,
ChannelRatio: relayInfo.PriceData.ChannelRatio,
ModelRatio: relayInfo.PriceData.ModelRatio,
OtherRatios: relayInfo.PriceData.OtherRatios,
OriginModelName: relayInfo.OriginModelName,
Expand Down
1 change: 1 addition & 0 deletions middleware/distributor.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,7 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode
case constant.ChannelTypeCoze:
c.Set("bot_id", channel.Other)
}
common.SetContextKey(c, constant.ContextKeyChannelRatio, channel.GetRatio())
return nil
}

Expand Down
6 changes: 4 additions & 2 deletions model/ability.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ type Ability struct {

type AbilityWithChannel struct {
Ability
ChannelType int `json:"channel_type"`
ChannelType int `json:"channel_type"`
ChannelRatio float64 `json:"channel_ratio"`
ChannelWeight uint `json:"channel_weight"`
}

func GetAllEnableAbilityWithChannels() ([]AbilityWithChannel, error) {
var abilities []AbilityWithChannel
err := DB.Table("abilities").
Select("abilities.*, channels.type as channel_type").
Select("abilities.*, channels.type as channel_type, COALESCE(channels.ratio, 1) as channel_ratio, COALESCE(channels.weight, 0) as channel_weight").
Joins("left join channels on abilities.channel_id = channels.id").
Where("abilities.enabled = ?", true).
Scan(&abilities).Error
Expand Down
11 changes: 11 additions & 0 deletions model/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ type Channel struct {

OtherSettings string `json:"settings" gorm:"column:settings"` // 其他设置,存储azure版本等不需要检索的信息,详见dto.ChannelOtherSettings

// Ratio is a per-channel billing multiplier applied on top of model and group ratios.
// nil or 0 is treated as 1.0 (no adjustment). Values < 1 reduce cost, > 1 increase cost.
Ratio *float64 `json:"ratio" gorm:"default:1"`

// cache info
Keys []string `json:"-" gorm:"-"`
}
Expand Down Expand Up @@ -342,6 +346,13 @@ func (channel *Channel) GetAutoBan() bool {
return *channel.AutoBan == 1
}

func (channel *Channel) GetRatio() float64 {
if channel.Ratio == nil || *channel.Ratio == 0 {
return 1.0
}
return *channel.Ratio
}

func (channel *Channel) Save() error {
return DB.Save(channel).Error
}
Expand Down
61 changes: 61 additions & 0 deletions model/pricing.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ type Pricing struct {
BillingMode string `json:"billing_mode,omitempty"`
BillingExpr string `json:"billing_expr,omitempty"`
PricingVersion string `json:"pricing_version,omitempty"`
// GroupChannelRatioMin holds the minimum channel ratio per group across all channels serving that model+group.
// Omitted when all channels use the default ratio of 1.0.
GroupChannelRatioMin map[string]float64 `json:"group_channel_ratio_min,omitempty"`
// GroupChannelRatioMax holds the maximum channel ratio per group.
// Omitted when equal to GroupChannelRatioMin (no range to show).
GroupChannelRatioMax map[string]float64 `json:"group_channel_ratio_max,omitempty"`
}

type PricingVendor struct {
Expand Down Expand Up @@ -190,13 +196,39 @@ func updatePricing() {

modelGroupsMap := make(map[string]*types.Set[string])

// modelGroupChannelRatio[model][group] = {min, max} channel ratio across all channels
type minMaxRatio struct {
min float64
max float64
}
modelGroupChannelRatio := make(map[string]map[string]*minMaxRatio)

for _, ability := range enableAbilities {
groups, ok := modelGroupsMap[ability.Model]
if !ok {
groups = types.NewSet[string]()
modelGroupsMap[ability.Model] = groups
}
groups.Add(ability.Group)

// accumulate min/max channel ratio per (model, group)
if _, ok := modelGroupChannelRatio[ability.Model]; !ok {
modelGroupChannelRatio[ability.Model] = make(map[string]*minMaxRatio)
}
cr := ability.ChannelRatio
if cr <= 0 {
cr = 1.0
}
if mm, exists := modelGroupChannelRatio[ability.Model][ability.Group]; exists {
if cr < mm.min {
mm.min = cr
}
if cr > mm.max {
mm.max = cr
}
} else {
modelGroupChannelRatio[ability.Model][ability.Group] = &minMaxRatio{min: cr, max: cr}
}
}

//这里使用切片而不是Set,因为一个模型可能支持多个端点类型,并且第一个端点是优先使用端点
Expand Down Expand Up @@ -337,6 +369,35 @@ func updatePricing() {
pricing.BillingExpr = expr
}
}

// compute per-group min/max channel ratio
if groupRatios, ok := modelGroupChannelRatio[model]; ok {
minMap := make(map[string]float64)
maxMap := make(map[string]float64)
for group, mm := range groupRatios {
if mm.min != 1.0 || mm.max != 1.0 {
minMap[group] = mm.min
maxMap[group] = mm.max
}
}
if len(minMap) > 0 {
pricing.GroupChannelRatioMin = minMap
}
if len(maxMap) > 0 {
// only store max when it differs from min somewhere
hasRange := false
for g, mx := range maxMap {
if mx != minMap[g] {
hasRange = true
break
}
}
if hasRange {
pricing.GroupChannelRatioMax = maxMap
}
}
}

pricingMap = append(pricingMap, pricing)
}

Expand Down
1 change: 1 addition & 0 deletions model/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ type TaskPrivateData struct {
type TaskBillingContext struct {
ModelPrice float64 `json:"model_price,omitempty"` // 模型单价
GroupRatio float64 `json:"group_ratio,omitempty"` // 分组倍率
ChannelRatio float64 `json:"channel_ratio,omitempty"` // 渠道倍率
ModelRatio float64 `json:"model_ratio,omitempty"` // 模型倍率
OtherRatios map[string]float64 `json:"other_ratios,omitempty"` // 附加倍率(时长、分辨率等)
OriginModelName string `json:"origin_model_name,omitempty"` // 模型名称,必须为OriginModelName
Expand Down
6 changes: 5 additions & 1 deletion pkg/billingexpr/settle.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ func ComputeTieredQuotaWithRequest(snap *BillingSnapshot, params TokenParams, re
}

quotaBeforeGroup := quotaConversion(cost, snap)
afterGroup := QuotaRound(quotaBeforeGroup * snap.GroupRatio)
cr := snap.ChannelRatio
if cr <= 0 {
cr = 1.0
}
afterGroup := QuotaRound(quotaBeforeGroup * snap.GroupRatio * cr)
crossed := trace.MatchedTier != snap.EstimatedTier

return TieredResult{
Expand Down
1 change: 1 addition & 0 deletions pkg/billingexpr/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type BillingSnapshot struct {
ExprString string `json:"expr_string"`
ExprHash string `json:"expr_hash"`
GroupRatio float64 `json:"group_ratio"`
ChannelRatio float64 `json:"channel_ratio"`
EstimatedPromptTokens int `json:"estimated_prompt_tokens"`
EstimatedCompletionTokens int `json:"estimated_completion_tokens"`
EstimatedQuotaBeforeGroup float64 `json:"estimated_quota_before_group"`
Expand Down
7 changes: 7 additions & 0 deletions relay/common/relay_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ type ChannelMeta struct {
UpstreamModelName string
IsModelMapped bool
SupportStreamOptions bool // 是否支持流式选项
ChannelRatio float64
}

type TokenCountMeta struct {
Expand Down Expand Up @@ -232,6 +233,12 @@ func (info *RelayInfo) InitChannelMeta(c *gin.Context) {
channelMeta.SupportStreamOptions = true
}

channelRatio, ok := common.GetContextKeyType[float64](c, constant.ContextKeyChannelRatio)
if !ok || channelRatio <= 0 {
channelRatio = 1.0
}
channelMeta.ChannelRatio = channelRatio

info.ChannelMeta = channelMeta

// reset some fields based on channel meta
Expand Down
31 changes: 24 additions & 7 deletions relay/helper/price.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"strings"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/logger"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/pkg/billingexpr"
Expand Down Expand Up @@ -35,6 +36,14 @@ func modelPriceNotConfiguredError(modelName string, userId int) error {
// https://docs.claude.com/en/docs/build-with-claude/prompt-caching#1-hour-cache-duration
const claudeCacheCreation1hMultiplier = 6 / 3.75

func getChannelRatioFromContext(c *gin.Context) float64 {
v, ok := common.GetContextKeyType[float64](c, constant.ContextKeyChannelRatio)
if !ok || v <= 0 {
return 1.0
}
return v
}

// HandleGroupRatio checks for "auto_group" in the context and updates the group ratio and relayInfo.UsingGroup if present
func HandleGroupRatio(ctx *gin.Context, relayInfo *relaycommon.RelayInfo) types.GroupRatioInfo {
groupRatioInfo := types.GroupRatioInfo{
Expand Down Expand Up @@ -69,9 +78,11 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens

groupRatioInfo := HandleGroupRatio(c, info)

channelRatio := getChannelRatioFromContext(c)

// Check if this model uses tiered_expr billing
if billing_setting.GetBillingMode(info.OriginModelName) == billing_setting.BillingModeTieredExpr {
return modelPriceHelperTiered(c, info, promptTokens, meta, groupRatioInfo)
return modelPriceHelperTiered(c, info, promptTokens, meta, groupRatioInfo, channelRatio)
}

var preConsumedQuota int
Expand Down Expand Up @@ -111,13 +122,13 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
imageRatio, _ = ratio_setting.GetImageRatio(info.OriginModelName)
audioRatio = ratio_setting.GetAudioRatio(info.OriginModelName)
audioCompletionRatio = ratio_setting.GetAudioCompletionRatio(info.OriginModelName)
ratio := modelRatio * groupRatioInfo.GroupRatio
ratio := modelRatio * groupRatioInfo.GroupRatio * channelRatio
preConsumedQuota = int(float64(preConsumedTokens) * ratio)
} else {
if meta.ImagePriceRatio != 0 {
modelPrice = modelPrice * meta.ImagePriceRatio
}
preConsumedQuota = int(modelPrice * common.QuotaPerUnit * groupRatioInfo.GroupRatio)
preConsumedQuota = int(modelPrice * common.QuotaPerUnit * groupRatioInfo.GroupRatio * channelRatio)
}

// check if free model pre-consume is disabled
Expand Down Expand Up @@ -145,6 +156,7 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
ModelRatio: modelRatio,
CompletionRatio: completionRatio,
GroupRatioInfo: groupRatioInfo,
ChannelRatio: channelRatio,
UsePrice: usePrice,
CacheRatio: cacheRatio,
ImageRatio: imageRatio,
Expand All @@ -167,6 +179,8 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
func ModelPriceHelperPerCall(c *gin.Context, info *relaycommon.RelayInfo) (types.PriceData, error) {
groupRatioInfo := HandleGroupRatio(c, info)

channelRatio := getChannelRatioFromContext(c)

modelPrice, success := ratio_setting.GetModelPrice(info.OriginModelName, true)
usePrice := success
var modelRatio float64
Expand Down Expand Up @@ -194,7 +208,7 @@ func ModelPriceHelperPerCall(c *gin.Context, info *relaycommon.RelayInfo) (types
freeModel := false

if usePrice {
quota = int(modelPrice * common.QuotaPerUnit * groupRatioInfo.GroupRatio)
quota = int(modelPrice * common.QuotaPerUnit * groupRatioInfo.GroupRatio * channelRatio)
if !operation_setting.GetQuotaSetting().EnableFreeModelPreConsume {
if groupRatioInfo.GroupRatio == 0 || modelPrice == 0 {
quota = 0
Expand All @@ -203,7 +217,7 @@ func ModelPriceHelperPerCall(c *gin.Context, info *relaycommon.RelayInfo) (types
}
} else {
// 按量计费:以模型倍率的一半作为预扣额度
quota = int(modelRatio / 2 * common.QuotaPerUnit * groupRatioInfo.GroupRatio)
quota = int(modelRatio / 2 * common.QuotaPerUnit * groupRatioInfo.GroupRatio * channelRatio)
modelPrice = -1
if !operation_setting.GetQuotaSetting().EnableFreeModelPreConsume {
if groupRatioInfo.GroupRatio == 0 || modelRatio == 0 {
Expand All @@ -219,6 +233,7 @@ func ModelPriceHelperPerCall(c *gin.Context, info *relaycommon.RelayInfo) (types
ModelRatio: modelRatio,
UsePrice: usePrice,
Quota: quota,
ChannelRatio: channelRatio,
GroupRatioInfo: groupRatioInfo,
}
return priceData, nil
Expand All @@ -238,7 +253,7 @@ func HasModelBillingConfig(modelName string) bool {
return ok && strings.TrimSpace(expr) != ""
}

func modelPriceHelperTiered(c *gin.Context, info *relaycommon.RelayInfo, promptTokens int, meta *types.TokenCountMeta, groupRatioInfo types.GroupRatioInfo) (types.PriceData, error) {
func modelPriceHelperTiered(c *gin.Context, info *relaycommon.RelayInfo, promptTokens int, meta *types.TokenCountMeta, groupRatioInfo types.GroupRatioInfo, channelRatio float64) (types.PriceData, error) {
exprStr, ok := billing_setting.GetBillingExpr(info.OriginModelName)
if !ok {
return types.PriceData{}, fmt.Errorf("model %s is configured as tiered_expr but has no billing expression", info.OriginModelName)
Expand All @@ -265,7 +280,7 @@ func modelPriceHelperTiered(c *gin.Context, info *relaycommon.RelayInfo, promptT

// Expression coefficients are $/1M tokens prices; convert to quota the same way per-call billing does.
quotaBeforeGroup := rawCost / 1_000_000 * common.QuotaPerUnit
preConsumedQuota := billingexpr.QuotaRound(quotaBeforeGroup * groupRatioInfo.GroupRatio)
preConsumedQuota := billingexpr.QuotaRound(quotaBeforeGroup * groupRatioInfo.GroupRatio * channelRatio)

freeModel := false
if !operation_setting.GetQuotaSetting().EnableFreeModelPreConsume {
Expand All @@ -282,6 +297,7 @@ func modelPriceHelperTiered(c *gin.Context, info *relaycommon.RelayInfo, promptT
ExprString: exprStr,
ExprHash: exprHash,
GroupRatio: groupRatioInfo.GroupRatio,
ChannelRatio: channelRatio,
EstimatedPromptTokens: promptTokens,
EstimatedCompletionTokens: estimatedCompletionTokens,
EstimatedQuotaBeforeGroup: quotaBeforeGroup,
Expand All @@ -296,6 +312,7 @@ func modelPriceHelperTiered(c *gin.Context, info *relaycommon.RelayInfo, promptT
priceData := types.PriceData{
FreeModel: freeModel,
GroupRatioInfo: groupRatioInfo,
ChannelRatio: channelRatio,
QuotaToPreConsume: preConsumedQuota,
}

Expand Down
6 changes: 6 additions & 0 deletions service/log_info_generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, m
other["cache_ratio"] = cacheRatio
other["model_price"] = modelPrice
other["user_group_ratio"] = userGroupRatio
if channelRatio := relayInfo.PriceData.ChannelRatio; channelRatio != 0 && channelRatio != 1.0 {
other["channel_ratio"] = channelRatio
}
other["frt"] = float64(relayInfo.FirstResponseTime.UnixMilli() - relayInfo.StartTime.UnixMilli())
if relayInfo.ReasoningEffort != "" {
other["reasoning_effort"] = relayInfo.ReasoningEffort
Expand Down Expand Up @@ -261,6 +264,9 @@ func GenerateMjOtherInfo(relayInfo *relaycommon.RelayInfo, priceData types.Price
if priceData.GroupRatioInfo.HasSpecialRatio {
other["user_group_ratio"] = priceData.GroupRatioInfo.GroupSpecialRatio
}
if priceData.ChannelRatio != 0 && priceData.ChannelRatio != 1.0 {
other["channel_ratio"] = priceData.ChannelRatio
}
appendRequestPath(nil, relayInfo, other)
return other
}
Expand Down
Loading