feat(task): add adaptor billing interface and async settlement framework

Add three billing lifecycle methods to the TaskAdaptor interface:
- EstimateBilling: compute OtherRatios from user request before pricing
- AdjustBillingOnSubmit: adjust ratios from upstream submit response
- AdjustBillingOnComplete: determine final quota at task terminal state

Introduce BaseBilling as embeddable no-op default for adaptors without
custom billing. Move Sora/Ali OtherRatios logic from shared validation
into per-adaptor EstimateBilling implementations.

Add TaskBillingContext to persist pricing params (model_price, group_ratio,
other_ratios) in task private data for async polling settlement.

Extract RecalculateTaskQuota as a general-purpose delta settlement
function and unify polling billing via settleTaskBillingOnComplete
(adaptor-first, then token-based fallback).
This commit is contained in:
CaIon
2026-02-10 21:15:09 +08:00
parent 9e3954428d
commit d6e11fd2e1
19 changed files with 321 additions and 116 deletions

View File

@@ -26,6 +26,9 @@ type TaskPollingAdaptor interface {
Init(info *relaycommon.RelayInfo)
FetchTask(baseURL string, key string, body map[string]any, proxy string) (*http.Response, error)
ParseTaskResult(body []byte) (*relaycommon.TaskInfo, error)
// AdjustBillingOnComplete 在任务到达终态(成功/失败)时由轮询循环调用。
// 返回正数触发差额结算(补扣/退还),返回 0 保持预扣费金额不变。
AdjustBillingOnComplete(task *model.Task, taskResult *relaycommon.TaskInfo) int
}
// GetTaskAdaptorFunc 由 main 包注入,用于获取指定平台的任务适配器。
@@ -372,10 +375,8 @@ func updateVideoSingleTask(ctx context.Context, adaptor TaskPollingAdaptor, ch *
task.PrivateData.ResultURL = taskcommon.BuildProxyURL(task.TaskID)
}
// 如果返回了 total_tokens根据模型倍率重新计费
if taskResult.TotalTokens > 0 {
RecalculateTaskQuotaByTokens(ctx, task, taskResult.TotalTokens)
}
// 完成时计费调整:优先由 adaptor 计算,回退到 token 重算
settleTaskBillingOnComplete(ctx, adaptor, task, taskResult)
case model.TaskStatusFailure:
logger.LogJson(ctx, fmt.Sprintf("Task %s failed", taskId), task)
task.Status = model.TaskStatusFailure
@@ -444,3 +445,22 @@ func truncateBase64(s string) string {
}
return s[:maxKeep] + "..."
}
// settleTaskBillingOnComplete 任务完成时的统一计费调整。
// 优先级1. adaptor.AdjustBillingOnComplete 返回正数 → 使用 adaptor 计算的额度
//
// 2. taskResult.TotalTokens > 0 → 按 token 重算
// 3. 都不满足 → 保持预扣额度不变
func settleTaskBillingOnComplete(ctx context.Context, adaptor TaskPollingAdaptor, task *model.Task, taskResult *relaycommon.TaskInfo) {
// 1. 优先让 adaptor 决定最终额度
if actualQuota := adaptor.AdjustBillingOnComplete(task, taskResult); actualQuota > 0 {
RecalculateTaskQuota(ctx, task, actualQuota, "adaptor计费调整")
return
}
// 2. 回退到 token 重算
if taskResult.TotalTokens > 0 {
RecalculateTaskQuotaByTokens(ctx, task, taskResult.TotalTokens)
return
}
// 3. 无调整,保持预扣额度
}