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 ( + <> + +
(refForm.current = formAPI)} + style={{ marginBottom: 15 }} + > + + + + { + 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" +}