diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 000000000000..9397c74a697c --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,11 @@ +{ + "permissions": { + "allow": [ + "Bash(go build:*)", + "Bash(npm run build)", + "Bash(git:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/Dockerfile b/Dockerfile index 08cc86f7254a..fb4cbbc367a9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,10 +9,12 @@ COPY ./VERSION . RUN DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) bun run build FROM golang:alpine AS builder2 +ENV GO111MODULE=on CGO_ENABLED=0 + +ARG TARGETOS +ARG TARGETARCH +ENV GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} -ENV GO111MODULE=on \ - CGO_ENABLED=0 \ - GOOS=linux WORKDIR /build diff --git a/controller/channel-test.go b/controller/channel-test.go index 5bbc20ebcc9f..60635c2095b2 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -586,9 +586,9 @@ func testAllChannels(notify bool) error { } // enable channel - if !isChannelEnabled && service.ShouldEnableChannel(newAPIError, channel.Status) { - service.EnableChannel(channel.Id, common.GetContextKeyString(result.context, constant.ContextKeyChannelKey), channel.Name) - } + //if !isChannelEnabled && service.ShouldEnableChannel(newAPIError, channel.Status) { + // service.EnableChannel(channel.Id, common.GetContextKeyString(result.context, constant.ContextKeyChannelKey), channel.Name) + //} channel.UpdateResponseTime(milliseconds) time.Sleep(common.RequestInterval) diff --git a/controller/option.go b/controller/option.go index 7d1c676f540e..5e9a2b168931 100644 --- a/controller/option.go +++ b/controller/option.go @@ -164,6 +164,24 @@ func UpdateOption(c *gin.Context) { }) return } + case "TokenRateLimitGroup": + err = setting.CheckTokenRateLimitGroup(option.Value.(string)) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + case "TokenDailyRateLimitGroup": + err = setting.CheckTokenDailyRateLimitGroup(option.Value.(string)) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } case "console_setting.api_info": err = console_setting.ValidateConsoleSettings(option.Value.(string), "ApiInfo") if err != nil { diff --git a/controller/relay.go b/controller/relay.go index 23d7251532b4..e99053dfd7d5 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -60,8 +60,29 @@ func geminiRelayHandler(c *gin.Context, info *relaycommon.RelayInfo) *types.NewA return err } -func Relay(c *gin.Context, relayFormat types.RelayFormat) { +func relayToChannel(c *gin.Context, relayInfo *relaycommon.RelayInfo, channel *model.Channel) *types.NewAPIError { + requestBody, _ := common.GetRequestBody(c) + c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody)) + + var newAPIError *types.NewAPIError + switch relayInfo.RelayFormat { + case types.RelayFormatOpenAIRealtime: + newAPIError = relay.WssHelper(c, relayInfo) + case types.RelayFormatClaude: + newAPIError = relay.ClaudeHelper(c, relayInfo) + case types.RelayFormatGemini: + newAPIError = geminiRelayHandler(c, relayInfo) + default: + newAPIError = relayHandler(c, relayInfo) + } + if newAPIError != nil { + processChannelError(c, *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, common.GetContextKeyString(c, constant.ContextKeyChannelKey), channel.GetAutoBan()), newAPIError) + } + return newAPIError +} + +func Relay(c *gin.Context, relayFormat types.RelayFormat) { requestId := c.GetString(common.RequestIdKey) group := common.GetContextKeyString(c, constant.ContextKeyUsingGroup) originalModel := common.GetContextKeyString(c, constant.ContextKeyOriginalModel) @@ -137,8 +158,6 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) { return } - // common.SetContextKey(c, constant.ContextKeyTokenCountMeta, meta) - newAPIError = service.PreConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo) if newAPIError != nil { return @@ -151,6 +170,7 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) { } }() + // Main retry loop for selecting channels for i := 0; i <= common.RetryTimes; i++ { channel, err := getChannel(c, group, originalModel, i) if err != nil { @@ -160,28 +180,64 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) { } addUsedChannel(c, channel.Id) - requestBody, _ := common.GetRequestBody(c) - c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody)) - - switch relayFormat { - case types.RelayFormatOpenAIRealtime: - newAPIError = relay.WssHelper(c, relayInfo) - case types.RelayFormatClaude: - newAPIError = relay.ClaudeHelper(c, relayInfo) - case types.RelayFormatGemini: - newAPIError = geminiRelayHandler(c, relayInfo) - default: - newAPIError = relayHandler(c, relayInfo) - } - if newAPIError == nil { - return + // Inner loop for retrying keys within a multi-key channel + if channel.ChannelInfo.IsMultiKey { + key, keyIdx, keyErr := channel.GetNextEnabledKey() + if keyErr != nil { + newAPIError = keyErr + break // No keys available, break to outer loop to switch channel + } + + triedKeys := make(map[int]bool) // Track tried keys for this channel + for j := 0; j < channel.ChannelInfo.MultiKeySize && j < 20; j++ { + if _, ok := triedKeys[keyIdx]; ok { + // This key has been tried, get another one + key, keyIdx, keyErr = channel.GetNextEnabledKey() + if keyErr != nil { + newAPIError = keyErr + break + } + continue + } + + // Setup context with the new key + middleware.SetupContextForSelectedChannelWithKey(c, channel, originalModel, key, keyIdx) + triedKeys[keyIdx] = true + + newAPIError = relayToChannel(c, relayInfo, channel) + if newAPIError == nil { + return // Success + } + + // inappropriate 错误直接切换渠道,不重试同一渠道的多个密钥 + if isInappropriateError(newAPIError) { + goto next_channel + } + + // If the error is not retryable for a key, break the inner loop + if !shouldRetry(c, newAPIError, common.RetryTimes-i) { + goto next_channel // Break inner loop and go to the next channel + } + + // Get next key for retry + key, keyIdx, keyErr = channel.GetNextEnabledKey() + if keyErr != nil { + newAPIError = keyErr + break // No more keys to try + } + } + } else { + // Single key channel logic + newAPIError = relayToChannel(c, relayInfo, channel) + if newAPIError == nil { + return // Success + } } - processChannelError(c, *types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, common.GetContextKeyString(c, constant.ContextKeyChannelKey), channel.GetAutoBan()), newAPIError) - + next_channel: if !shouldRetry(c, newAPIError, common.RetryTimes-i) { - break + break // Break outer loop if error is not retryable for the channel } } @@ -233,10 +289,29 @@ func getChannel(c *gin.Context, group, originalModel string, retryCount int) (*m return channel, nil } +// isInappropriateError 检测错误是否包含 inappropriate 字样 +// 此类错误需要强制渠道间重试,不重试同一渠道的多个密钥 +func isInappropriateError(openaiErr *types.NewAPIError) bool { + if openaiErr == nil { + return false + } + return strings.Contains(strings.ToLower(openaiErr.Error()), "inappropriate") +} + func shouldRetry(c *gin.Context, openaiErr *types.NewAPIError, retryTimes int) bool { if openaiErr == nil { return false } + if strings.Contains(openaiErr.Error(), "no response received") { + return false + } + if strings.Contains(openaiErr.Error(), "no candidates reMeoWturned") { + return false + } + // inappropriate 错误强制渠道间重试 + if isInappropriateError(openaiErr) { + return true + } if types.IsChannelError(openaiErr) { return true } @@ -281,7 +356,11 @@ func processChannelError(c *gin.Context, channelError types.ChannelError, err *t // do not use context to get channel info, there may be inconsistent channel info when processing asynchronously if service.ShouldDisableChannel(channelError.ChannelId, err) && channelError.AutoBan { gopool.Go(func() { - service.DisableChannel(channelError, err.Error()) + reason := err.Error() + if err.GetErrorType() == "insufficient_quota" { + reason = "insufficient_quota: " + reason + } + service.DisableChannel(channelError, reason) }) } diff --git a/dto/gemini.go b/dto/gemini.go index b91701723c9f..5d96eb937f78 100644 --- a/dto/gemini.go +++ b/dto/gemini.go @@ -16,7 +16,7 @@ type GeminiChatRequest struct { GenerationConfig GeminiChatGenerationConfig `json:"generationConfig,omitempty"` Tools json.RawMessage `json:"tools,omitempty"` ToolConfig *ToolConfig `json:"toolConfig,omitempty"` - SystemInstructions *GeminiChatContent `json:"systemInstruction,omitempty"` + SystemInstruction *GeminiChatContent `json:"system_instruction,omitempty"` CachedContent string `json:"cachedContent,omitempty"` } diff --git a/middleware/distributor.go b/middleware/distributor.go index a33ca5af95eb..74e81e2b53e5 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -298,6 +298,10 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode if newAPIError != nil { return newAPIError } + return SetupContextForSelectedChannelWithKey(c, channel, modelName, key, index) +} + +func SetupContextForSelectedChannelWithKey(c *gin.Context, channel *model.Channel, modelName string, key string, index int) *types.NewAPIError { if channel.ChannelInfo.IsMultiKey { common.SetContextKey(c, constant.ContextKeyChannelIsMultiKey, true) common.SetContextKey(c, constant.ContextKeyChannelMultiKeyIndex, index) diff --git a/middleware/model-rate-limit.go b/middleware/model-rate-limit.go index 14d9a737ea04..5197d42656b2 100644 --- a/middleware/model-rate-limit.go +++ b/middleware/model-rate-limit.go @@ -73,15 +73,17 @@ func recordRedisRequest(ctx context.Context, rdb *redis.Client, key string, maxC rdb.Expire(ctx, key, time.Duration(setting.ModelRequestRateLimitDurationMinutes)*time.Minute) } -// Redis限流处理器 +// Redis限流处理器 (per-user 限流,使用 user ID) func redisRateLimitHandler(duration int64, totalMaxCount, successMaxCount int) gin.HandlerFunc { return func(c *gin.Context) { - userId := strconv.Itoa(c.GetInt("id")) + // per-user 限流使用 user ID + userId := c.GetInt("id") + rateLimitKey := strconv.Itoa(userId) ctx := context.Background() rdb := common.RDB // 1. 检查成功请求数限制 - successKey := fmt.Sprintf("rateLimit:%s:%s", ModelRequestRateLimitSuccessCountMark, userId) + successKey := fmt.Sprintf("rateLimit:%s:%s", ModelRequestRateLimitSuccessCountMark, rateLimitKey) allowed, err := checkRedisRateLimit(ctx, rdb, successKey, successMaxCount, duration) if err != nil { fmt.Println("检查成功请求数限制失败:", err.Error()) @@ -95,7 +97,7 @@ func redisRateLimitHandler(duration int64, totalMaxCount, successMaxCount int) g //2.检查总请求数限制并记录总请求(当totalMaxCount为0时会自动跳过,使用令牌桶限流器 if totalMaxCount > 0 { - totalKey := fmt.Sprintf("rateLimit:%s", userId) + totalKey := fmt.Sprintf("rateLimit:%s", rateLimitKey) // 初始化 tb := limiter.New(ctx, rdb) allowed, err = tb.Allow( @@ -127,14 +129,16 @@ func redisRateLimitHandler(duration int64, totalMaxCount, successMaxCount int) g } } -// 内存限流处理器 +// 内存限流处理器 (per-user 限流,使用 user ID) func memoryRateLimitHandler(duration int64, totalMaxCount, successMaxCount int) gin.HandlerFunc { inMemoryRateLimiter.Init(time.Duration(setting.ModelRequestRateLimitDurationMinutes) * time.Minute) return func(c *gin.Context) { - userId := strconv.Itoa(c.GetInt("id")) - totalKey := ModelRequestRateLimitCountMark + userId - successKey := ModelRequestRateLimitSuccessCountMark + userId + // per-user 限流使用 user ID + userId := c.GetInt("id") + rateLimitKey := strconv.Itoa(userId) + totalKey := ModelRequestRateLimitCountMark + rateLimitKey + successKey := ModelRequestRateLimitSuccessCountMark + rateLimitKey // 1. 检查总请求数限制(当totalMaxCount为0时跳过) if totalMaxCount > 0 && !inMemoryRateLimiter.Request(totalKey, totalMaxCount, duration) { @@ -162,12 +166,333 @@ func memoryRateLimitHandler(duration int64, totalMaxCount, successMaxCount int) } } +// Token rate limit constants +const ( + TokenRateLimitCountMark = "TRL" + TokenRateLimitSuccessCountMark = "TRLS" + TokenDailyRateLimitCountMark = "TDRL" + TokenDailyRateLimitSuccessCountMark = "TDRLS" +) + +// checkTokenRateLimit 检查 token 分钟级限流 +func checkTokenRateLimit(c *gin.Context) bool { + if !setting.TokenRateLimitEnabled { + return true + } + + tokenId := common.GetContextKeyInt(c, constant.ContextKeyTokenId) + if tokenId == 0 { + // 如果没有 token ID,跳过 per-key 限流 + return true + } + + // 获取分组配置(使用 token group) + group := common.GetContextKeyString(c, constant.ContextKeyTokenGroup) + totalMaxCount := setting.TokenRateLimitCount + successMaxCount := setting.TokenRateLimitSuccessCount + + // 获取分组的限流配置 + groupTotalCount, groupSuccessCount, found := setting.GetTokenRateLimit(group) + if found { + totalMaxCount = groupTotalCount + successMaxCount = groupSuccessCount + } + + // 如果两个限制都为0,表示不限制 + if totalMaxCount == 0 && successMaxCount == 0 { + return true + } + + rateLimitKey := strconv.Itoa(tokenId) + duration := int64(setting.TokenRateLimitDurationMinutes * 60) + + if common.RedisEnabled { + return checkTokenRateLimitRedis(c, rateLimitKey, totalMaxCount, successMaxCount, duration) + } else { + return checkTokenRateLimitMemory(c, rateLimitKey, totalMaxCount, successMaxCount, duration) + } +} + +// checkTokenRateLimitRedis Redis版本的分钟级限流检查 +func checkTokenRateLimitRedis(c *gin.Context, rateLimitKey string, totalMaxCount, successMaxCount int, duration int64) bool { + ctx := context.Background() + rdb := common.RDB + + // 1. 检查成功请求数限制 + if successMaxCount > 0 { + successKey := fmt.Sprintf("rateLimit:%s:%s", TokenRateLimitSuccessCountMark, rateLimitKey) + allowed, err := checkRedisRateLimit(ctx, rdb, successKey, successMaxCount, duration) + if err != nil { + fmt.Println("检查密钥成功请求数限制失败:", err.Error()) + abortWithOpenAiMessage(c, http.StatusInternalServerError, "rate_limit_check_failed") + return false + } + if !allowed { + abortWithOpenAiMessage(c, http.StatusTooManyRequests, fmt.Sprintf("您已达到密钥请求数限制:%d分钟内最多请求%d次", setting.TokenRateLimitDurationMinutes, successMaxCount)) + return false + } + } + + // 2. 检查总请求数限制 + if totalMaxCount > 0 { + totalKey := fmt.Sprintf("rateLimit:%s:%s", TokenRateLimitCountMark, rateLimitKey) + tb := limiter.New(ctx, rdb) + allowed, err := tb.Allow( + ctx, + totalKey, + limiter.WithCapacity(int64(totalMaxCount)*duration), + limiter.WithRate(int64(totalMaxCount)), + limiter.WithRequested(duration), + ) + + if err != nil { + fmt.Println("检查密钥总请求数限制失败:", err.Error()) + abortWithOpenAiMessage(c, http.StatusInternalServerError, "rate_limit_check_failed") + return false + } + + if !allowed { + abortWithOpenAiMessage(c, http.StatusTooManyRequests, fmt.Sprintf("您已达到密钥总请求数限制:%d分钟内最多请求%d次(包括失败请求)", setting.TokenRateLimitDurationMinutes, totalMaxCount)) + return false + } + } + + return true +} + +// recordTokenRateLimitSuccess 记录分钟级成功请求 +func recordTokenRateLimitSuccess(c *gin.Context) { + if !setting.TokenRateLimitEnabled { + return + } + + tokenId := common.GetContextKeyInt(c, constant.ContextKeyTokenId) + if tokenId == 0 { + return + } + + // 获取分组配置 + group := common.GetContextKeyString(c, constant.ContextKeyTokenGroup) + successMaxCount := setting.TokenRateLimitSuccessCount + + _, groupSuccessCount, found := setting.GetTokenRateLimit(group) + if found { + successMaxCount = groupSuccessCount + } + + if successMaxCount == 0 { + return + } + + rateLimitKey := strconv.Itoa(tokenId) + + if common.RedisEnabled { + ctx := context.Background() + rdb := common.RDB + successKey := fmt.Sprintf("rateLimit:%s:%s", TokenRateLimitSuccessCountMark, rateLimitKey) + recordRedisRequest(ctx, rdb, successKey, successMaxCount) + } else { + duration := int64(setting.TokenRateLimitDurationMinutes * 60) + successKey := TokenRateLimitSuccessCountMark + rateLimitKey + inMemoryRateLimiter.Request(successKey, successMaxCount, duration) + } +} + +// checkTokenRateLimitMemory 内存版本的分钟级限流检查 +func checkTokenRateLimitMemory(c *gin.Context, rateLimitKey string, totalMaxCount, successMaxCount int, duration int64) bool { + inMemoryRateLimiter.Init(time.Duration(setting.TokenRateLimitDurationMinutes) * time.Minute) + + totalKey := TokenRateLimitCountMark + rateLimitKey + successKey := TokenRateLimitSuccessCountMark + rateLimitKey + + // 1. 检查总请求数限制 + if totalMaxCount > 0 && !inMemoryRateLimiter.Request(totalKey, totalMaxCount, duration) { + abortWithOpenAiMessage(c, http.StatusTooManyRequests, fmt.Sprintf("您已达到密钥总请求数限制:%d分钟内最多请求%d次(包括失败请求)", setting.TokenRateLimitDurationMinutes, totalMaxCount)) + return false + } + + // 2. 检查成功请求数限制(使用临时key检查) + if successMaxCount > 0 { + checkKey := successKey + "_check" + if !inMemoryRateLimiter.Request(checkKey, successMaxCount, duration) { + abortWithOpenAiMessage(c, http.StatusTooManyRequests, fmt.Sprintf("您已达到密钥请求数限制:%d分钟内最多请求%d次", setting.TokenRateLimitDurationMinutes, successMaxCount)) + return false + } + } + + return true +} + +// checkTokenDailyRateLimit 检查 token 每日限流 +func checkTokenDailyRateLimit(c *gin.Context) bool { + if !setting.TokenDailyRateLimitEnabled { + return true + } + + tokenId := common.GetContextKeyInt(c, constant.ContextKeyTokenId) + if tokenId == 0 { + // 如果没有 token ID,跳过 per-key 限流 + return true + } + + // 获取分组配置 + group := common.GetContextKeyString(c, constant.ContextKeyTokenGroup) + totalMaxCount := setting.TokenDailyRateLimitCount + successMaxCount := setting.TokenDailyRateLimitSuccessCount + + // 获取分组的限流配置 + groupTotalCount, groupSuccessCount, found := setting.GetTokenDailyRateLimit(group) + if found { + totalMaxCount = groupTotalCount + successMaxCount = groupSuccessCount + } + + // 如果两个限制都为0,表示不限制 + if totalMaxCount == 0 && successMaxCount == 0 { + return true + } + + rateLimitKey := strconv.Itoa(tokenId) + duration := int64(86400) // 24小时 = 86400秒 + + if common.RedisEnabled { + return checkTokenDailyRateLimitRedis(c, rateLimitKey, totalMaxCount, successMaxCount, duration) + } else { + return checkTokenDailyRateLimitMemory(c, rateLimitKey, totalMaxCount, successMaxCount, duration) + } +} + +// checkTokenDailyRateLimitRedis Redis版本的每日限流检查 +func checkTokenDailyRateLimitRedis(c *gin.Context, rateLimitKey string, totalMaxCount, successMaxCount int, duration int64) bool { + ctx := context.Background() + rdb := common.RDB + + // 1. 检查成功请求数限制 + if successMaxCount > 0 { + successKey := fmt.Sprintf("rateLimit:%s:%s", TokenDailyRateLimitSuccessCountMark, rateLimitKey) + allowed, err := checkRedisRateLimit(ctx, rdb, successKey, successMaxCount, duration) + if err != nil { + fmt.Println("检查每日成功请求数限制失败:", err.Error()) + abortWithOpenAiMessage(c, http.StatusInternalServerError, "rate_limit_check_failed") + return false + } + if !allowed { + abortWithOpenAiMessage(c, http.StatusTooManyRequests, "您已达到每日请求数限制") + return false + } + } + + // 2. 检查总请求数限制 + if totalMaxCount > 0 { + totalKey := fmt.Sprintf("rateLimit:%s:%s", TokenDailyRateLimitCountMark, rateLimitKey) + tb := limiter.New(ctx, rdb) + allowed, err := tb.Allow( + ctx, + totalKey, + limiter.WithCapacity(int64(totalMaxCount)*duration), + limiter.WithRate(int64(totalMaxCount)), + limiter.WithRequested(duration), + ) + + if err != nil { + fmt.Println("检查每日总请求数限制失败:", err.Error()) + abortWithOpenAiMessage(c, http.StatusInternalServerError, "rate_limit_check_failed") + return false + } + + if !allowed { + abortWithOpenAiMessage(c, http.StatusTooManyRequests, "您已达到每日总请求数限制(包括失败请求)") + return false + } + } + + return true +} + +// recordTokenDailySuccess 记录每日成功请求 +func recordTokenDailySuccess(c *gin.Context) { + if !setting.TokenDailyRateLimitEnabled { + return + } + + tokenId := common.GetContextKeyInt(c, constant.ContextKeyTokenId) + if tokenId == 0 { + return + } + + // 获取分组配置 + group := common.GetContextKeyString(c, constant.ContextKeyTokenGroup) + successMaxCount := setting.TokenDailyRateLimitSuccessCount + + _, groupSuccessCount, found := setting.GetTokenDailyRateLimit(group) + if found { + successMaxCount = groupSuccessCount + } + + if successMaxCount == 0 { + return + } + + rateLimitKey := strconv.Itoa(tokenId) + + if common.RedisEnabled { + ctx := context.Background() + rdb := common.RDB + successKey := fmt.Sprintf("rateLimit:%s:%s", TokenDailyRateLimitSuccessCountMark, rateLimitKey) + recordRedisRequest(ctx, rdb, successKey, successMaxCount) + } else { + duration := int64(86400) + successKey := TokenDailyRateLimitSuccessCountMark + rateLimitKey + inMemoryRateLimiter.Request(successKey, successMaxCount, duration) + } +} + +// checkTokenDailyRateLimitMemory 内存版本的每日限流检查 +func checkTokenDailyRateLimitMemory(c *gin.Context, rateLimitKey string, totalMaxCount, successMaxCount int, duration int64) bool { + inMemoryRateLimiter.Init(24 * time.Hour) + + totalKey := TokenDailyRateLimitCountMark + rateLimitKey + successKey := TokenDailyRateLimitSuccessCountMark + rateLimitKey + + // 1. 检查总请求数限制 + if totalMaxCount > 0 && !inMemoryRateLimiter.Request(totalKey, totalMaxCount, duration) { + abortWithOpenAiMessage(c, http.StatusTooManyRequests, "您已达到每日总请求数限制(包括失败请求)") + return false + } + + // 2. 检查成功请求数限制(使用临时key检查) + if successMaxCount > 0 { + checkKey := successKey + "_check" + if !inMemoryRateLimiter.Request(checkKey, successMaxCount, duration) { + abortWithOpenAiMessage(c, http.StatusTooManyRequests, "您已达到每日请求数限制") + return false + } + } + + return true +} + // ModelRequestRateLimit 模型请求限流中间件 func ModelRequestRateLimit() func(c *gin.Context) { return func(c *gin.Context) { - // 在每个请求时检查是否启用限流 + // 1. 先检查 per-key 分钟级限流(新功能) + if !checkTokenRateLimit(c) { + return + } + + // 2. 检查 per-key 每日限流(新功能) + if !checkTokenDailyRateLimit(c) { + return + } + + // 3. 再检查原有的 per-user 限流(保持兼容性) if !setting.ModelRequestRateLimitEnabled { c.Next() + // 请求成功后记录 per-key 成功请求 + if c.Writer.Status() < 400 { + recordTokenRateLimitSuccess(c) + recordTokenDailySuccess(c) + } return } @@ -176,14 +501,11 @@ func ModelRequestRateLimit() func(c *gin.Context) { totalMaxCount := setting.ModelRequestRateLimitCount successMaxCount := setting.ModelRequestRateLimitSuccessCount - // 获取分组 - group := common.GetContextKeyString(c, constant.ContextKeyTokenGroup) - if group == "" { - group = common.GetContextKeyString(c, constant.ContextKeyUserGroup) - } + // per-user 限流使用 user group(不是 token group) + userGroup := common.GetContextKeyString(c, constant.ContextKeyUserGroup) //获取分组的限流配置 - groupTotalCount, groupSuccessCount, found := setting.GetGroupRateLimit(group) + groupTotalCount, groupSuccessCount, found := setting.GetGroupRateLimit(userGroup) if found { totalMaxCount = groupTotalCount successMaxCount = groupSuccessCount @@ -195,5 +517,11 @@ func ModelRequestRateLimit() func(c *gin.Context) { } else { memoryRateLimitHandler(duration, totalMaxCount, successMaxCount)(c) } + + // 请求成功后记录 per-key 成功请求 + if c.Writer.Status() < 400 { + recordTokenRateLimitSuccess(c) + recordTokenDailySuccess(c) + } } } diff --git a/model/channel.go b/model/channel.go index 8d1616a9b4c6..a546e2bf3c11 100644 --- a/model/channel.go +++ b/model/channel.go @@ -577,6 +577,10 @@ func handlerMultiKeyUpdate(channel *Channel, usingKey string, status int, reason } if status == common.ChannelStatusEnabled { delete(channel.ChannelInfo.MultiKeyStatusList, keyIndex) + // If the channel was auto-disabled, re-enable it since at least one key is now enabled. + if channel.Status == common.ChannelStatusAutoDisabled { + channel.Status = common.ChannelStatusEnabled + } } else { channel.ChannelInfo.MultiKeyStatusList[keyIndex] = status if channel.ChannelInfo.MultiKeyDisabledReason == nil { diff --git a/model/option.go b/model/option.go index 77525ea25239..3078f28dd809 100644 --- a/model/option.go +++ b/model/option.go @@ -106,6 +106,15 @@ func InitOptionMap() { common.OptionMap["ModelRequestRateLimitDurationMinutes"] = strconv.Itoa(setting.ModelRequestRateLimitDurationMinutes) common.OptionMap["ModelRequestRateLimitSuccessCount"] = strconv.Itoa(setting.ModelRequestRateLimitSuccessCount) common.OptionMap["ModelRequestRateLimitGroup"] = setting.ModelRequestRateLimitGroup2JSONString() + common.OptionMap["TokenRateLimitEnabled"] = strconv.FormatBool(setting.TokenRateLimitEnabled) + common.OptionMap["TokenRateLimitDurationMinutes"] = strconv.Itoa(setting.TokenRateLimitDurationMinutes) + common.OptionMap["TokenRateLimitCount"] = strconv.Itoa(setting.TokenRateLimitCount) + common.OptionMap["TokenRateLimitSuccessCount"] = strconv.Itoa(setting.TokenRateLimitSuccessCount) + common.OptionMap["TokenRateLimitGroup"] = setting.TokenRateLimitGroup2JSONString() + common.OptionMap["TokenDailyRateLimitEnabled"] = strconv.FormatBool(setting.TokenDailyRateLimitEnabled) + common.OptionMap["TokenDailyRateLimitCount"] = strconv.Itoa(setting.TokenDailyRateLimitCount) + common.OptionMap["TokenDailyRateLimitSuccessCount"] = strconv.Itoa(setting.TokenDailyRateLimitSuccessCount) + common.OptionMap["TokenDailyRateLimitGroup"] = setting.TokenDailyRateLimitGroup2JSONString() common.OptionMap["ModelRatio"] = ratio_setting.ModelRatio2JSONString() common.OptionMap["ModelPrice"] = ratio_setting.ModelPrice2JSONString() common.OptionMap["CacheRatio"] = ratio_setting.CacheRatio2JSONString() @@ -279,6 +288,10 @@ func updateOptionMap(key string, value string) (err error) { setting.CheckSensitiveOnPromptEnabled = boolValue case "ModelRequestRateLimitEnabled": setting.ModelRequestRateLimitEnabled = boolValue + case "TokenRateLimitEnabled": + setting.TokenRateLimitEnabled = boolValue + case "TokenDailyRateLimitEnabled": + setting.TokenDailyRateLimitEnabled = boolValue case "StopOnSensitiveEnabled": setting.StopOnSensitiveEnabled = boolValue case "SMTPSSLEnabled": @@ -391,6 +404,20 @@ func updateOptionMap(key string, value string) (err error) { setting.ModelRequestRateLimitSuccessCount, _ = strconv.Atoi(value) case "ModelRequestRateLimitGroup": err = setting.UpdateModelRequestRateLimitGroupByJSONString(value) + case "TokenRateLimitDurationMinutes": + setting.TokenRateLimitDurationMinutes, _ = strconv.Atoi(value) + case "TokenRateLimitCount": + setting.TokenRateLimitCount, _ = strconv.Atoi(value) + case "TokenRateLimitSuccessCount": + setting.TokenRateLimitSuccessCount, _ = strconv.Atoi(value) + case "TokenRateLimitGroup": + err = setting.UpdateTokenRateLimitGroupByJSONString(value) + case "TokenDailyRateLimitCount": + setting.TokenDailyRateLimitCount, _ = strconv.Atoi(value) + case "TokenDailyRateLimitSuccessCount": + setting.TokenDailyRateLimitSuccessCount, _ = strconv.Atoi(value) + case "TokenDailyRateLimitGroup": + err = setting.UpdateTokenDailyRateLimitGroupByJSONString(value) case "RetryTimes": common.RetryTimes, _ = strconv.Atoi(value) case "DataExportInterval": diff --git a/relay/channel/gemini/relay-gemini-native.go b/relay/channel/gemini/relay-gemini-native.go index 974a22f50a74..e899d4a85040 100644 --- a/relay/channel/gemini/relay-gemini-native.go +++ b/relay/channel/gemini/relay-gemini-native.go @@ -12,8 +12,6 @@ import ( "one-api/types" "strings" - "github.com/pkg/errors" - "github.com/gin-gonic/gin" ) @@ -147,8 +145,26 @@ func GeminiTextGenerationStreamHandler(c *gin.Context, info *relaycommon.RelayIn return true }) + // 允许空回复,正常计费但补全token为0 if info.SendResponseCount == 0 { - return nil, types.NewOpenAIError(errors.New("no response received from Gemini API"), types.ErrorCodeEmptyResponse, http.StatusInternalServerError) + // 空补全,发送空响应 + stopReason := "STOP" + emptyResponse := dto.GeminiChatResponse{ + Candidates: []dto.GeminiChatCandidate{ + { + Content: dto.GeminiChatContent{ + Parts: []dto.GeminiPart{ + {Text: ""}, + }, + Role: "model", + }, + FinishReason: &stopReason, + Index: 0, + }, + }, + } + responseBody, _ := common.Marshal(emptyResponse) + service.IOCopyBytesGracefully(c, resp, responseBody) } if imageCount != 0 { @@ -157,15 +173,13 @@ func GeminiTextGenerationStreamHandler(c *gin.Context, info *relaycommon.RelayIn } } - // 如果usage.CompletionTokens为0,则使用本地统计的completion tokens + // 空补全时,补全token为0,但保留prompt token用于计费 if usage.CompletionTokens == 0 { str := responseText.String() if len(str) > 0 { usage = service.ResponseText2Usage(responseText.String(), info.UpstreamModelName, info.PromptTokens) - } else { - // 空补全,不需要使用量 - usage = &dto.Usage{} } + // 即使是空补全,也保留usage用于计费 } // 移除流式响应结尾的[Done],因为Gemini API没有发送Done的行为 diff --git a/relay/channel/gemini/relay-gemini.go b/relay/channel/gemini/relay-gemini.go index a8247217b5a7..d2097f89a373 100644 --- a/relay/channel/gemini/relay-gemini.go +++ b/relay/channel/gemini/relay-gemini.go @@ -474,7 +474,7 @@ func CovertGemini2OpenAI(c *gin.Context, textRequest dto.GeneralOpenAIRequest, i } if len(system_content) > 0 { - geminiRequest.SystemInstructions = &dto.GeminiChatContent{ + geminiRequest.SystemInstruction = &dto.GeminiChatContent{ Parts: []dto.GeminiPart{ { Text: strings.Join(system_content, "\n"), @@ -998,10 +998,29 @@ func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp * return true }) + // 允许空回复,正常计费但补全token为0 if info.SendResponseCount == 0 { - // 空补全,报错不计费 - // empty response, throw an error - return nil, types.NewOpenAIError(errors.New("no response received from Gemini API"), types.ErrorCodeEmptyResponse, http.StatusInternalServerError) + // 空补全,发送空响应 + emptyContent := "" + emptyChoice := dto.ChatCompletionsStreamResponseChoice{ + Index: 0, + Delta: dto.ChatCompletionsStreamResponseChoiceDelta{ + Role: "assistant", + Content: &emptyContent, + }, + FinishReason: &constant.FinishReasonStop, + } + emptyResponse := dto.ChatCompletionsStreamResponse{ + Id: id, + Object: "chat.completion.chunk", + Created: createAt, + Model: info.UpstreamModelName, + Choices: []dto.ChatCompletionsStreamResponseChoice{emptyChoice}, + } + err := handleStream(c, info, &emptyResponse) + if err != nil { + common.SysLog("send empty response failed: " + err.Error()) + } } if imageCount != 0 { @@ -1013,14 +1032,13 @@ func GeminiChatStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp * usage.PromptTokensDetails.TextTokens = usage.PromptTokens usage.CompletionTokens = usage.TotalTokens - usage.PromptTokens + // 空补全时,补全token为0,但保留prompt token用于计费 if usage.CompletionTokens == 0 { str := responseText.String() if len(str) > 0 { usage = service.ResponseText2Usage(responseText.String(), info.UpstreamModelName, info.PromptTokens) - } else { - // 空补全,不需要使用量 - usage = &dto.Usage{} } + // 即使是空补全,也保留usage用于计费 } response := helper.GenerateFinalUsageResponse(id, createAt, info.UpstreamModelName, *usage) @@ -1049,13 +1067,12 @@ func GeminiChatHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.R if err != nil { return nil, types.NewOpenAIError(err, types.ErrorCodeBadResponseBody, http.StatusInternalServerError) } + // 允许空回复,正常处理;但如果被阻止则返回错误 if len(geminiResponse.Candidates) == 0 { - //return nil, types.NewOpenAIError(errors.New("no candidates returned"), types.ErrorCodeBadResponseBody, http.StatusInternalServerError) if geminiResponse.PromptFeedback != nil && geminiResponse.PromptFeedback.BlockReason != nil { return nil, types.NewOpenAIError(errors.New("request blocked by Gemini API: "+*geminiResponse.PromptFeedback.BlockReason), types.ErrorCodePromptBlocked, http.StatusBadRequest) - } else { - return nil, types.NewOpenAIError(errors.New("empty response from Gemini API"), types.ErrorCodeEmptyResponse, http.StatusInternalServerError) } + // 空回复但未被阻止,继续正常处理 } fullTextResponse := responseGeminiChat2OpenAI(c, &geminiResponse) fullTextResponse.Model = info.UpstreamModelName diff --git a/relay/gemini_handler.go b/relay/gemini_handler.go index 1410da606df1..939bbfb81059 100644 --- a/relay/gemini_handler.go +++ b/relay/gemini_handler.go @@ -96,42 +96,42 @@ func GeminiHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *typ adaptor.Init(info) if info.ChannelSetting.SystemPrompt != "" { - if request.SystemInstructions == nil { - request.SystemInstructions = &dto.GeminiChatContent{ + if request.SystemInstruction == nil { + request.SystemInstruction = &dto.GeminiChatContent{ Parts: []dto.GeminiPart{ {Text: info.ChannelSetting.SystemPrompt}, }, } - } else if len(request.SystemInstructions.Parts) == 0 { - request.SystemInstructions.Parts = []dto.GeminiPart{{Text: info.ChannelSetting.SystemPrompt}} + } else if len(request.SystemInstruction.Parts) == 0 { + request.SystemInstruction.Parts = []dto.GeminiPart{{Text: info.ChannelSetting.SystemPrompt}} } else if info.ChannelSetting.SystemPromptOverride { common.SetContextKey(c, constant.ContextKeySystemPromptOverride, true) merged := false - for i := range request.SystemInstructions.Parts { - if request.SystemInstructions.Parts[i].Text == "" { + for i := range request.SystemInstruction.Parts { + if request.SystemInstruction.Parts[i].Text == "" { continue } - request.SystemInstructions.Parts[i].Text = info.ChannelSetting.SystemPrompt + "\n" + request.SystemInstructions.Parts[i].Text + request.SystemInstruction.Parts[i].Text = info.ChannelSetting.SystemPrompt + "\n" + request.SystemInstruction.Parts[i].Text merged = true break } if !merged { - request.SystemInstructions.Parts = append([]dto.GeminiPart{{Text: info.ChannelSetting.SystemPrompt}}, request.SystemInstructions.Parts...) + request.SystemInstruction.Parts = append([]dto.GeminiPart{{Text: info.ChannelSetting.SystemPrompt}}, request.SystemInstruction.Parts...) } } } // Clean up empty system instruction - if request.SystemInstructions != nil { + if request.SystemInstruction != nil { hasContent := false - for _, part := range request.SystemInstructions.Parts { + for _, part := range request.SystemInstruction.Parts { if part.Text != "" { hasContent = true break } } if !hasContent { - request.SystemInstructions = nil + request.SystemInstruction = nil } } diff --git a/service/channel.go b/service/channel.go index 5c55855b270d..aa0f4652dccb 100644 --- a/service/channel.go +++ b/service/channel.go @@ -2,110 +2,45 @@ package service import ( "fmt" - "net/http" "one-api/common" - "one-api/constant" - "one-api/dto" "one-api/model" - "one-api/setting/operation_setting" "one-api/types" "strings" ) -func formatNotifyType(channelId int, status int) string { - return fmt.Sprintf("%s_%d_%d", dto.NotifyTypeChannelUpdate, channelId, status) -} - -// disable & notify -func DisableChannel(channelError types.ChannelError, reason string) { - common.SysLog(fmt.Sprintf("通道「%s」(#%d)发生错误,准备禁用,原因:%s", channelError.ChannelName, channelError.ChannelId, reason)) - - // 检查是否启用自动禁用功能 - if !channelError.AutoBan { - common.SysLog(fmt.Sprintf("通道「%s」(#%d)未启用自动禁用功能,跳过禁用操作", channelError.ChannelName, channelError.ChannelId)) - return - } - - success := model.UpdateChannelStatus(channelError.ChannelId, channelError.UsingKey, common.ChannelStatusAutoDisabled, reason) - if success { - subject := fmt.Sprintf("通道「%s」(#%d)已被禁用", channelError.ChannelName, channelError.ChannelId) - content := fmt.Sprintf("通道「%s」(#%d)已被禁用,原因:%s", channelError.ChannelName, channelError.ChannelId, reason) - NotifyRootUser(formatNotifyType(channelError.ChannelId, common.ChannelStatusAutoDisabled), subject, content) - } -} - -func EnableChannel(channelId int, usingKey string, channelName string) { - success := model.UpdateChannelStatus(channelId, usingKey, common.ChannelStatusEnabled, "") - if success { - subject := fmt.Sprintf("通道「%s」(#%d)已被启用", channelName, channelId) - content := fmt.Sprintf("通道「%s」(#%d)已被启用", channelName, channelId) - NotifyRootUser(formatNotifyType(channelId, common.ChannelStatusEnabled), subject, content) - } -} - -func ShouldDisableChannel(channelType int, err *types.NewAPIError) bool { +func ShouldDisableChannel(channelId int, err *types.NewAPIError) bool { if !common.AutomaticDisableChannelEnabled { return false } if err == nil { return false } - if types.IsChannelError(err) { - return true - } - if types.IsSkipRetryError(err) { + errMsg := strings.ToLower(err.Error()) + if strings.Contains(errMsg, "no candidates returned") || strings.Contains(errMsg, "deadline exceeded") || strings.Contains(errMsg, "timeout") || strings.Contains(errMsg, "connect") || strings.Contains(errMsg, "do request failed") || strings.Contains(errMsg, "provider returned error") || strings.Contains(errMsg, "internal server error") || strings.Contains(errMsg, "no response received") { return false } - if err.StatusCode == http.StatusUnauthorized { + if err.StatusCode == 401 { return true } - if err.StatusCode == http.StatusForbidden { - switch channelType { - case constant.ChannelTypeGemini: - return true - } + if err.StatusCode == 429 { + // too many requests + return false } - oaiErr := err.ToOpenAIError() - switch oaiErr.Code { - case "invalid_api_key": - return true - case "account_deactivated": - return true - case "billing_not_active": - return true - case "pre_consume_token_quota_failed": - return true - case "Arrearage": + if err.StatusCode == 403 { + // forbidden return true } - switch oaiErr.Type { - case "insufficient_quota": - return true - case "insufficient_user_quota": - return true - // https://docs.anthropic.com/claude/reference/errors - case "authentication_error": - return true - case "permission_error": - return true - case "forbidden": + if err.GetErrorType() == "insufficient_quota" { return true } - - lowerMessage := strings.ToLower(err.Error()) - search, _ := AcSearch(lowerMessage, operation_setting.AutomaticDisableKeywords, true) - return search + return false } -func ShouldEnableChannel(newAPIError *types.NewAPIError, status int) bool { - if !common.AutomaticEnableChannelEnabled { - return false - } - if newAPIError != nil { - return false - } - if status != common.ChannelStatusAutoDisabled { - return false +func DisableChannel(channelError types.ChannelError, reason string) { + success := model.UpdateChannelStatus(channelError.ChannelId, channelError.UsingKey, common.ChannelStatusAutoDisabled, reason) + if success { + common.SysLog(fmt.Sprintf("channel #%d (%s) disabled, reason: %s", channelError.ChannelId, channelError.ChannelName, reason)) + } else { + common.SysLog(fmt.Sprintf("failed to disable channel #%d (%s)", channelError.ChannelId, channelError.ChannelName)) } - return true } diff --git a/service/convert.go b/service/convert.go index 1a39e537a8eb..af4dd7c4ee1a 100644 --- a/service/convert.go +++ b/service/convert.go @@ -597,11 +597,11 @@ func GeminiToOpenAIRequest(geminiRequest *dto.GeminiChatRequest, info *relaycomm } // gemini system instructions - if geminiRequest.SystemInstructions != nil { + if geminiRequest.SystemInstruction != nil { // 将系统指令作为第一条消息插入 systemMessage := dto.Message{ Role: "system", - Content: extractTextFromGeminiParts(geminiRequest.SystemInstructions.Parts), + Content: extractTextFromGeminiParts(geminiRequest.SystemInstruction.Parts), } openaiRequest.Messages = append([]dto.Message{systemMessage}, openaiRequest.Messages...) } diff --git a/setting/rate_limit.go b/setting/rate_limit.go index 141463e14533..d26288576978 100644 --- a/setting/rate_limit.go +++ b/setting/rate_limit.go @@ -8,6 +8,7 @@ import ( "sync" ) +// Per-user rate limit settings (原有的按用户限流) var ModelRequestRateLimitEnabled = false var ModelRequestRateLimitDurationMinutes = 1 var ModelRequestRateLimitCount = 0 @@ -15,6 +16,21 @@ var ModelRequestRateLimitSuccessCount = 1000 var ModelRequestRateLimitGroup = map[string][2]int{} var ModelRequestRateLimitMutex sync.RWMutex +// Per-key minute rate limit settings (按密钥的分钟级限流) +var TokenRateLimitEnabled = false +var TokenRateLimitDurationMinutes = 1 +var TokenRateLimitCount = 0 +var TokenRateLimitSuccessCount = 0 +var TokenRateLimitGroup = map[string][2]int{} +var TokenRateLimitMutex sync.RWMutex + +// Per-key daily rate limit settings (按密钥的每日限流) +var TokenDailyRateLimitEnabled = false +var TokenDailyRateLimitCount = 0 // 每日总请求数限制(0表示不限制) +var TokenDailyRateLimitSuccessCount = 0 // 每日成功请求数限制(0表示不限制) +var TokenDailyRateLimitGroup = map[string][2]int{} // 按分组的每日限制 [总请求数, 成功请求数] +var TokenDailyRateLimitMutex sync.RWMutex + func ModelRequestRateLimitGroup2JSONString() string { ModelRequestRateLimitMutex.RLock() defer ModelRequestRateLimitMutex.RUnlock() @@ -66,3 +82,109 @@ func CheckModelRequestRateLimitGroup(jsonStr string) error { return nil } + +// Token minute rate limit functions +func TokenRateLimitGroup2JSONString() string { + TokenRateLimitMutex.RLock() + defer TokenRateLimitMutex.RUnlock() + + jsonBytes, err := json.Marshal(TokenRateLimitGroup) + if err != nil { + common.SysLog("error marshalling token rate limit group: " + err.Error()) + } + return string(jsonBytes) +} + +func UpdateTokenRateLimitGroupByJSONString(jsonStr string) error { + TokenRateLimitMutex.Lock() + defer TokenRateLimitMutex.Unlock() + + TokenRateLimitGroup = make(map[string][2]int) + return json.Unmarshal([]byte(jsonStr), &TokenRateLimitGroup) +} + +func GetTokenRateLimit(group string) (totalCount, successCount int, found bool) { + TokenRateLimitMutex.RLock() + defer TokenRateLimitMutex.RUnlock() + + if TokenRateLimitGroup == nil { + return 0, 0, false + } + + limits, found := TokenRateLimitGroup[group] + if !found { + return 0, 0, false + } + return limits[0], limits[1], true +} + +func CheckTokenRateLimitGroup(jsonStr string) error { + checkTokenRateLimitGroup := make(map[string][2]int) + err := json.Unmarshal([]byte(jsonStr), &checkTokenRateLimitGroup) + if err != nil { + return err + } + for group, limits := range checkTokenRateLimitGroup { + if limits[0] < 0 || limits[1] < 0 { + return fmt.Errorf("group %s has negative rate limit values: [%d, %d]", group, limits[0], limits[1]) + } + if limits[0] > math.MaxInt32 || limits[1] > math.MaxInt32 { + return fmt.Errorf("group %s [%d, %d] has max rate limits value 2147483647", group, limits[0], limits[1]) + } + } + + return nil +} + +// Token daily rate limit functions +func TokenDailyRateLimitGroup2JSONString() string { + TokenDailyRateLimitMutex.RLock() + defer TokenDailyRateLimitMutex.RUnlock() + + jsonBytes, err := json.Marshal(TokenDailyRateLimitGroup) + if err != nil { + common.SysLog("error marshalling token daily rate limit group: " + err.Error()) + } + return string(jsonBytes) +} + +func UpdateTokenDailyRateLimitGroupByJSONString(jsonStr string) error { + TokenDailyRateLimitMutex.Lock() + defer TokenDailyRateLimitMutex.Unlock() + + TokenDailyRateLimitGroup = make(map[string][2]int) + return json.Unmarshal([]byte(jsonStr), &TokenDailyRateLimitGroup) +} + +func GetTokenDailyRateLimit(group string) (totalCount, successCount int, found bool) { + TokenDailyRateLimitMutex.RLock() + defer TokenDailyRateLimitMutex.RUnlock() + + if TokenDailyRateLimitGroup == nil { + return 0, 0, false + } + + limits, found := TokenDailyRateLimitGroup[group] + if !found { + return 0, 0, false + } + return limits[0], limits[1], true +} + +func CheckTokenDailyRateLimitGroup(jsonStr string) error { + checkTokenDailyRateLimitGroup := make(map[string][2]int) + err := json.Unmarshal([]byte(jsonStr), &checkTokenDailyRateLimitGroup) + if err != nil { + return err + } + for group, limits := range checkTokenDailyRateLimitGroup { + if limits[0] < 0 || limits[1] < 0 { + return fmt.Errorf("group %s has negative rate limit values: [%d, %d]", group, limits[0], limits[1]) + } + if limits[0] > math.MaxInt32 || limits[1] > math.MaxInt32 { + return fmt.Errorf("group %s [%d, %d] has max rate limits value 2147483647", group, limits[0], limits[1]) + } + } + + return nil +} diff --git a/web/src/components/settings/RateLimitSetting.jsx b/web/src/components/settings/RateLimitSetting.jsx index be83e0277292..6feb5b207215 100644 --- a/web/src/components/settings/RateLimitSetting.jsx +++ b/web/src/components/settings/RateLimitSetting.jsx @@ -32,6 +32,15 @@ const RateLimitSetting = () => { ModelRequestRateLimitSuccessCount: 1000, ModelRequestRateLimitDurationMinutes: 1, ModelRequestRateLimitGroup: '', + TokenRateLimitEnabled: false, + TokenRateLimitCount: 0, + TokenRateLimitSuccessCount: 0, + TokenRateLimitDurationMinutes: 1, + TokenRateLimitGroup: '', + TokenDailyRateLimitEnabled: false, + TokenDailyRateLimitCount: 0, + TokenDailyRateLimitSuccessCount: 0, + TokenDailyRateLimitGroup: '', }); let [loading, setLoading] = useState(false); @@ -42,8 +51,15 @@ const RateLimitSetting = () => { if (success) { let newInputs = {}; data.forEach((item) => { - if (item.key === 'ModelRequestRateLimitGroup') { - item.value = JSON.stringify(JSON.parse(item.value), null, 2); + if (item.key === 'ModelRequestRateLimitGroup' || + item.key === 'TokenRateLimitGroup' || + item.key === 'TokenDailyRateLimitGroup') { + try { + item.value = JSON.stringify(JSON.parse(item.value), null, 2); + } catch (e) { + // 如果解析失败,保持原值 + item.value = item.value || ''; + } } if (item.key.endsWith('Enabled')) { diff --git a/web/src/components/table/redemptions/modals/EditRedemptionModal.jsx b/web/src/components/table/redemptions/modals/EditRedemptionModal.jsx index bcde7260488f..d9d4967bdac5 100644 --- a/web/src/components/table/redemptions/modals/EditRedemptionModal.jsx +++ b/web/src/components/table/redemptions/modals/EditRedemptionModal.jsx @@ -307,12 +307,16 @@ const EditRedemptionModal = (props) => { Number(values.quota) || 0, )} data={[ - { value: 500000, label: '1$' }, { value: 5000000, label: '10$' }, + { value: 10000000, label: '20$' }, + { value: 15000000, label: '30$' }, + { value: 20000000, label: '40$' }, { value: 25000000, label: '50$' }, + { value: 30000000, label: '60$' }, + { value: 35000000, label: '70$' }, + { value: 40000000, label: '80$' }, + { value: 45000000, label: '90$' }, { value: 50000000, label: '100$' }, - { value: 250000000, label: '500$' }, - { value: 500000000, label: '1000$' }, ]} showClear /> diff --git a/web/src/components/table/tokens/modals/EditTokenModal.jsx b/web/src/components/table/tokens/modals/EditTokenModal.jsx index 0994a542fe74..87f4ac1e0570 100644 --- a/web/src/components/table/tokens/modals/EditTokenModal.jsx +++ b/web/src/components/table/tokens/modals/EditTokenModal.jsx @@ -488,12 +488,16 @@ const EditTokenModal = (props) => { : [{ required: true, message: t('请输入额度') }] } data={[ - { value: 500000, label: '1$' }, - { value: 5000000, label: '10$' }, - { value: 25000000, label: '50$' }, - { value: 50000000, label: '100$' }, - { value: 250000000, label: '500$' }, - { value: 500000000, label: '1000$' }, + { value: 5000000, label: '10$' }, + { value: 10000000, label: '20$' }, + { value: 15000000, label: '30$' }, + { value: 20000000, label: '40$' }, + { value: 25000000, label: '50$' }, + { value: 30000000, label: '60$' }, + { value: 35000000, label: '70$' }, + { value: 40000000, label: '80$' }, + { value: 45000000, label: '90$' }, + { value: 50000000, label: '100$' }, ]} /> diff --git a/web/src/pages/Setting/RateLimit/SettingsRequestRateLimit.jsx b/web/src/pages/Setting/RateLimit/SettingsRequestRateLimit.jsx index 12b9763819c4..c88eaf753385 100644 --- a/web/src/pages/Setting/RateLimit/SettingsRequestRateLimit.jsx +++ b/web/src/pages/Setting/RateLimit/SettingsRequestRateLimit.jsx @@ -39,6 +39,15 @@ export default function RequestRateLimit(props) { ModelRequestRateLimitSuccessCount: 1000, ModelRequestRateLimitDurationMinutes: 1, ModelRequestRateLimitGroup: '', + TokenRateLimitEnabled: false, + TokenRateLimitCount: 0, + TokenRateLimitSuccessCount: 0, + TokenRateLimitDurationMinutes: 1, + TokenRateLimitGroup: '', + TokenDailyRateLimitEnabled: false, + TokenDailyRateLimitCount: 0, + TokenDailyRateLimitSuccessCount: 0, + TokenDailyRateLimitGroup: '', }); const refForm = useRef(); const [inputsRow, setInputsRow] = useState(inputs); @@ -235,6 +244,220 @@ export default function RequestRateLimit(props) { + +
{t('说明:')}
+{t('说明:')}
+