mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-10-28 08:35:33 +08:00
Group rate limits only picked the limit; every request was still counted against the user as a whole. A user with 200 successful requests in a group allowed 500 per minute got a 429 on the first request in a group allowed 50, and in Redis one request in the smaller group cut the larger group's remaining burst down to the smaller one. A group that has its own limit now counts each user's requests on its own, for both the total and the success limit. Groups without their own limit still share the user's global limit, so tokens spread over several groups do not multiply it.
232 lines
7.2 KiB
Go
232 lines
7.2 KiB
Go
package middleware
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/common/limiter"
|
|
"github.com/QuantumNous/new-api/constant"
|
|
"github.com/QuantumNous/new-api/i18n"
|
|
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
|
"github.com/QuantumNous/new-api/setting"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/go-redis/redis/v8"
|
|
)
|
|
|
|
const (
|
|
ModelRequestRateLimitCountMark = "MRRL"
|
|
ModelRequestRateLimitSuccessCountMark = "MRRLS"
|
|
modelRateLimitTimeFormat = "2006-01-02T15:04:05.000Z"
|
|
)
|
|
|
|
// 检查Redis中的请求限制
|
|
func checkRedisRateLimit(ctx context.Context, rdb *redis.Client, key string, maxCount int, duration int64) (bool, error) {
|
|
// 如果maxCount为0,表示不限制
|
|
if maxCount == 0 {
|
|
return true, nil
|
|
}
|
|
|
|
// 获取当前计数
|
|
length, err := rdb.LLen(ctx, key).Result()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// 如果未达到限制,允许请求
|
|
if length < int64(maxCount) {
|
|
return true, nil
|
|
}
|
|
|
|
// 检查时间窗口
|
|
oldTimeStr, _ := rdb.LIndex(ctx, key, -1).Result()
|
|
oldTime, err := time.Parse(modelRateLimitTimeFormat, oldTimeStr)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
nowTimeStr := time.Now().UTC().Format(modelRateLimitTimeFormat)
|
|
nowTime, err := time.Parse(modelRateLimitTimeFormat, nowTimeStr)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
// 如果在时间窗口内已达到限制,拒绝请求
|
|
subTime := nowTime.Sub(oldTime).Seconds()
|
|
if int64(subTime) < duration {
|
|
rdb.Expire(ctx, key, time.Duration(setting.ModelRequestRateLimitDurationMinutes)*time.Minute)
|
|
return false, nil
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// 记录Redis请求
|
|
func recordRedisRequest(ctx context.Context, rdb *redis.Client, key string, maxCount int) {
|
|
// 如果maxCount为0,不记录请求
|
|
if maxCount == 0 {
|
|
return
|
|
}
|
|
|
|
now := time.Now().UTC().Format(modelRateLimitTimeFormat)
|
|
rdb.LPush(ctx, key, now)
|
|
rdb.LTrim(ctx, key, 0, int64(maxCount-1))
|
|
rdb.Expire(ctx, key, time.Duration(setting.ModelRequestRateLimitDurationMinutes)*time.Minute)
|
|
}
|
|
|
|
// Redis限流处理器
|
|
func redisRateLimitHandler(subject string, duration int64, totalMaxCount, successMaxCount int) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
ctx := context.Background()
|
|
rdb := common.RDB
|
|
|
|
// 1. 检查成功请求数限制
|
|
successKey := fmt.Sprintf("rateLimit:%s:%s", ModelRequestRateLimitSuccessCountMark, subject)
|
|
allowed, err := checkRedisRateLimit(ctx, rdb, successKey, successMaxCount, duration)
|
|
if err != nil {
|
|
fmt.Println(common.LogText("failed to check the successful request limit: %s", err.Error()))
|
|
abortWithOpenAiMessage(c, http.StatusInternalServerError, "rate_limit_check_failed")
|
|
return
|
|
}
|
|
if !allowed {
|
|
abortWithOpenAiMessage(c, http.StatusTooManyRequests, i18n.T(c, i18n.MsgRateLimitReached, map[string]any{"Minutes": setting.ModelRequestRateLimitDurationMinutes, "Max": successMaxCount}))
|
|
return
|
|
}
|
|
|
|
//2.检查总请求数限制并记录总请求(当totalMaxCount为0时会自动跳过,使用令牌桶限流器
|
|
if totalMaxCount > 0 {
|
|
totalKey := fmt.Sprintf("rateLimit:%s", subject)
|
|
// 初始化
|
|
tb := limiter.New(ctx, rdb)
|
|
allowed, err = tb.Allow(
|
|
ctx,
|
|
totalKey,
|
|
limiter.WithCapacity(rateLimitCapacity(totalMaxCount, duration)),
|
|
limiter.WithRate(int64(totalMaxCount)),
|
|
limiter.WithRequested(duration),
|
|
)
|
|
|
|
if err != nil {
|
|
fmt.Println(common.LogText("failed to check the total request limit: %s", err.Error()))
|
|
abortWithOpenAiMessage(c, http.StatusInternalServerError, "rate_limit_check_failed")
|
|
return
|
|
}
|
|
|
|
if !allowed {
|
|
abortWithOpenAiMessage(c, http.StatusTooManyRequests, i18n.T(c, i18n.MsgRateLimitTotalReached, map[string]any{"Minutes": setting.ModelRequestRateLimitDurationMinutes, "Max": totalMaxCount}))
|
|
return
|
|
}
|
|
}
|
|
|
|
// 4. 处理请求
|
|
c.Next()
|
|
|
|
// 5. 如果请求成功,记录成功请求
|
|
if modelRequestSucceeded(c) {
|
|
recordRedisRequest(ctx, rdb, successKey, successMaxCount)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 内存限流处理器
|
|
func memoryRateLimitHandler(subject string, duration int64, totalMaxCount, successMaxCount int) gin.HandlerFunc {
|
|
inMemoryRateLimiter.Init(time.Duration(setting.ModelRequestRateLimitDurationMinutes) * time.Minute)
|
|
|
|
return func(c *gin.Context) {
|
|
totalKey := ModelRequestRateLimitCountMark + subject
|
|
successKey := ModelRequestRateLimitSuccessCountMark + subject
|
|
|
|
// 1. 检查总请求数限制(当totalMaxCount为0时跳过)
|
|
if totalMaxCount > 0 && !inMemoryRateLimiter.Request(totalKey, totalMaxCount, duration) {
|
|
abortWithOpenAiMessage(c, http.StatusTooManyRequests, i18n.T(c, i18n.MsgRateLimitTotalReached, map[string]any{"Minutes": setting.ModelRequestRateLimitDurationMinutes, "Max": totalMaxCount}))
|
|
return
|
|
}
|
|
|
|
var reservation *common.RateLimitReservation
|
|
if successMaxCount > 0 {
|
|
reservation = inMemoryRateLimiter.Reserve(successKey, successMaxCount, duration)
|
|
if reservation == nil {
|
|
abortWithOpenAiMessage(c, http.StatusTooManyRequests, i18n.T(c, i18n.MsgRateLimitReached, map[string]any{"Minutes": setting.ModelRequestRateLimitDurationMinutes, "Max": successMaxCount}))
|
|
return
|
|
}
|
|
defer reservation.Complete(false)
|
|
}
|
|
|
|
// 3. 处理请求
|
|
c.Next()
|
|
|
|
// 4. 如果请求成功,记录到实际的成功请求计数中
|
|
reservation.Complete(modelRequestSucceeded(c))
|
|
}
|
|
}
|
|
|
|
func modelRequestSucceeded(c *gin.Context) bool {
|
|
status, _ := common.GetContextKeyType[*relaycommon.StreamStatus](c, constant.ContextKeyResponseStreamStatus)
|
|
return c.Writer.Status() < 400 && !status.ResponseFailed()
|
|
}
|
|
|
|
// ModelRequestRateLimit 模型请求限流中间件
|
|
func ModelRequestRateLimit() func(c *gin.Context) {
|
|
return func(c *gin.Context) {
|
|
// 在每个请求时检查是否启用限流
|
|
if !setting.ModelRequestRateLimitEnabled {
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
// 计算限流参数
|
|
duration := rateLimitDurationSeconds(setting.ModelRequestRateLimitDurationMinutes)
|
|
totalMaxCount := setting.ModelRequestRateLimitCount
|
|
successMaxCount := setting.ModelRequestRateLimitSuccessCount
|
|
|
|
// 获取分组
|
|
group := common.GetContextKeyString(c, constant.ContextKeyTokenGroup)
|
|
if group == "" {
|
|
group = common.GetContextKeyString(c, constant.ContextKeyUserGroup)
|
|
}
|
|
|
|
// 获取分组的限流配置。配置了独立限流的分组按用户和分组单独计数,
|
|
// 其余分组共用该用户的全局计数
|
|
subject := strconv.Itoa(c.GetInt("id"))
|
|
groupTotalCount, groupSuccessCount, found := setting.GetGroupRateLimit(group)
|
|
if found {
|
|
totalMaxCount = groupTotalCount
|
|
successMaxCount = groupSuccessCount
|
|
subject += ":" + group
|
|
}
|
|
|
|
// 根据存储类型选择并执行限流处理器
|
|
if common.RedisEnabled {
|
|
redisRateLimitHandler(subject, duration, totalMaxCount, successMaxCount)(c)
|
|
} else {
|
|
memoryRateLimitHandler(subject, duration, totalMaxCount, successMaxCount)(c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func rateLimitDurationSeconds(durationMinutes int) int64 {
|
|
if durationMinutes <= 0 {
|
|
return 0
|
|
}
|
|
minutes := int64(durationMinutes)
|
|
if minutes > math.MaxInt64/60 {
|
|
return math.MaxInt64
|
|
}
|
|
return minutes * 60
|
|
}
|
|
|
|
func rateLimitCapacity(count int, durationSeconds int64) int64 {
|
|
if count <= 0 || durationSeconds <= 0 {
|
|
return 0
|
|
}
|
|
c := int64(count)
|
|
if c > math.MaxInt64/durationSeconds {
|
|
return math.MaxInt64
|
|
}
|
|
return c * durationSeconds
|
|
}
|