tokenFactory/service/task_profit_share.go

280 lines
8.9 KiB
Go

/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
package service
import (
"context"
"math"
"strings"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/model"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/setting/ratio_setting"
"github.com/QuantumNous/new-api/types"
)
// profitShareExtraTotalTokensKey 由 RecalculateTaskQuotaByTokens 传入,用于异步 token 补扣的加价比例推算;不入库日志。
const profitShareExtraTotalTokensKey = "_profit_share_total_tokens"
// TryPostWalletProfitShareForTaskBilledQuota 任务完成侧已知「最终对用户计费额度」后入账利润分成(含预扣与实际一致、操练场等无补扣 delta 的场景)。
// 提交阶段预扣结算不调用本函数,由轮询/fetch 完成路径统一触发。
func TryPostWalletProfitShareForTaskBilledQuota(ctx context.Context, task *model.Task, billedQuota int, hintTotalTokens int) {
_ = ctx
if task == nil || billedQuota <= 0 {
return
}
if !common.IsDistributorProfitShareMode() {
return
}
bs := strings.TrimSpace(task.PrivateData.BillingSource)
if bs != "" && bs != BillingSourceWallet {
return
}
ratio, ok := taskProfitShareMarkupSliceRatio(task, hintTotalTokens)
if !ok || ratio <= 0 {
return
}
slice := int(math.Round(float64(billedQuota) * ratio))
if slice <= 0 {
return
}
invitee, err := model.GetUserById(task.UserId, false)
if err != nil || invitee == nil || invitee.InviterId <= 0 {
return
}
inviter, err2 := model.GetUserById(invitee.InviterId, false)
if err2 != nil || inviter == nil || !model.UserIsDistributor(inviter) {
return
}
modelName := strings.TrimSpace(taskModelName(task))
bps := model.EffectiveAffiliateCommissionBps(inviter, task.UserId)
if bps <= 0 {
return
}
maxBps := 10000
if bps > maxBps {
bps = maxBps
}
reward := int(int64(slice) * int64(bps) / int64(maxBps))
if reward <= 0 {
return
}
if err := model.CreditDistributorProfitShare(invitee.InviterId, task.UserId, task.ChannelId, modelName, billedQuota, slice, reward, bps); err != nil {
common.SysError("TryPostWalletProfitShareForTaskBilledQuota: " + err.Error())
}
}
func taskProfitShareMarkupSliceRatio(task *model.Task, hintTotalTokens int) (float64, bool) {
if task == nil {
return 0, false
}
ch, err := model.CacheGetChannel(task.ChannelId)
if err != nil || ch == nil {
return 0, false
}
if constant.IsVideoTaskChannel(ch.Type) {
if r, ok := taskProfitShareMarkupRatioVideoComplete(task); ok {
return r, true
}
if r, ok := taskProfitShareMarkupRatioVideoSubmitInput(task); ok {
return r, true
}
}
if hintTotalTokens > 0 {
if r, ok := taskProfitShareMarkupRatioFromUpstreamTokens(task, hintTotalTokens); ok {
return r, true
}
}
if r, ok := taskProfitShareMarkupRatioPerCallModelPrice(task); ok {
return r, true
}
return 0, false
}
func taskProfitShareMarkupRatioVideoComplete(task *model.Task) (float64, bool) {
meta, ok := extractVideoMetadataFromTaskData(task)
if !ok {
return 0, false
}
modelName := strings.TrimSpace(taskModelName(task))
if modelName == "" {
return 0, false
}
mode := detectTaskVideoBillingMode(task)
channelPerSec := channelVideoPerSecondUSD(task.ChannelId, modelName, mode, meta.Width, meta.Height, meta.HasAudio)
if channelPerSec <= 0 {
return 0, false
}
seconds := int(math.Ceil(meta.DurationSec))
if seconds <= 0 {
return 0, false
}
groupRatio := 1.0
if task.PrivateData.BillingContext != nil && task.PrivateData.BillingContext.GroupRatio > 0 {
groupRatio = task.PrivateData.BillingContext.GroupRatio
}
costDisc := channelCostDiscountPercentFromTask(task)
markup := model.ResolveEffectiveMarkupDiscountPercentForInviteeBilling(task.UserId, task.ChannelId, modelName)
globalPerSec := globalVideoPerSecondUSD(modelName, mode, meta.Width, meta.Height, meta.HasAudio)
effW := effectiveVideoPerSecondUSD(channelPerSec, globalPerSec, costDisc, markup)
eff0 := effectiveVideoPerSecondUSD(channelPerSec, globalPerSec, costDisc, 0)
qW := int(math.Round(float64(seconds) * effW * common.QuotaPerUnit * groupRatio))
q0 := int(math.Round(float64(seconds) * eff0 * common.QuotaPerUnit * groupRatio))
if qW <= 0 || qW <= q0 {
return 0, false
}
return float64(qW-q0) / float64(qW), true
}
func taskProfitShareMarkupRatioVideoSubmitInput(task *model.Task) (float64, bool) {
if task == nil || strings.TrimSpace(task.Properties.Input) == "" {
return 0, false
}
var req relaycommon.TaskSubmitReq
if err := common.UnmarshalJsonStr(task.Properties.Input, &req); err != nil {
return 0, false
}
ri, ok := relayInfoSnapshotForProfitShare(task)
if !ok {
return 0, false
}
qW := calcVideoPerSecondQuotaFromTaskReq(ri, &req, ri.PriceData.MarkupDiscountPercent)
q0 := calcVideoPerSecondQuotaFromTaskReq(ri, &req, 0)
if qW <= 0 || qW <= q0 {
return 0, false
}
return float64(qW-q0) / float64(qW), true
}
func taskProfitShareMarkupRatioFromUpstreamTokens(task *model.Task, totalTokens int) (float64, bool) {
if task == nil || totalTokens <= 0 {
return 0, false
}
ri, ok := relayInfoSnapshotForProfitShare(task)
if !ok {
return 0, false
}
globalMr, globalOK, _ := ratio_setting.GetModelRatio(ri.OriginModelName)
if !globalOK {
globalMr = ri.PriceData.GlobalModelRatio
}
if globalMr <= 0 {
globalMr = ri.PriceData.GlobalModelRatio
}
pd := ri.PriceData
pd.GlobalModelRatio = globalMr
ri.PriceData = pd
qW := calcQuotaByUpstreamTokensWithMarkup(ri, totalTokens, pd.MarkupDiscountPercent)
q0 := calcQuotaByUpstreamTokensWithMarkup(ri, totalTokens, 0)
if qW <= 0 || qW <= q0 {
return 0, false
}
return float64(qW-q0) / float64(qW), true
}
func taskProfitShareMarkupRatioPerCallModelPrice(task *model.Task) (float64, bool) {
bc := task.PrivateData.BillingContext
if bc == nil || bc.ModelPrice <= 0 {
return 0, false
}
modelName := strings.TrimSpace(taskModelName(task))
if modelName == "" {
return 0, false
}
costDisc := channelCostDiscountPercentFromTask(task)
markup := model.ResolveEffectiveMarkupDiscountPercentForInviteeBilling(task.UserId, task.ChannelId, modelName)
globalPrice, _ := ratio_setting.GetModelPrice(modelName, false)
effW := model.EffectiveModelPrice(bc.ModelPrice, globalPrice, costDisc, markup)
eff0 := model.EffectiveModelPrice(bc.ModelPrice, globalPrice, costDisc, 0)
if effW <= 0 || effW <= eff0 {
return 0, false
}
return (effW - eff0) / effW, true
}
func channelCostDiscountPercentFromTask(task *model.Task) float64 {
if task == nil {
return 100
}
if bc := task.PrivateData.BillingContext; bc != nil && bc.ChannelPriceDiscountPercent > 0 {
return bc.ChannelPriceDiscountPercent
}
return model.ResolveChannelPriceDiscountPercent(task.ChannelId)
}
func relayInfoSnapshotForProfitShare(task *model.Task) (*relaycommon.RelayInfo, bool) {
if task == nil {
return nil, false
}
ch, err := model.CacheGetChannel(task.ChannelId)
if err != nil || ch == nil {
return nil, false
}
bc := task.PrivateData.BillingContext
if bc == nil {
return nil, false
}
modelName := strings.TrimSpace(taskModelName(task))
if modelName == "" {
return nil, false
}
markup := model.ResolveEffectiveMarkupDiscountPercentForInviteeBilling(task.UserId, task.ChannelId, modelName)
cost := bc.ChannelPriceDiscountPercent
if cost <= 0 {
cost = model.ResolveChannelPriceDiscountPercent(task.ChannelId)
}
gr := bc.GroupRatio
if gr <= 0 {
gr = 1
}
globalMr, globalOK, _ := ratio_setting.GetModelRatio(modelName)
if !globalOK {
globalMr = 0
}
ri := &relaycommon.RelayInfo{
UserId: task.UserId,
OriginModelName: modelName,
BillingSource: task.PrivateData.BillingSource,
ChannelMeta: &relaycommon.ChannelMeta{ChannelType: ch.Type, ChannelId: task.ChannelId},
}
other := bc.OtherRatios
if other != nil {
cp := make(map[string]float64, len(other))
for k, v := range other {
cp[k] = v
}
other = cp
}
ri.PriceData = types.PriceData{
ModelPrice: bc.ModelPrice,
ModelRatio: bc.ModelRatio,
GlobalModelRatio: globalMr,
GroupRatioInfo: types.GroupRatioInfo{GroupRatio: gr},
UsePrice: true,
CostDiscountPercent: cost,
MarkupDiscountPercent: markup,
OtherRatios: other,
VideoOutputTokens: 0,
}
return ri, true
}