Files
new-api/middleware/model-rate-limit.go
CaIon 45fbb149f0 fix(rate-limit): count each group with its own limit separately
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.
2026-10-15 22:38:36 +08:00

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
}