From 7ff4cebdbedfb1f9710ab0edb9bc4515fe38747b Mon Sep 17 00:00:00 2001
From: "1808837298@qq.com" <1808837298@qq.com>
Date: Mon, 24 Feb 2025 14:18:15 +0800
Subject: [PATCH 1/4] feat: Enhance token counting and content parsing for
messages
---
common/constants.go | 2 +-
dto/openai_request.go | 20 ++++++++++++++++--
service/token_counter.go | 44 +++++++++++++++++++---------------------
3 files changed, 40 insertions(+), 26 deletions(-)
diff --git a/common/constants.go b/common/constants.go
index 04fb1b9a..bcab24fc 100644
--- a/common/constants.go
+++ b/common/constants.go
@@ -276,7 +276,7 @@ var ChannelBaseURLs = []string{
"https://api.cohere.ai", //34
"https://api.minimax.chat", //35
"", //36
- "", //37
+ "https://api.dify.ai", //37
"https://api.jina.ai", //38
"https://api.cloudflare.com", //39
"https://api.siliconflow.cn", //40
diff --git a/dto/openai_request.go b/dto/openai_request.go
index 028e0286..88cb6c30 100644
--- a/dto/openai_request.go
+++ b/dto/openai_request.go
@@ -1,6 +1,9 @@
package dto
-import "encoding/json"
+import (
+ "encoding/json"
+ "strings"
+)
type ResponseFormat struct {
Type string `json:"type,omitempty"`
@@ -153,11 +156,24 @@ func (m *Message) StringContent() string {
if m.parsedStringContent != nil {
return *m.parsedStringContent
}
+
var stringContent string
if err := json.Unmarshal(m.Content, &stringContent); err == nil {
+ m.parsedStringContent = &stringContent
return stringContent
}
- return string(m.Content)
+
+ contentStr := new(strings.Builder)
+ arrayContent := m.ParseContent()
+ for _, content := range arrayContent {
+ if content.Type == ContentTypeText {
+ contentStr.WriteString(content.Text)
+ }
+ }
+ stringContent = contentStr.String()
+ m.parsedStringContent = &stringContent
+
+ return stringContent
}
func (m *Message) SetStringContent(content string) {
diff --git a/service/token_counter.go b/service/token_counter.go
index 319c9b11..0a7e6de3 100644
--- a/service/token_counter.go
+++ b/service/token_counter.go
@@ -78,6 +78,9 @@ func getTokenEncoder(model string) *tiktoken.Tiktoken {
}
func getTokenNum(tokenEncoder *tiktoken.Tiktoken, text string) int {
+ if text == "" {
+ return 0
+ }
return len(tokenEncoder.Encode(text, nil, nil))
}
@@ -282,30 +285,25 @@ func CountTokenMessages(info *relaycommon.RelayInfo, messages []dto.Message, mod
tokenNum += tokensPerMessage
tokenNum += getTokenNum(tokenEncoder, message.Role)
if len(message.Content) > 0 {
- if message.IsStringContent() {
- stringContent := message.StringContent()
- tokenNum += getTokenNum(tokenEncoder, stringContent)
- if message.Name != nil {
- tokenNum += tokensPerName
- tokenNum += getTokenNum(tokenEncoder, *message.Name)
- }
- } else {
- arrayContent := message.ParseContent()
- for _, m := range arrayContent {
- if m.Type == dto.ContentTypeImageURL {
- imageUrl := m.ImageUrl.(dto.MessageImageUrl)
- imageTokenNum, err := getImageToken(info, &imageUrl, model, stream)
- if err != nil {
- return 0, err
- }
- tokenNum += imageTokenNum
- log.Printf("image token num: %d", imageTokenNum)
- } else if m.Type == dto.ContentTypeInputAudio {
- // TODO: 音频token数量计算
- tokenNum += 100
- } else {
- tokenNum += getTokenNum(tokenEncoder, m.Text)
+ if message.Name != nil {
+ tokenNum += tokensPerName
+ tokenNum += getTokenNum(tokenEncoder, *message.Name)
+ }
+ arrayContent := message.ParseContent()
+ for _, m := range arrayContent {
+ if m.Type == dto.ContentTypeImageURL {
+ imageUrl := m.ImageUrl.(dto.MessageImageUrl)
+ imageTokenNum, err := getImageToken(info, &imageUrl, model, stream)
+ if err != nil {
+ return 0, err
}
+ tokenNum += imageTokenNum
+ log.Printf("image token num: %d", imageTokenNum)
+ } else if m.Type == dto.ContentTypeInputAudio {
+ // TODO: 音频token数量计算
+ tokenNum += 100
+ } else {
+ tokenNum += getTokenNum(tokenEncoder, m.Text)
}
}
}
From b6f95dca417d48bef759bbb1c0121dda59b3fd67 Mon Sep 17 00:00:00 2001
From: "1808837298@qq.com" <1808837298@qq.com>
Date: Mon, 24 Feb 2025 14:18:30 +0800
Subject: [PATCH 2/4] feat: Add support for different Dify bot types and
request URLs
---
relay/channel/dify/adaptor.go | 30 ++++++++++++++++++++++++++++--
1 file changed, 28 insertions(+), 2 deletions(-)
diff --git a/relay/channel/dify/adaptor.go b/relay/channel/dify/adaptor.go
index ce73c78c..2626dd7d 100644
--- a/relay/channel/dify/adaptor.go
+++ b/relay/channel/dify/adaptor.go
@@ -9,9 +9,18 @@ import (
"one-api/dto"
"one-api/relay/channel"
relaycommon "one-api/relay/common"
+ "strings"
+)
+
+const (
+ BotTypeChatFlow = 1 // chatflow default
+ BotTypeAgent = 2
+ BotTypeWorkFlow = 3
+ BotTypeCompletion = 4
)
type Adaptor struct {
+ BotType int
}
func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
@@ -25,10 +34,28 @@ func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInf
}
func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
+ if strings.HasPrefix(info.UpstreamModelName, "agent") {
+ a.BotType = BotTypeAgent
+ } else if strings.HasPrefix(info.UpstreamModelName, "workflow") {
+ a.BotType = BotTypeWorkFlow
+ } else if strings.HasPrefix(info.UpstreamModelName, "chat") {
+ a.BotType = BotTypeCompletion
+ } else {
+ a.BotType = BotTypeChatFlow
+ }
}
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
- return fmt.Sprintf("%s/v1/chat-messages", info.BaseUrl), nil
+ switch a.BotType {
+ case BotTypeWorkFlow:
+ return fmt.Sprintf("%s/v1/workflows/run", info.BaseUrl), nil
+ case BotTypeCompletion:
+ return fmt.Sprintf("%s/v1/completion-messages", info.BaseUrl), nil
+ case BotTypeAgent:
+ fallthrough
+ default:
+ return fmt.Sprintf("%s/v1/chat-messages", info.BaseUrl), nil
+ }
}
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
@@ -53,7 +80,6 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela
return nil, errors.New("not implemented")
}
-
func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) {
return channel.DoApiRequest(a, c, info, requestBody)
}
From 83a37e4653d30776b707ea604eb8972688c7ffeb Mon Sep 17 00:00:00 2001
From: "1808837298@qq.com" <1808837298@qq.com>
Date: Mon, 24 Feb 2025 16:20:55 +0800
Subject: [PATCH 3/4] feat: Add model request rate limiting functionality
---
middleware/model-rate-limit.go | 172 ++++++++++++++++++
model/option.go | 13 ++
router/relay-router.go | 1 +
setting/rate_limit.go | 6 +
web/src/components/RateLimitSetting.js | 80 ++++++++
.../RateLimit/SettingsRequestRateLimit.js | 159 ++++++++++++++++
web/src/pages/Setting/index.js | 6 +
7 files changed, 437 insertions(+)
create mode 100644 middleware/model-rate-limit.go
create mode 100644 setting/rate_limit.go
create mode 100644 web/src/components/RateLimitSetting.js
create mode 100644 web/src/pages/Setting/RateLimit/SettingsRequestRateLimit.js
diff --git a/middleware/model-rate-limit.go b/middleware/model-rate-limit.go
new file mode 100644
index 00000000..135e0005
--- /dev/null
+++ b/middleware/model-rate-limit.go
@@ -0,0 +1,172 @@
+package middleware
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "one-api/common"
+ "one-api/setting"
+ "strconv"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ "github.com/go-redis/redis/v8"
+)
+
+const (
+ ModelRequestRateLimitCountMark = "MRRL"
+ ModelRequestRateLimitSuccessCountMark = "MRRLS"
+)
+
+// 检查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(timeFormat, oldTimeStr)
+ if err != nil {
+ return false, err
+ }
+
+ nowTimeStr := time.Now().Format(timeFormat)
+ nowTime, err := time.Parse(timeFormat, nowTimeStr)
+ if err != nil {
+ return false, err
+ }
+ // 如果在时间窗口内已达到限制,拒绝请求
+ subTime := nowTime.Sub(oldTime).Seconds()
+ if int64(subTime) < duration {
+ rdb.Expire(ctx, key, common.RateLimitKeyExpirationDuration)
+ 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().Format(timeFormat)
+ rdb.LPush(ctx, key, now)
+ rdb.LTrim(ctx, key, 0, int64(maxCount-1))
+ rdb.Expire(ctx, key, common.RateLimitKeyExpirationDuration)
+}
+
+// Redis限流处理器
+func redisRateLimitHandler(duration int64, totalMaxCount, successMaxCount int) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ userId := strconv.Itoa(c.GetInt("id"))
+ ctx := context.Background()
+ rdb := common.RDB
+
+ // 1. 检查总请求数限制(当totalMaxCount为0时会自动跳过)
+ totalKey := fmt.Sprintf("rateLimit:%s:%s", ModelRequestRateLimitCountMark, userId)
+ allowed, err := checkRedisRateLimit(ctx, rdb, totalKey, totalMaxCount, duration)
+ if err != nil {
+ fmt.Println("检查总请求数限制失败:", err.Error())
+ abortWithOpenAiMessage(c, http.StatusInternalServerError, "rate_limit_check_failed")
+ return
+ }
+ if !allowed {
+ abortWithOpenAiMessage(c, http.StatusTooManyRequests, fmt.Sprintf("您已达到总请求数限制:%d分钟内最多请求%d次,包括失败次数,请检查您的请求是否正确", setting.ModelRequestRateLimitDurationMinutes, totalMaxCount))
+ }
+
+ // 2. 检查成功请求数限制
+ successKey := fmt.Sprintf("rateLimit:%s:%s", ModelRequestRateLimitSuccessCountMark, userId)
+ allowed, err = checkRedisRateLimit(ctx, rdb, successKey, successMaxCount, duration)
+ if err != nil {
+ fmt.Println("检查成功请求数限制失败:", err.Error())
+ abortWithOpenAiMessage(c, http.StatusInternalServerError, "rate_limit_check_failed")
+ return
+ }
+ if !allowed {
+ abortWithOpenAiMessage(c, http.StatusTooManyRequests, fmt.Sprintf("您已达到请求数限制:%d分钟内最多请求%d次", setting.ModelRequestRateLimitDurationMinutes, successMaxCount))
+ return
+ }
+
+ // 3. 记录总请求(当totalMaxCount为0时会自动跳过)
+ recordRedisRequest(ctx, rdb, totalKey, totalMaxCount)
+
+ // 4. 处理请求
+ c.Next()
+
+ // 5. 如果请求成功,记录成功请求
+ if c.Writer.Status() < 400 {
+ recordRedisRequest(ctx, rdb, successKey, successMaxCount)
+ }
+ }
+}
+
+// 内存限流处理器
+func memoryRateLimitHandler(duration int64, totalMaxCount, successMaxCount int) gin.HandlerFunc {
+ inMemoryRateLimiter.Init(common.RateLimitKeyExpirationDuration)
+
+ return func(c *gin.Context) {
+ userId := strconv.Itoa(c.GetInt("id"))
+ totalKey := ModelRequestRateLimitCountMark + userId
+ successKey := ModelRequestRateLimitSuccessCountMark + userId
+
+ // 1. 检查总请求数限制(当totalMaxCount为0时跳过)
+ if totalMaxCount > 0 && !inMemoryRateLimiter.Request(totalKey, totalMaxCount, duration) {
+ c.Status(http.StatusTooManyRequests)
+ c.Abort()
+ return
+ }
+
+ // 2. 检查成功请求数限制
+ // 使用一个临时key来检查限制,这样可以避免实际记录
+ checkKey := successKey + "_check"
+ if !inMemoryRateLimiter.Request(checkKey, successMaxCount, duration) {
+ c.Status(http.StatusTooManyRequests)
+ c.Abort()
+ return
+ }
+
+ // 3. 处理请求
+ c.Next()
+
+ // 4. 如果请求成功,记录到实际的成功请求计数中
+ if c.Writer.Status() < 400 {
+ inMemoryRateLimiter.Request(successKey, successMaxCount, duration)
+ }
+ }
+}
+
+// ModelRequestRateLimit 模型请求限流中间件
+func ModelRequestRateLimit() func(c *gin.Context) {
+ // 如果未启用限流,直接放行
+ if !setting.ModelRequestRateLimitEnabled {
+ return defNext
+ }
+
+ // 计算限流参数
+ duration := int64(setting.ModelRequestRateLimitDurationMinutes * 60)
+ totalMaxCount := setting.ModelRequestRateLimitCount
+ successMaxCount := setting.ModelRequestRateLimitSuccessCount
+
+ // 根据存储类型选择限流处理器
+ if common.RedisEnabled {
+ return redisRateLimitHandler(duration, totalMaxCount, successMaxCount)
+ } else {
+ return memoryRateLimitHandler(duration, totalMaxCount, successMaxCount)
+ }
+}
diff --git a/model/option.go b/model/option.go
index 24935c69..3e9e9541 100644
--- a/model/option.go
+++ b/model/option.go
@@ -85,6 +85,9 @@ func InitOptionMap() {
common.OptionMap["QuotaForInvitee"] = strconv.Itoa(common.QuotaForInvitee)
common.OptionMap["QuotaRemindThreshold"] = strconv.Itoa(common.QuotaRemindThreshold)
common.OptionMap["ShouldPreConsumedQuota"] = strconv.Itoa(common.PreConsumedQuota)
+ common.OptionMap["ModelRequestRateLimitCount"] = strconv.Itoa(setting.ModelRequestRateLimitCount)
+ common.OptionMap["ModelRequestRateLimitDurationMinutes"] = strconv.Itoa(setting.ModelRequestRateLimitDurationMinutes)
+ common.OptionMap["ModelRequestRateLimitSuccessCount"] = strconv.Itoa(setting.ModelRequestRateLimitSuccessCount)
common.OptionMap["ModelRatio"] = common.ModelRatio2JSONString()
common.OptionMap["ModelPrice"] = common.ModelPrice2JSONString()
common.OptionMap["GroupRatio"] = setting.GroupRatio2JSONString()
@@ -105,6 +108,7 @@ func InitOptionMap() {
common.OptionMap["MjActionCheckSuccessEnabled"] = strconv.FormatBool(setting.MjActionCheckSuccessEnabled)
common.OptionMap["CheckSensitiveEnabled"] = strconv.FormatBool(setting.CheckSensitiveEnabled)
common.OptionMap["DemoSiteEnabled"] = strconv.FormatBool(setting.DemoSiteEnabled)
+ common.OptionMap["ModelRequestRateLimitEnabled"] = strconv.FormatBool(setting.ModelRequestRateLimitEnabled)
common.OptionMap["CheckSensitiveOnPromptEnabled"] = strconv.FormatBool(setting.CheckSensitiveOnPromptEnabled)
//common.OptionMap["CheckSensitiveOnCompletionEnabled"] = strconv.FormatBool(constant.CheckSensitiveOnCompletionEnabled)
common.OptionMap["StopOnSensitiveEnabled"] = strconv.FormatBool(setting.StopOnSensitiveEnabled)
@@ -226,6 +230,9 @@ func updateOptionMap(key string, value string) (err error) {
setting.DemoSiteEnabled = boolValue
case "CheckSensitiveOnPromptEnabled":
setting.CheckSensitiveOnPromptEnabled = boolValue
+ case "ModelRequestRateLimitEnabled":
+ setting.ModelRequestRateLimitEnabled = boolValue
+
//case "CheckSensitiveOnCompletionEnabled":
// constant.CheckSensitiveOnCompletionEnabled = boolValue
case "StopOnSensitiveEnabled":
@@ -308,6 +315,12 @@ func updateOptionMap(key string, value string) (err error) {
common.QuotaRemindThreshold, _ = strconv.Atoi(value)
case "ShouldPreConsumedQuota":
common.PreConsumedQuota, _ = strconv.Atoi(value)
+ case "ModelRequestRateLimitCount":
+ setting.ModelRequestRateLimitCount, _ = strconv.Atoi(value)
+ case "ModelRequestRateLimitDurationMinutes":
+ setting.ModelRequestRateLimitDurationMinutes, _ = strconv.Atoi(value)
+ case "ModelRequestRateLimitSuccessCount":
+ setting.ModelRequestRateLimitSuccessCount, _ = strconv.Atoi(value)
case "RetryTimes":
common.RetryTimes, _ = strconv.Atoi(value)
case "DataExportInterval":
diff --git a/router/relay-router.go b/router/relay-router.go
index 63f5c36d..32e0c682 100644
--- a/router/relay-router.go
+++ b/router/relay-router.go
@@ -24,6 +24,7 @@ func SetRelayRouter(router *gin.Engine) {
}
relayV1Router := router.Group("/v1")
relayV1Router.Use(middleware.TokenAuth())
+ relayV1Router.Use(middleware.ModelRequestRateLimit())
{
// WebSocket 路由
wsRouter := relayV1Router.Group("")
diff --git a/setting/rate_limit.go b/setting/rate_limit.go
new file mode 100644
index 00000000..4b216948
--- /dev/null
+++ b/setting/rate_limit.go
@@ -0,0 +1,6 @@
+package setting
+
+var ModelRequestRateLimitEnabled = false
+var ModelRequestRateLimitDurationMinutes = 1
+var ModelRequestRateLimitCount = 0
+var ModelRequestRateLimitSuccessCount = 1000
diff --git a/web/src/components/RateLimitSetting.js b/web/src/components/RateLimitSetting.js
new file mode 100644
index 00000000..b6c92917
--- /dev/null
+++ b/web/src/components/RateLimitSetting.js
@@ -0,0 +1,80 @@
+import React, { useEffect, useState } from 'react';
+import { Card, Spin, Tabs } from '@douyinfe/semi-ui';
+import SettingsGeneral from '../pages/Setting/Operation/SettingsGeneral.js';
+import SettingsDrawing from '../pages/Setting/Operation/SettingsDrawing.js';
+import SettingsSensitiveWords from '../pages/Setting/Operation/SettingsSensitiveWords.js';
+import SettingsLog from '../pages/Setting/Operation/SettingsLog.js';
+import SettingsDataDashboard from '../pages/Setting/Operation/SettingsDataDashboard.js';
+import SettingsMonitoring from '../pages/Setting/Operation/SettingsMonitoring.js';
+import SettingsCreditLimit from '../pages/Setting/Operation/SettingsCreditLimit.js';
+import SettingsMagnification from '../pages/Setting/Operation/SettingsMagnification.js';
+import ModelSettingsVisualEditor from '../pages/Setting/Operation/ModelSettingsVisualEditor.js';
+import GroupRatioSettings from '../pages/Setting/Operation/GroupRatioSettings.js';
+import ModelRatioSettings from '../pages/Setting/Operation/ModelRatioSettings.js';
+
+
+import { API, showError, showSuccess } from '../helpers';
+import SettingsChats from '../pages/Setting/Operation/SettingsChats.js';
+import { useTranslation } from 'react-i18next';
+import RequestRateLimit from '../pages/Setting/RateLimit/SettingsRequestRateLimit.js';
+
+const RateLimitSetting = () => {
+ const { t } = useTranslation();
+ let [inputs, setInputs] = useState({
+ ModelRequestRateLimitEnabled: false,
+ ModelRequestRateLimitCount: 0,
+ ModelRequestRateLimitSuccessCount: 1000,
+ ModelRequestRateLimitDurationMinutes: 1,
+ });
+
+ let [loading, setLoading] = useState(false);
+
+ const getOptions = async () => {
+ const res = await API.get('/api/option/');
+ const { success, message, data } = res.data;
+ if (success) {
+ let newInputs = {};
+ data.forEach((item) => {
+ if (
+ item.key.endsWith('Enabled')
+ ) {
+ newInputs[item.key] = item.value === 'true' ? true : false;
+ } else {
+ newInputs[item.key] = item.value;
+ }
+ });
+
+ setInputs(newInputs);
+ } else {
+ showError(message);
+ }
+ };
+ async function onRefresh() {
+ try {
+ setLoading(true);
+ await getOptions();
+ // showSuccess('刷新成功');
+ } catch (error) {
+ showError('刷新失败');
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ useEffect(() => {
+ onRefresh();
+ }, []);
+
+ return (
+ <>
+
+ {/* AI请求速率限制 */}
+
+
+
+
+ >
+ );
+};
+
+export default RateLimitSetting;
diff --git a/web/src/pages/Setting/RateLimit/SettingsRequestRateLimit.js b/web/src/pages/Setting/RateLimit/SettingsRequestRateLimit.js
new file mode 100644
index 00000000..6f4a5571
--- /dev/null
+++ b/web/src/pages/Setting/RateLimit/SettingsRequestRateLimit.js
@@ -0,0 +1,159 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
+import {
+ compareObjects,
+ API,
+ showError,
+ showSuccess,
+ showWarning,
+} from '../../../helpers';
+import { useTranslation } from 'react-i18next';
+
+export default function RequestRateLimit(props) {
+ const { t } = useTranslation();
+
+ const [loading, setLoading] = useState(false);
+ const [inputs, setInputs] = useState({
+ ModelRequestRateLimitEnabled: false,
+ ModelRequestRateLimitCount: -1,
+ ModelRequestRateLimitSuccessCount: 1000,
+ ModelRequestRateLimitDurationMinutes: 1
+ });
+ const refForm = useRef();
+ const [inputsRow, setInputsRow] = useState(inputs);
+
+ function onSubmit() {
+ const updateArray = compareObjects(inputs, inputsRow);
+ if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
+ const requestQueue = updateArray.map((item) => {
+ let value = '';
+ if (typeof inputs[item.key] === 'boolean') {
+ value = String(inputs[item.key]);
+ } else {
+ value = inputs[item.key];
+ }
+ return API.put('/api/option/', {
+ key: item.key,
+ value,
+ });
+ });
+ setLoading(true);
+ Promise.all(requestQueue)
+ .then((res) => {
+ if (requestQueue.length === 1) {
+ if (res.includes(undefined)) return;
+ } else if (requestQueue.length > 1) {
+ if (res.includes(undefined)) return showError(t('部分保存失败,请重试'));
+ }
+ showSuccess(t('保存成功'));
+ props.refresh();
+ })
+ .catch(() => {
+ showError(t('保存失败,请重试'));
+ })
+ .finally(() => {
+ setLoading(false);
+ });
+ }
+
+ useEffect(() => {
+ const currentInputs = {};
+ for (let key in props.options) {
+ if (Object.keys(inputs).includes(key)) {
+ currentInputs[key] = props.options[key];
+ }
+ }
+ setInputs(currentInputs);
+ setInputsRow(structuredClone(currentInputs));
+ refForm.current.setValues(currentInputs);
+ }, [props.options]);
+
+ return (
+ <>
+
+
+
+
+ {
+ setInputs({
+ ...inputs,
+ ModelRequestRateLimitEnabled: value,
+ });
+ }}
+ />
+
+
+
+
+
+ setInputs({
+ ...inputs,
+ ModelRequestRateLimitDurationMinutes: String(value),
+ })
+ }
+ />
+
+
+
+
+
+ setInputs({
+ ...inputs,
+ ModelRequestRateLimitCount: String(value),
+ })
+ }
+ />
+
+
+
+ setInputs({
+ ...inputs,
+ ModelRequestRateLimitSuccessCount: String(value),
+ })
+ }
+ />
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/web/src/pages/Setting/index.js b/web/src/pages/Setting/index.js
index 385fbfeb..b5c5e268 100644
--- a/web/src/pages/Setting/index.js
+++ b/web/src/pages/Setting/index.js
@@ -8,6 +8,7 @@ import { isRoot } from '../../helpers';
import OtherSetting from '../../components/OtherSetting';
import PersonalSetting from '../../components/PersonalSetting';
import OperationSetting from '../../components/OperationSetting';
+import RateLimitSetting from '../../components/RateLimitSetting.js';
const Setting = () => {
const { t } = useTranslation();
@@ -28,6 +29,11 @@ const Setting = () => {
content: ,
itemKey: 'operation',
});
+ panes.push({
+ tab: t('速率限制设置'),
+ content: ,
+ itemKey: 'ratelimit',
+ });
panes.push({
tab: t('系统设置'),
content: ,
From e9ba392af8cf07befddac0d27d9bb41bb58f3376 Mon Sep 17 00:00:00 2001
From: "1808837298@qq.com" <1808837298@qq.com>
Date: Mon, 24 Feb 2025 16:27:20 +0800
Subject: [PATCH 4/4] feat: Add model rate limit settings in system
configuration
---
README.en.md | 1 +
README.md | 3 ++-
web/src/i18n/locales/en.json | 16 +++++++++++++---
3 files changed, 16 insertions(+), 4 deletions(-)
diff --git a/README.en.md b/README.en.md
index 2c1172ea..51cf38bb 100644
--- a/README.en.md
+++ b/README.en.md
@@ -64,6 +64,7 @@
- Add suffix `-medium` to set medium reasoning effort
- Add suffix `-low` to set low reasoning effort
17. 🔄 Thinking to content option `thinking_to_content` in `Channel->Edit->Channel Extra Settings`, default is `false`, when `true`, the `reasoning_content` of the thinking content will be converted to `` tags and concatenated to the content returned.
+18. 🔄 Model rate limit, support setting total request limit and successful request limit in `System Settings->Rate Limit Settings`
## Model Support
This version additionally supports:
diff --git a/README.md b/README.md
index 7db01f6a..c19f5f0a 100644
--- a/README.md
+++ b/README.md
@@ -69,7 +69,8 @@
- 添加后缀 `-high` 设置为 high reasoning effort (例如: `o3-mini-high`)
- 添加后缀 `-medium` 设置为 medium reasoning effort (例如: `o3-mini-medium`)
- 添加后缀 `-low` 设置为 low reasoning effort (例如: `o3-mini-low`)
- 18. 🔄 思考转内容,支持在 `渠道-编辑-渠道额外设置` 中设置 `thinking_to_content` 选项,默认`false`,开启后会将思考内容`reasoning_content`转换为``标签拼接到内容中返回。
+18. 🔄 思考转内容,支持在 `渠道-编辑-渠道额外设置` 中设置 `thinking_to_content` 选项,默认`false`,开启后会将思考内容`reasoning_content`转换为``标签拼接到内容中返回。
+19. 🔄 模型限流,支持在 `系统设置-速率限制设置` 中设置模型限流,支持设置总请求数限制和成功请求数限制
## 模型支持
此版本额外支持以下模型:
diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json
index 36e21f05..7036fa20 100644
--- a/web/src/i18n/locales/en.json
+++ b/web/src/i18n/locales/en.json
@@ -856,7 +856,7 @@
"IP黑名单": "IP blacklist",
"不允许的IP,一行一个": "IPs not allowed, one per line",
"请选择该渠道所支持的模型": "Please select the model supported by this channel",
- "次": "Second-rate",
+ "次": "times",
"达到限速报错内容": "Error content when the speed limit is reached",
"不填则使用默认报错": "If not filled in, the default error will be reported.",
"Midjouney 设置 (可选)": "Midjouney settings (optional)",
@@ -1271,5 +1271,15 @@
"留空则使用账号绑定的邮箱": "If left blank, the email address bound to the account will be used",
"代理站地址": "Base URL",
"对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "For official channels, the new-api has a built-in address. Unless it is a third-party proxy site or a special Azure access address, there is no need to fill it in",
- "渠道额外设置": "Channel extra settings"
-}
\ No newline at end of file
+ "渠道额外设置": "Channel extra settings",
+ "模型请求速率限制": "Model request rate limit",
+ "启用用户模型请求速率限制(可能会影响高并发性能)": "Enable user model request rate limit (may affect high concurrency performance)",
+ "限制周期": "Limit period",
+ "用户每周期最多请求次数": "User max request times per period",
+ "用户每周期最多请求完成次数": "User max successful request times per period",
+ "包括失败请求的次数,0代表不限制": "Including failed request times, 0 means no limit",
+ "频率限制的周期(分钟)": "Rate limit period (minutes)",
+ "只包括请求成功的次数": "Only include successful request times",
+ "保存模型速率限制": "Save model rate limit settings",
+ "速率限制设置": "Rate limit settings"
+}