one-api/relay/billing/billing-instance.go
2024-06-13 16:50:51 +08:00

161 lines
5.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package billing
import (
"context"
"errors"
"fmt"
"net/http"
"github.com/songquanpeng/one-api/common/logger"
"github.com/songquanpeng/one-api/model"
"github.com/songquanpeng/one-api/relay/adaptor/openai"
billingratio "github.com/songquanpeng/one-api/relay/billing/ratio"
relaymodel "github.com/songquanpeng/one-api/relay/model"
)
// Bookkeeper 记账员逻辑,用于处理用户的配额消费
// 预扣配额检测逻辑:
//
// 开始请求前根据不同的请求类型预先计算需要消费的配额根据请求用户和token的配额余量来判断是否有足够的配额来满足这个请求
// 如果余量配额不能满足这个请求,直接返回错误, 如果余量配额可以满足这个请求,那么预先消费这个配额,然后开始请求, 如果余量远远超过这个请求,那么不需要预先消费配额
// 由于预先计算的配额不是实际消费的配额所以需要在请求结束后根据实际消费的配额来更新用户和token的配额退费或者扣费。
type Bookkeeper interface {
// 获取模型的费率
ModelRatio(model string) float64
// 获取组的费率
GroupRation(group string) float64
// 获取模型的补全费率
ModelCompletionRatio(model string) float64
// 根据消费记录扣除用户token 的配额
Consume(ctx context.Context, consumeLog *ConsumeLog)
// 预消费配额, 当用户配额不足时,预消费配额, 预消费成功返回预消费的配额,失败返回错误, 如果预消费的配额为0表示用户有足够的配额
PreConsumeQuota(ctx context.Context, preConsumedQuota int64, userId, tokenId int) (int64, *relaymodel.ErrorWithStatusCode)
// 退回预消费的配额, 这通常在调用上游api失败的时候执行
RefundQuota(ctx context.Context, preConsumedQuota int64, tokenId int)
// 检测用户是否有足够的配额
// UserHasEnoughQuota(ctx context.Context, userID int, quota int64) bool
// 检测用户是否有远远超过需求的配额, 如果用户的配额远远超过需求,那么不需要预消费配额
// UserHasMuchMoreQuota(ctx context.Context, userID int, quota int64) bool
}
type defaultBookkeeper struct {
}
func NewBookkeeper() Bookkeeper {
return &defaultBookkeeper{}
}
func (b *defaultBookkeeper) ModelRatio(model string) float64 {
return billingratio.GetModelRatio(model)
}
func (b *defaultBookkeeper) GroupRation(group string) float64 {
return billingratio.GetGroupRatio(group)
}
func (b *defaultBookkeeper) ModelCompletionRatio(model string) float64 {
return billingratio.GetCompletionRatio(model)
}
func (b *defaultBookkeeper) Ratio(group, model string) float64 {
modelRatio := billingratio.GetModelRatio(model)
groupRatio := billingratio.GetGroupRatio(group)
return modelRatio * groupRatio
}
// ConsumeLog 消费记录实体
type ConsumeLog struct {
UserId int
ChannelId int
PromptTokens int
CompletionTokens int
ModelName string
TokenId int
TokenName string
Quota int64
Content string
PreConsumedQuota int64
}
func (b *defaultBookkeeper) UserHasEnoughQuota(ctx context.Context, userID int, quota int64) bool {
userQuota, err := model.CacheGetUserQuota(ctx, userID)
if err != nil {
return false
}
return userQuota >= quota
}
func (b *defaultBookkeeper) UserHasMuchMoreQuota(ctx context.Context, userID int, quota int64) bool {
userQuota, err := model.CacheGetUserQuota(ctx, userID)
if err != nil {
return false
}
return userQuota > 100*quota
}
func (b *defaultBookkeeper) Consume(ctx context.Context, consumeLog *ConsumeLog) {
// 更新 access_token 的配额
quotaDelta := consumeLog.Quota - consumeLog.PreConsumedQuota
err := model.PostConsumeTokenQuota(consumeLog.TokenId, quotaDelta)
if err != nil {
logger.SysError("error consuming token remain quota: " + err.Error())
}
err = model.CacheUpdateUserQuota(ctx, consumeLog.UserId)
if err != nil {
logger.SysError("error update user quota cache: " + err.Error())
}
// 更新用户的配额
model.UpdateUserUsedQuotaAndRequestCount(consumeLog.UserId, consumeLog.Quota)
// 更新渠道的配额
model.UpdateChannelUsedQuota(consumeLog.ChannelId, consumeLog.Quota)
// 记录消费日志
model.RecordConsumeLog(
ctx,
consumeLog.UserId,
consumeLog.ChannelId,
consumeLog.PromptTokens,
consumeLog.CompletionTokens,
consumeLog.ModelName,
consumeLog.TokenName,
consumeLog.Quota,
consumeLog.Content,
)
}
func (b *defaultBookkeeper) PreConsumeQuota(ctx context.Context, preConsumedQuota int64, userId, tokenId int) (int64, *relaymodel.ErrorWithStatusCode) {
userQuota, err := model.CacheGetUserQuota(ctx, userId)
if err != nil {
return preConsumedQuota, openai.ErrorWrapper(err, "get_user_quota_failed", http.StatusInternalServerError)
}
if userQuota-preConsumedQuota < 0 {
return preConsumedQuota, openai.ErrorWrapper(errors.New("user quota is not enough"), "insufficient_user_quota", http.StatusForbidden)
}
err = model.CacheDecreaseUserQuota(userId, preConsumedQuota)
if err != nil {
return preConsumedQuota, openai.ErrorWrapper(err, "decrease_user_quota_failed", http.StatusInternalServerError)
}
if userQuota > 100*preConsumedQuota {
// in this case, we do not pre-consume quota
// because the user has enough quota
preConsumedQuota = 0
logger.Info(ctx, fmt.Sprintf("user %d has enough quota %d, trusted and no need to pre-consume", userId, userQuota))
}
if preConsumedQuota > 0 {
err := model.PreConsumeTokenQuota(tokenId, preConsumedQuota)
if err != nil {
return preConsumedQuota, openai.ErrorWrapper(err, "pre_consume_token_quota_failed", http.StatusForbidden)
}
}
return preConsumedQuota, nil
}
func (b *defaultBookkeeper) RefundQuota(ctx context.Context, preConsumedQuota int64, tokenId int) {
if preConsumedQuota != 0 {
err := model.PostConsumeTokenQuota(tokenId, -preConsumedQuota)
if err != nil {
logger.Error(ctx, "error return pre-consumed quota: "+err.Error())
}
}
}