feat(api): 添加多媒体内容支持并优化钱包计费系统
- 在 API 接口定义中为 video_url 和 audio_url 类型添加 mime_type 字段 - 实现 Google Gemini 客户端对视频和音频内容的支持,包括媒体类型检测和数据传输 - 添加 Gemini 客户端测试用例验证多媒体内容转换功能 - 重构 Playground 页面的媒体上传逻辑以支持 MIME 类型传递 - 实现钱包计费预留机制,确保任务执行前余额充足 - 添加钱包冻结余额管理,防止并发操作导致的超扣问题 - 实现计费预留释放逻辑,处理任务失败或取消情况下的资金返还 - 优化数据库事务处理,确保计费操作的原子性和一致性 - 添加数据库集成测试验证迁移脚本执行流程 - 统一 Google Gemini 相关模型提供商标识符映射
This commit is contained in:
@@ -160,6 +160,13 @@ func (s *Service) execute(ctx context.Context, task store.GatewayTask, user *aut
|
||||
normalizedModelType := modelType
|
||||
attemptNo := task.AttemptCount
|
||||
var firstPreprocessing parameterPreprocessingLog
|
||||
var walletReservations []store.WalletBillingReservation
|
||||
walletReservationFinalized := false
|
||||
defer func() {
|
||||
if !walletReservationFinalized && len(walletReservations) > 0 {
|
||||
_ = s.store.ReleaseTaskBillingReservations(context.WithoutCancel(ctx), walletReservations, "task_not_settled")
|
||||
}
|
||||
}()
|
||||
if len(candidates) > 0 {
|
||||
preprocessing := s.preprocessRequestWithScripts(ctx, task.Kind, body, candidates[0])
|
||||
firstCandidateBody = preprocessing.Body
|
||||
@@ -191,15 +198,17 @@ func (s *Service) execute(ctx context.Context, task store.GatewayTask, user *aut
|
||||
return Result{}, err
|
||||
}
|
||||
estimatedBillings := s.estimatedBillings(ctx, user, task.Kind, firstCandidateBody, candidates[0])
|
||||
if err := s.ensureWalletBalance(ctx, user, estimatedBillings); err != nil {
|
||||
if errors.Is(err, store.ErrInsufficientWalletBalance) {
|
||||
var reserveErr error
|
||||
walletReservations, reserveErr = s.store.ReserveTaskBilling(ctx, task, user, estimatedBillings)
|
||||
if reserveErr != nil {
|
||||
if errors.Is(reserveErr, store.ErrInsufficientWalletBalance) {
|
||||
attemptNo = s.recordFailedAttempt(ctx, failedAttemptRecord{
|
||||
Task: task,
|
||||
Body: firstCandidateBody,
|
||||
Candidate: &candidates[0],
|
||||
AttemptNo: attemptNo + 1,
|
||||
Code: "insufficient_balance",
|
||||
Cause: err,
|
||||
Cause: reserveErr,
|
||||
Simulated: task.RunMode == "simulation",
|
||||
Scope: "wallet_balance",
|
||||
Reason: "wallet_balance_check_failed",
|
||||
@@ -207,13 +216,13 @@ func (s *Service) execute(ctx context.Context, task store.GatewayTask, user *aut
|
||||
Preprocessing: &firstPreprocessing,
|
||||
ModelType: normalizedModelType,
|
||||
})
|
||||
failed, finishErr := s.failTask(ctx, task.ID, "insufficient_balance", err.Error(), task.RunMode == "simulation", err, parameterPreprocessingMetrics(firstPreprocessing))
|
||||
failed, finishErr := s.failTask(ctx, task.ID, "insufficient_balance", reserveErr.Error(), task.RunMode == "simulation", reserveErr, parameterPreprocessingMetrics(firstPreprocessing))
|
||||
if finishErr != nil {
|
||||
return Result{}, finishErr
|
||||
}
|
||||
return Result{Task: failed, Output: failed.Result}, err
|
||||
return Result{Task: failed, Output: failed.Result}, reserveErr
|
||||
}
|
||||
return Result{}, err
|
||||
return Result{}, reserveErr
|
||||
}
|
||||
}
|
||||
if err := s.emit(ctx, task.ID, "task.progress", "running", "normalizing", 0.15, "request normalized", map[string]any{"modelType": normalizedModelType}, task.RunMode == "simulation"); err != nil {
|
||||
@@ -286,9 +295,18 @@ candidatesLoop:
|
||||
if finishErr != nil {
|
||||
return Result{}, finishErr
|
||||
}
|
||||
if settleErr := s.store.SettleTaskBilling(ctx, finished); settleErr != nil {
|
||||
return Result{}, settleErr
|
||||
if finished.FinalChargeAmount > 0 {
|
||||
walletReservationFinalized = true
|
||||
if settleErr := s.store.SettleTaskBilling(ctx, finished); settleErr != nil {
|
||||
return Result{}, settleErr
|
||||
}
|
||||
} else if len(walletReservations) > 0 {
|
||||
if releaseErr := s.store.ReleaseTaskBillingReservations(ctx, walletReservations, "task_billing_zero"); releaseErr != nil {
|
||||
return Result{}, releaseErr
|
||||
}
|
||||
walletReservationFinalized = true
|
||||
}
|
||||
walletReservationFinalized = true
|
||||
if finished.FinalChargeAmount > 0 {
|
||||
if err := s.emit(ctx, task.ID, "task.billing.settled", "succeeded", "billing", 0.98, "task billing settled", map[string]any{
|
||||
"amount": finished.FinalChargeAmount,
|
||||
@@ -695,6 +713,11 @@ func (s *Service) clientFor(candidate store.RuntimeModelCandidate, simulated boo
|
||||
if key == "" {
|
||||
key = strings.ToLower(strings.TrimSpace(candidate.Provider))
|
||||
}
|
||||
provider := strings.ToLower(strings.TrimSpace(candidate.Provider))
|
||||
baseURL := strings.ToLower(strings.TrimSpace(candidate.BaseURL))
|
||||
if key == "google-gemini" || provider == "gemini" || provider == "google-gemini" || provider == "gemini-openai" || strings.Contains(baseURL, "generativelanguage.googleapis.com") {
|
||||
key = "gemini"
|
||||
}
|
||||
if client, ok := s.clients[key]; ok {
|
||||
return client
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user