feat(billing): enhance task billing process with video input detection and updated pricing logic
- Added `EstimateBilling` function to check for video input in request metadata and return corresponding discount ratios. - Updated `ModelPriceHelperPerCall` to incorporate new pricing logic based on model ratios and video input. - Enhanced task billing logs to include model ratio information and adjusted calculations for actual quota based on additional multipliers. - Introduced `renderTaskBillingProcess` to improve rendering of task billing information in the UI.
This commit is contained in:
+1
-1
@@ -581,7 +581,7 @@ func RelayTask(c *gin.Context) {
|
||||
ModelRatio: relayInfo.PriceData.ModelRatio,
|
||||
OtherRatios: relayInfo.PriceData.OtherRatios,
|
||||
OriginModelName: relayInfo.OriginModelName,
|
||||
PerCallBilling: common.StringsContains(constant.TaskPricePatches, relayInfo.OriginModelName),
|
||||
PerCallBilling: common.StringsContains(constant.TaskPricePatches, relayInfo.OriginModelName) || relayInfo.PriceData.UsePrice,
|
||||
}
|
||||
task.Quota = result.Quota
|
||||
task.Data = result.TaskData
|
||||
|
||||
@@ -132,6 +132,49 @@ func (a *TaskAdaptor) BuildRequestHeader(_ *gin.Context, req *http.Request, _ *r
|
||||
return nil
|
||||
}
|
||||
|
||||
// EstimateBilling 检测请求 metadata 中是否包含视频输入,返回视频折扣 OtherRatio。
|
||||
func (a *TaskAdaptor) EstimateBilling(c *gin.Context, info *relaycommon.RelayInfo) map[string]float64 {
|
||||
req, err := relaycommon.GetTaskRequest(c)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if hasVideoInMetadata(req.Metadata) {
|
||||
if ratio, ok := GetVideoInputRatio(info.OriginModelName); ok {
|
||||
return map[string]float64{"video_input": ratio}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasVideoInMetadata 直接检查 metadata 的 content 数组是否包含 video_url 条目,
|
||||
// 避免构建完整的上游 requestPayload。
|
||||
func hasVideoInMetadata(metadata map[string]interface{}) bool {
|
||||
if metadata == nil {
|
||||
return false
|
||||
}
|
||||
contentRaw, ok := metadata["content"]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
contentSlice, ok := contentRaw.([]interface{})
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for _, item := range contentSlice {
|
||||
itemMap, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if itemMap["type"] == "video_url" {
|
||||
return true
|
||||
}
|
||||
if _, has := itemMap["video_url"]; has {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// BuildRequestBody converts request into Doubao specific format.
|
||||
func (a *TaskAdaptor) BuildRequestBody(c *gin.Context, info *relaycommon.RelayInfo) (io.Reader, error) {
|
||||
req, err := relaycommon.GetTaskRequest(c)
|
||||
|
||||
@@ -10,3 +10,16 @@ var ModelList = []string{
|
||||
}
|
||||
|
||||
var ChannelName = "doubao-video"
|
||||
|
||||
// videoInputRatioMap 视频输入折扣比率(含视频单价 / 不含视频单价)。
|
||||
// 管理员应将 ModelRatio 设置为"不含视频"的较高费率,
|
||||
// 系统在检测到视频输入时自动乘以此折扣。
|
||||
var videoInputRatioMap = map[string]float64{
|
||||
"doubao-seedance-2-0-260128": 28.0 / 46.0, // ~0.6087
|
||||
"doubao-seedance-2-0-fast-260128": 22.0 / 37.0, // ~0.5946
|
||||
}
|
||||
|
||||
func GetVideoInputRatio(modelName string) (float64, bool) {
|
||||
r, ok := videoInputRatioMap[modelName]
|
||||
return r, ok
|
||||
}
|
||||
|
||||
+29
-15
@@ -139,21 +139,23 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
|
||||
return priceData, nil
|
||||
}
|
||||
|
||||
// ModelPriceHelperPerCall 按次计费的 PriceHelper (MJ、Task)
|
||||
// ModelPriceHelperPerCall 按次/按量计费的 PriceHelper (MJ、Task)
|
||||
func ModelPriceHelperPerCall(c *gin.Context, info *relaycommon.RelayInfo) (types.PriceData, error) {
|
||||
groupRatioInfo := HandleGroupRatio(c, info)
|
||||
|
||||
modelPrice, success := ratio_setting.GetModelPrice(info.OriginModelName, true)
|
||||
// 如果没有配置价格,检查模型倍率配置
|
||||
if !success {
|
||||
usePrice := success
|
||||
var modelRatio float64
|
||||
|
||||
// 没有配置费用,也要使用默认费用,否则按费率计费模型无法使用
|
||||
if !success {
|
||||
defaultPrice, ok := ratio_setting.GetDefaultModelPriceMap()[info.OriginModelName]
|
||||
if ok {
|
||||
modelPrice = defaultPrice
|
||||
usePrice = true
|
||||
} else {
|
||||
// 没有配置倍率也不接受没配置,那就返回错误
|
||||
_, ratioSuccess, matchName := ratio_setting.GetModelRatio(info.OriginModelName)
|
||||
var ratioSuccess bool
|
||||
var matchName string
|
||||
modelRatio, ratioSuccess, matchName = ratio_setting.GetModelRatio(info.OriginModelName)
|
||||
acceptUnsetRatio := false
|
||||
if info.UserSetting.AcceptUnsetRatioModel {
|
||||
acceptUnsetRatio = true
|
||||
@@ -161,25 +163,37 @@ func ModelPriceHelperPerCall(c *gin.Context, info *relaycommon.RelayInfo) (types
|
||||
if !ratioSuccess && !acceptUnsetRatio {
|
||||
return types.PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置,请联系管理员设置或开始自用模式;Model %s ratio or price not set, please set or start self-use mode", matchName, matchName)
|
||||
}
|
||||
// 未配置价格但配置了倍率,使用默认预扣价格
|
||||
modelPrice = float64(common.PreConsumedQuota) / common.QuotaPerUnit
|
||||
}
|
||||
|
||||
}
|
||||
quota := int(modelPrice * common.QuotaPerUnit * groupRatioInfo.GroupRatio)
|
||||
|
||||
// 免费模型检测(与 ModelPriceHelper 对齐)
|
||||
var quota int
|
||||
freeModel := false
|
||||
if !operation_setting.GetQuotaSetting().EnableFreeModelPreConsume {
|
||||
if groupRatioInfo.GroupRatio == 0 || modelPrice == 0 {
|
||||
quota = 0
|
||||
freeModel = true
|
||||
|
||||
if usePrice {
|
||||
quota = int(modelPrice * common.QuotaPerUnit * groupRatioInfo.GroupRatio)
|
||||
if !operation_setting.GetQuotaSetting().EnableFreeModelPreConsume {
|
||||
if groupRatioInfo.GroupRatio == 0 || modelPrice == 0 {
|
||||
quota = 0
|
||||
freeModel = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 按量计费:以模型倍率的一半作为预扣额度
|
||||
quota = int(modelRatio / 2 * common.QuotaPerUnit * groupRatioInfo.GroupRatio)
|
||||
modelPrice = -1
|
||||
if !operation_setting.GetQuotaSetting().EnableFreeModelPreConsume {
|
||||
if groupRatioInfo.GroupRatio == 0 || modelRatio == 0 {
|
||||
quota = 0
|
||||
freeModel = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
priceData := types.PriceData{
|
||||
FreeModel: freeModel,
|
||||
ModelPrice: modelPrice,
|
||||
ModelRatio: modelRatio,
|
||||
UsePrice: usePrice,
|
||||
Quota: quota,
|
||||
GroupRatioInfo: groupRatioInfo,
|
||||
}
|
||||
|
||||
+20
-4
@@ -36,8 +36,12 @@ func LogTaskConsumption(c *gin.Context, info *relaycommon.RelayInfo) {
|
||||
}
|
||||
}
|
||||
other := make(map[string]interface{})
|
||||
other["is_task"] = true
|
||||
other["request_path"] = c.Request.URL.Path
|
||||
other["model_price"] = info.PriceData.ModelPrice
|
||||
if info.PriceData.ModelRatio > 0 {
|
||||
other["model_ratio"] = info.PriceData.ModelRatio
|
||||
}
|
||||
other["group_ratio"] = info.PriceData.GroupRatioInfo.GroupRatio
|
||||
if info.PriceData.GroupRatioInfo.HasSpecialRatio {
|
||||
other["user_group_ratio"] = info.PriceData.GroupRatioInfo.GroupSpecialRatio
|
||||
@@ -117,6 +121,9 @@ func taskBillingOther(task *model.Task) map[string]interface{} {
|
||||
other := make(map[string]interface{})
|
||||
if bc := task.PrivateData.BillingContext; bc != nil {
|
||||
other["model_price"] = bc.ModelPrice
|
||||
if bc.ModelRatio > 0 {
|
||||
other["model_ratio"] = bc.ModelRatio
|
||||
}
|
||||
other["group_ratio"] = bc.GroupRatio
|
||||
if len(bc.OtherRatios) > 0 {
|
||||
for k, v := range bc.OtherRatios {
|
||||
@@ -222,7 +229,6 @@ func RecalculateTaskQuota(ctx context.Context, task *model.Task, actualQuota int
|
||||
}
|
||||
other := taskBillingOther(task)
|
||||
other["task_id"] = task.TaskID
|
||||
//other["reason"] = reason
|
||||
other["pre_consumed_quota"] = preConsumedQuota
|
||||
other["actual_quota"] = actualQuota
|
||||
model.RecordTaskBillingLog(model.RecordTaskBillingLogParams{
|
||||
@@ -277,9 +283,19 @@ func RecalculateTaskQuotaByTokens(ctx context.Context, task *model.Task, totalTo
|
||||
finalGroupRatio = groupRatio
|
||||
}
|
||||
|
||||
// 计算实际应扣费额度: totalTokens * modelRatio * groupRatio
|
||||
actualQuota := int(float64(totalTokens) * modelRatio * finalGroupRatio)
|
||||
// 计算 OtherRatios 乘积(视频折扣、时长等)
|
||||
otherMultiplier := 1.0
|
||||
if bc := task.PrivateData.BillingContext; bc != nil {
|
||||
for _, r := range bc.OtherRatios {
|
||||
if r != 1.0 && r > 0 {
|
||||
otherMultiplier *= r
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reason := fmt.Sprintf("token重算:tokens=%d, modelRatio=%.2f, groupRatio=%.2f", totalTokens, modelRatio, finalGroupRatio)
|
||||
// 计算实际应扣费额度: totalTokens * modelRatio * groupRatio * otherMultiplier
|
||||
actualQuota := int(float64(totalTokens) * modelRatio * finalGroupRatio * otherMultiplier)
|
||||
|
||||
reason := fmt.Sprintf("token重算:tokens=%d, modelRatio=%.2f, groupRatio=%.2f, otherMultiplier=%.4f", totalTokens, modelRatio, finalGroupRatio, otherMultiplier)
|
||||
RecalculateTaskQuota(ctx, task, actualQuota, reason)
|
||||
}
|
||||
|
||||
Vendored
+12
@@ -1620,6 +1620,18 @@ function renderPriceSimpleCore({
|
||||
return result;
|
||||
}
|
||||
|
||||
export function renderTaskBillingProcess(other, content) {
|
||||
if (other?.task_id != null) {
|
||||
return renderBillingArticle(
|
||||
[content].filter(Boolean),
|
||||
{ showReferenceNote: false },
|
||||
);
|
||||
}
|
||||
return renderBillingArticle([
|
||||
buildBillingText('任务预扣费(将在任务完成后按实际token重算)'),
|
||||
]);
|
||||
}
|
||||
|
||||
export function renderModelPrice(
|
||||
inputTokens,
|
||||
completionTokens,
|
||||
|
||||
+5
-1
@@ -36,6 +36,7 @@ import {
|
||||
renderAudioModelPrice,
|
||||
renderClaudeModelPrice,
|
||||
renderModelPrice,
|
||||
renderTaskBillingProcess,
|
||||
} from '../../helpers';
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import { useTableCompactMode } from '../common/useTableCompactMode';
|
||||
@@ -497,7 +498,10 @@ export const useLogsData = () => {
|
||||
|
||||
let content = '';
|
||||
if (!isViolationFeeLog) {
|
||||
if (other?.ws || other?.audio) {
|
||||
const isTaskLog = other?.is_task === true || other?.task_id != null;
|
||||
if (isTaskLog && other?.model_price === -1) {
|
||||
content = renderTaskBillingProcess(other, logs[i].content);
|
||||
} else if (other?.ws || other?.audio) {
|
||||
content = renderAudioModelPrice(
|
||||
other?.text_input,
|
||||
other?.text_output,
|
||||
|
||||
Reference in New Issue
Block a user