mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-08 13:17:27 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58dc7ad770 | ||
|
|
7b176015b8 | ||
|
|
cc2d9f539d | ||
|
|
7f86bdf548 | ||
|
|
0d929800cf | ||
|
|
9ebfcaf6aa | ||
|
|
40efa73a42 | ||
|
|
4a59b3ccd6 | ||
|
|
ec61534256 | ||
|
|
2a218c1c89 | ||
|
|
993cd6b624 | ||
|
|
3d4bd76083 | ||
|
|
7192437863 | ||
|
|
4bbcb00d13 | ||
|
|
9de24668d8 | ||
|
|
7aa54a2cd7 | ||
|
|
a836e97315 | ||
|
|
3373f5e0a0 | ||
|
|
d6e601b424 | ||
|
|
8c3a559690 | ||
|
|
c008d391df | ||
|
|
7c29844e4a | ||
|
|
02acc52fdb | ||
|
|
3d243c3ee2 | ||
|
|
87188cd7d4 | ||
|
|
bbab729619 | ||
|
|
0be3678c9c | ||
|
|
1cb4d750e4 | ||
|
|
88ed83f419 | ||
|
|
1513ed7847 | ||
|
|
1e1d24d1b0 | ||
|
|
7e7d6112ca | ||
|
|
6c3fb7777e |
@@ -107,7 +107,7 @@ For detailed configuration instructions, please refer to [Installation Guide-Env
|
||||
- `GEMINI_VISION_MAX_IMAGE_NUM`: Maximum number of images for Gemini models, default is `16`
|
||||
- `MAX_FILE_DOWNLOAD_MB`: Maximum file download size in MB, default is `20`
|
||||
- `CRYPTO_SECRET`: Encryption key used for encrypting database content
|
||||
- `AZURE_DEFAULT_API_VERSION`: Azure channel default API version, default is `2024-12-01-preview`
|
||||
- `AZURE_DEFAULT_API_VERSION`: Azure channel default API version, default is `2025-04-01-preview`
|
||||
- `NOTIFICATION_LIMIT_DURATION_MINUTE`: Notification limit duration, default is `10` minutes
|
||||
- `NOTIFY_LIMIT_COUNT`: Maximum number of user notifications within the specified duration, default is `2`
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ New API提供了丰富的功能,详细特性请参考[特性说明](https://do
|
||||
- `GEMINI_VISION_MAX_IMAGE_NUM`:Gemini模型最大图片数量,默认 `16`
|
||||
- `MAX_FILE_DOWNLOAD_MB`: 最大文件下载大小,单位MB,默认 `20`
|
||||
- `CRYPTO_SECRET`:加密密钥,用于加密数据库内容
|
||||
- `AZURE_DEFAULT_API_VERSION`:Azure渠道默认API版本,默认 `2024-12-01-preview`
|
||||
- `AZURE_DEFAULT_API_VERSION`:Azure渠道默认API版本,默认 `2025-04-01-preview`
|
||||
- `NOTIFICATION_LIMIT_DURATION_MINUTE`:通知限制持续时间,默认 `10`分钟
|
||||
- `NOTIFY_LIMIT_COUNT`:用户通知在指定持续时间内的最大数量,默认 `2`
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ func InitEnv() {
|
||||
GetMediaToken = common.GetEnvOrDefaultBool("GET_MEDIA_TOKEN", true)
|
||||
GetMediaTokenNotStream = common.GetEnvOrDefaultBool("GET_MEDIA_TOKEN_NOT_STREAM", true)
|
||||
UpdateTask = common.GetEnvOrDefaultBool("UPDATE_TASK", true)
|
||||
AzureDefaultAPIVersion = common.GetEnvOrDefaultString("AZURE_DEFAULT_API_VERSION", "2024-12-01-preview")
|
||||
AzureDefaultAPIVersion = common.GetEnvOrDefaultString("AZURE_DEFAULT_API_VERSION", "2025-04-01-preview")
|
||||
GeminiVisionMaxImageNum = common.GetEnvOrDefault("GEMINI_VISION_MAX_IMAGE_NUM", 16)
|
||||
NotifyLimitCount = common.GetEnvOrDefault("NOTIFY_LIMIT_COUNT", 2)
|
||||
NotificationLimitDurationMinute = common.GetEnvOrDefault("NOTIFICATION_LIMIT_DURATION_MINUTE", 10)
|
||||
|
||||
@@ -108,6 +108,13 @@ type DeepSeekUsageResponse struct {
|
||||
} `json:"balance_infos"`
|
||||
}
|
||||
|
||||
type OpenRouterCreditResponse struct {
|
||||
Data struct {
|
||||
TotalCredits float64 `json:"total_credits"`
|
||||
TotalUsage float64 `json:"total_usage"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// GetAuthHeader get auth header
|
||||
func GetAuthHeader(token string) http.Header {
|
||||
h := http.Header{}
|
||||
@@ -281,6 +288,22 @@ func updateChannelAIGC2DBalance(channel *model.Channel) (float64, error) {
|
||||
return response.TotalAvailable, nil
|
||||
}
|
||||
|
||||
func updateChannelOpenRouterBalance(channel *model.Channel) (float64, error) {
|
||||
url := "https://openrouter.ai/api/v1/credits"
|
||||
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
response := OpenRouterCreditResponse{}
|
||||
err = json.Unmarshal(body, &response)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
balance := response.Data.TotalCredits - response.Data.TotalUsage
|
||||
channel.UpdateBalance(balance)
|
||||
return balance, nil
|
||||
}
|
||||
|
||||
func updateChannelBalance(channel *model.Channel) (float64, error) {
|
||||
baseURL := common.ChannelBaseURLs[channel.Type]
|
||||
if channel.GetBaseURL() == "" {
|
||||
@@ -307,6 +330,8 @@ func updateChannelBalance(channel *model.Channel) (float64, error) {
|
||||
return updateChannelSiliconFlowBalance(channel)
|
||||
case common.ChannelTypeDeepSeek:
|
||||
return updateChannelDeepSeekBalance(channel)
|
||||
case common.ChannelTypeOpenRouter:
|
||||
return updateChannelOpenRouterBalance(channel)
|
||||
default:
|
||||
return 0, errors.New("尚未实现")
|
||||
}
|
||||
|
||||
@@ -110,6 +110,15 @@ func UpdateOption(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
case "ModelRequestRateLimitGroup":
|
||||
err = setting.CheckModelRequestRateLimitGroup(option.Value)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
err = model.UpdateOption(option.Key, option.Value)
|
||||
|
||||
21
dto/dalle.go
21
dto/dalle.go
@@ -1,17 +1,16 @@
|
||||
package dto
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type ImageRequest struct {
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt" binding:"required"`
|
||||
N int `json:"n,omitempty"`
|
||||
Size string `json:"size,omitempty"`
|
||||
Quality string `json:"quality,omitempty"`
|
||||
ResponseFormat string `json:"response_format,omitempty"`
|
||||
Style string `json:"style,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
ExtraFields json.RawMessage `json:"extra_fields,omitempty"`
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt" binding:"required"`
|
||||
N int `json:"n,omitempty"`
|
||||
Size string `json:"size,omitempty"`
|
||||
Quality string `json:"quality,omitempty"`
|
||||
ResponseFormat string `json:"response_format,omitempty"`
|
||||
Style string `json:"style,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
Moderation string `json:"moderation,omitempty"`
|
||||
Background string `json:"background,omitempty"`
|
||||
}
|
||||
|
||||
type ImageResponse struct {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/common/limiter"
|
||||
"one-api/constant"
|
||||
"one-api/setting"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -175,6 +176,19 @@ func ModelRequestRateLimit() func(c *gin.Context) {
|
||||
totalMaxCount := setting.ModelRequestRateLimitCount
|
||||
successMaxCount := setting.ModelRequestRateLimitSuccessCount
|
||||
|
||||
// 获取分组
|
||||
group := c.GetString("token_group")
|
||||
if group == "" {
|
||||
group = c.GetString(constant.ContextKeyUserGroup)
|
||||
}
|
||||
|
||||
//获取分组的限流配置
|
||||
groupTotalCount, groupSuccessCount, found := setting.GetGroupRateLimit(group)
|
||||
if found {
|
||||
totalMaxCount = groupTotalCount
|
||||
successMaxCount = groupSuccessCount
|
||||
}
|
||||
|
||||
// 根据存储类型选择并执行限流处理器
|
||||
if common.RedisEnabled {
|
||||
redisRateLimitHandler(duration, totalMaxCount, successMaxCount)(c)
|
||||
|
||||
@@ -67,6 +67,7 @@ func InitOptionMap() {
|
||||
common.OptionMap["ServerAddress"] = ""
|
||||
common.OptionMap["WorkerUrl"] = setting.WorkerUrl
|
||||
common.OptionMap["WorkerValidKey"] = setting.WorkerValidKey
|
||||
common.OptionMap["WorkerAllowHttpImageRequestEnabled"] = strconv.FormatBool(setting.WorkerAllowHttpImageRequestEnabled)
|
||||
common.OptionMap["PayAddress"] = ""
|
||||
common.OptionMap["CustomCallbackAddress"] = ""
|
||||
common.OptionMap["EpayId"] = ""
|
||||
@@ -92,6 +93,7 @@ func InitOptionMap() {
|
||||
common.OptionMap["ModelRequestRateLimitCount"] = strconv.Itoa(setting.ModelRequestRateLimitCount)
|
||||
common.OptionMap["ModelRequestRateLimitDurationMinutes"] = strconv.Itoa(setting.ModelRequestRateLimitDurationMinutes)
|
||||
common.OptionMap["ModelRequestRateLimitSuccessCount"] = strconv.Itoa(setting.ModelRequestRateLimitSuccessCount)
|
||||
common.OptionMap["ModelRequestRateLimitGroup"] = setting.ModelRequestRateLimitGroup2JSONString()
|
||||
common.OptionMap["ModelRatio"] = operation_setting.ModelRatio2JSONString()
|
||||
common.OptionMap["ModelPrice"] = operation_setting.ModelPrice2JSONString()
|
||||
common.OptionMap["CacheRatio"] = operation_setting.CacheRatio2JSONString()
|
||||
@@ -256,6 +258,8 @@ func updateOptionMap(key string, value string) (err error) {
|
||||
setting.StopOnSensitiveEnabled = boolValue
|
||||
case "SMTPSSLEnabled":
|
||||
common.SMTPSSLEnabled = boolValue
|
||||
case "WorkerAllowHttpImageRequestEnabled":
|
||||
setting.WorkerAllowHttpImageRequestEnabled = boolValue
|
||||
}
|
||||
}
|
||||
switch key {
|
||||
@@ -338,6 +342,8 @@ func updateOptionMap(key string, value string) (err error) {
|
||||
setting.ModelRequestRateLimitDurationMinutes, _ = strconv.Atoi(value)
|
||||
case "ModelRequestRateLimitSuccessCount":
|
||||
setting.ModelRequestRateLimitSuccessCount, _ = strconv.Atoi(value)
|
||||
case "ModelRequestRateLimitGroup":
|
||||
err = setting.UpdateModelRequestRateLimitGroupByJSONString(value)
|
||||
case "RetryTimes":
|
||||
common.RetryTimes, _ = strconv.Atoi(value)
|
||||
case "DataExportInterval":
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
package channel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"io"
|
||||
"net/http"
|
||||
common2 "one-api/common"
|
||||
"one-api/relay/common"
|
||||
"one-api/relay/constant"
|
||||
"one-api/relay/helper"
|
||||
"one-api/service"
|
||||
"one-api/setting/operation_setting"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/gopkg/util/gopool"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
func SetupApiRequestHeader(info *common.RelayInfo, c *gin.Context, req *http.Header) {
|
||||
@@ -55,6 +62,9 @@ func DoFormRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBod
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get request url failed: %w", err)
|
||||
}
|
||||
if common2.DebugEnabled {
|
||||
println("fullRequestURL:", fullRequestURL)
|
||||
}
|
||||
req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("new request failed: %w", err)
|
||||
@@ -105,7 +115,62 @@ func doRequest(c *gin.Context, req *http.Request, info *common.RelayInfo) (*http
|
||||
} else {
|
||||
client = service.GetHttpClient()
|
||||
}
|
||||
// 流式请求 ping 保活
|
||||
var stopPinger func()
|
||||
generalSettings := operation_setting.GetGeneralSetting()
|
||||
pingEnabled := generalSettings.PingIntervalEnabled
|
||||
var pingerWg sync.WaitGroup
|
||||
if info.IsStream {
|
||||
helper.SetEventStreamHeaders(c)
|
||||
pingInterval := time.Duration(generalSettings.PingIntervalSeconds) * time.Second
|
||||
var pingerCtx context.Context
|
||||
pingerCtx, stopPinger = context.WithCancel(c.Request.Context())
|
||||
|
||||
if pingEnabled {
|
||||
pingerWg.Add(1)
|
||||
gopool.Go(func() {
|
||||
defer pingerWg.Done()
|
||||
if pingInterval <= 0 {
|
||||
pingInterval = helper.DefaultPingInterval
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(pingInterval)
|
||||
defer ticker.Stop()
|
||||
var pingMutex sync.Mutex
|
||||
if common2.DebugEnabled {
|
||||
println("SSE ping goroutine started")
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
pingMutex.Lock()
|
||||
err2 := helper.PingData(c)
|
||||
pingMutex.Unlock()
|
||||
if err2 != nil {
|
||||
common2.LogError(c, "SSE ping error: "+err.Error())
|
||||
return
|
||||
}
|
||||
if common2.DebugEnabled {
|
||||
println("SSE ping data sent.")
|
||||
}
|
||||
case <-pingerCtx.Done():
|
||||
if common2.DebugEnabled {
|
||||
println("SSE ping goroutine stopped.")
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
// request结束后停止ping
|
||||
if info.IsStream && pingEnabled {
|
||||
stopPinger()
|
||||
pingerWg.Wait()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -67,9 +67,6 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
if info.RelayFormat == relaycommon.RelayFormatClaude {
|
||||
return fmt.Sprintf("%s/v1/chat/completions", info.BaseUrl), nil
|
||||
}
|
||||
if info.RelayMode == constant.RelayModeResponses {
|
||||
return fmt.Sprintf("%s/v1/responses", info.BaseUrl), nil
|
||||
}
|
||||
if info.RelayMode == constant.RelayModeRealtime {
|
||||
if strings.HasPrefix(info.BaseUrl, "https://") {
|
||||
baseUrl := strings.TrimPrefix(info.BaseUrl, "https://")
|
||||
|
||||
@@ -215,10 +215,35 @@ func OpenaiHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayI
|
||||
StatusCode: resp.StatusCode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
forceFormat := false
|
||||
if forceFmt, ok := info.ChannelSetting[constant.ForceFormat].(bool); ok {
|
||||
forceFormat = forceFmt
|
||||
}
|
||||
|
||||
if simpleResponse.Usage.TotalTokens == 0 || (simpleResponse.Usage.PromptTokens == 0 && simpleResponse.Usage.CompletionTokens == 0) {
|
||||
completionTokens := 0
|
||||
for _, choice := range simpleResponse.Choices {
|
||||
ctkm, _ := service.CountTextToken(choice.Message.StringContent()+choice.Message.ReasoningContent+choice.Message.Reasoning, info.UpstreamModelName)
|
||||
completionTokens += ctkm
|
||||
}
|
||||
simpleResponse.Usage = dto.Usage{
|
||||
PromptTokens: info.PromptTokens,
|
||||
CompletionTokens: completionTokens,
|
||||
TotalTokens: info.PromptTokens + completionTokens,
|
||||
}
|
||||
}
|
||||
|
||||
switch info.RelayFormat {
|
||||
case relaycommon.RelayFormatOpenAI:
|
||||
break
|
||||
if forceFormat {
|
||||
responseBody, err = json.Marshal(simpleResponse)
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
case relaycommon.RelayFormatClaude:
|
||||
claudeResp := service.ResponseOpenAI2Claude(&simpleResponse, info)
|
||||
claudeRespStr, err := json.Marshal(claudeResp)
|
||||
@@ -244,18 +269,6 @@ func OpenaiHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayI
|
||||
common.SysError("error copying response body: " + err.Error())
|
||||
}
|
||||
resp.Body.Close()
|
||||
if simpleResponse.Usage.TotalTokens == 0 || (simpleResponse.Usage.PromptTokens == 0 && simpleResponse.Usage.CompletionTokens == 0) {
|
||||
completionTokens := 0
|
||||
for _, choice := range simpleResponse.Choices {
|
||||
ctkm, _ := service.CountTextToken(choice.Message.StringContent()+choice.Message.ReasoningContent+choice.Message.Reasoning, info.UpstreamModelName)
|
||||
completionTokens += ctkm
|
||||
}
|
||||
simpleResponse.Usage = dto.Usage{
|
||||
PromptTokens: info.PromptTokens,
|
||||
CompletionTokens: completionTokens,
|
||||
TotalTokens: info.PromptTokens + completionTokens,
|
||||
}
|
||||
}
|
||||
return nil, &simpleResponse.Usage
|
||||
}
|
||||
|
||||
|
||||
@@ -12,11 +12,19 @@ import (
|
||||
)
|
||||
|
||||
func SetEventStreamHeaders(c *gin.Context) {
|
||||
c.Writer.Header().Set("Content-Type", "text/event-stream")
|
||||
c.Writer.Header().Set("Cache-Control", "no-cache")
|
||||
c.Writer.Header().Set("Connection", "keep-alive")
|
||||
c.Writer.Header().Set("Transfer-Encoding", "chunked")
|
||||
c.Writer.Header().Set("X-Accel-Buffering", "no")
|
||||
// 检查是否已经设置过头部
|
||||
if _, exists := c.Get("event_stream_headers_set"); exists {
|
||||
return
|
||||
}
|
||||
|
||||
c.Writer.Header().Set("Content-Type", "text/event-stream")
|
||||
c.Writer.Header().Set("Cache-Control", "no-cache")
|
||||
c.Writer.Header().Set("Connection", "keep-alive")
|
||||
c.Writer.Header().Set("Transfer-Encoding", "chunked")
|
||||
c.Writer.Header().Set("X-Accel-Buffering", "no")
|
||||
|
||||
// 设置标志,表示头部已经设置过
|
||||
c.Set("event_stream_headers_set", true)
|
||||
}
|
||||
|
||||
func ClaudeData(c *gin.Context, resp dto.ClaudeResponse) error {
|
||||
|
||||
@@ -23,7 +23,7 @@ type PriceData struct {
|
||||
}
|
||||
|
||||
func (p PriceData) ToSetting() string {
|
||||
return fmt.Sprintf("ModelPrice: %f, ModelRatio: %f, CompletionRatio: %f, CacheRatio: %f, GroupRatio: %f, UsePrice: %t, CacheCreationRatio: %f, ShouldPreConsumedQuota: %d, ImageRatio: %d", p.ModelPrice, p.ModelRatio, p.CompletionRatio, p.CacheRatio, p.GroupRatio, p.UsePrice, p.CacheCreationRatio, p.ShouldPreConsumedQuota, p.ImageRatio)
|
||||
return fmt.Sprintf("ModelPrice: %f, ModelRatio: %f, CompletionRatio: %f, CacheRatio: %f, GroupRatio: %f, UsePrice: %t, CacheCreationRatio: %f, ShouldPreConsumedQuota: %d, ImageRatio: %f", p.ModelPrice, p.ModelRatio, p.CompletionRatio, p.CacheRatio, p.GroupRatio, p.UsePrice, p.CacheCreationRatio, p.ShouldPreConsumedQuota, p.ImageRatio)
|
||||
}
|
||||
|
||||
func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens int, maxTokens int) (PriceData, error) {
|
||||
|
||||
@@ -3,7 +3,6 @@ package helper
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"github.com/bytedance/gopkg/util/gopool"
|
||||
"io"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
@@ -14,6 +13,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/gopkg/util/gopool"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
||||
@@ -194,6 +194,7 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
|
||||
|
||||
var httpResp *http.Response
|
||||
resp, err := adaptor.DoRequest(c, relayInfo, requestBody)
|
||||
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapper(err, "do_request_failed", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func DoWorkerRequest(req *WorkerRequest) (*http.Response, error) {
|
||||
if !setting.EnableWorker() {
|
||||
return nil, fmt.Errorf("worker not enabled")
|
||||
}
|
||||
if !strings.HasPrefix(req.URL, "https") {
|
||||
if !setting.WorkerAllowHttpImageRequestEnabled && !strings.HasPrefix(req.URL, "https") {
|
||||
return nil, fmt.Errorf("only support https url")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,64 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"one-api/common"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var ModelRequestRateLimitEnabled = false
|
||||
var ModelRequestRateLimitDurationMinutes = 1
|
||||
var ModelRequestRateLimitCount = 0
|
||||
var ModelRequestRateLimitSuccessCount = 1000
|
||||
var ModelRequestRateLimitGroup = map[string][2]int{}
|
||||
var ModelRequestRateLimitMutex sync.RWMutex
|
||||
|
||||
func ModelRequestRateLimitGroup2JSONString() string {
|
||||
ModelRequestRateLimitMutex.RLock()
|
||||
defer ModelRequestRateLimitMutex.RUnlock()
|
||||
|
||||
jsonBytes, err := json.Marshal(ModelRequestRateLimitGroup)
|
||||
if err != nil {
|
||||
common.SysError("error marshalling model ratio: " + err.Error())
|
||||
}
|
||||
return string(jsonBytes)
|
||||
}
|
||||
|
||||
func UpdateModelRequestRateLimitGroupByJSONString(jsonStr string) error {
|
||||
ModelRequestRateLimitMutex.RLock()
|
||||
defer ModelRequestRateLimitMutex.RUnlock()
|
||||
|
||||
ModelRequestRateLimitGroup = make(map[string][2]int)
|
||||
return json.Unmarshal([]byte(jsonStr), &ModelRequestRateLimitGroup)
|
||||
}
|
||||
|
||||
func GetGroupRateLimit(group string) (totalCount, successCount int, found bool) {
|
||||
ModelRequestRateLimitMutex.RLock()
|
||||
defer ModelRequestRateLimitMutex.RUnlock()
|
||||
|
||||
if ModelRequestRateLimitGroup == nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
|
||||
limits, found := ModelRequestRateLimitGroup[group]
|
||||
if !found {
|
||||
return 0, 0, false
|
||||
}
|
||||
return limits[0], limits[1], true
|
||||
}
|
||||
|
||||
func CheckModelRequestRateLimitGroup(jsonStr string) error {
|
||||
checkModelRequestRateLimitGroup := make(map[string][2]int)
|
||||
err := json.Unmarshal([]byte(jsonStr), &checkModelRequestRateLimitGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for group, limits := range checkModelRequestRateLimitGroup {
|
||||
if limits[0] < 0 || limits[1] < 1 {
|
||||
return fmt.Errorf("group %s has negative rate limit values: [%d, %d]", group, limits[0], limits[1])
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package setting
|
||||
var ServerAddress = "http://localhost:3000"
|
||||
var WorkerUrl = ""
|
||||
var WorkerValidKey = ""
|
||||
var WorkerAllowHttpImageRequestEnabled = false
|
||||
|
||||
func EnableWorker() bool {
|
||||
return WorkerUrl != ""
|
||||
|
||||
@@ -13,6 +13,7 @@ const RateLimitSetting = () => {
|
||||
ModelRequestRateLimitCount: 0,
|
||||
ModelRequestRateLimitSuccessCount: 1000,
|
||||
ModelRequestRateLimitDurationMinutes: 1,
|
||||
ModelRequestRateLimitGroup: '',
|
||||
});
|
||||
|
||||
let [loading, setLoading] = useState(false);
|
||||
@@ -23,10 +24,14 @@ const RateLimitSetting = () => {
|
||||
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;
|
||||
if (item.key === 'ModelRequestRateLimitGroup') {
|
||||
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
|
||||
}
|
||||
|
||||
if (item.key.endsWith('Enabled')) {
|
||||
newInputs[item.key] = item.value === 'true' ? true : false;
|
||||
} else {
|
||||
newInputs[item.key] = item.value;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
verifyJSON,
|
||||
} from '../helpers/utils';
|
||||
import { API } from '../helpers/api';
|
||||
import axios from "axios";
|
||||
import axios from 'axios';
|
||||
|
||||
const SystemSetting = () => {
|
||||
let [inputs, setInputs] = useState({
|
||||
@@ -45,6 +45,7 @@ const SystemSetting = () => {
|
||||
ServerAddress: '',
|
||||
WorkerUrl: '',
|
||||
WorkerValidKey: '',
|
||||
WorkerAllowHttpImageRequestEnabled: '',
|
||||
EpayId: '',
|
||||
EpayKey: '',
|
||||
Price: 7.3,
|
||||
@@ -111,6 +112,7 @@ const SystemSetting = () => {
|
||||
case 'SMTPSSLEnabled':
|
||||
case 'LinuxDOOAuthEnabled':
|
||||
case 'oidc.enabled':
|
||||
case 'WorkerAllowHttpImageRequestEnabled':
|
||||
item.value = item.value === 'true';
|
||||
break;
|
||||
case 'Price':
|
||||
@@ -206,7 +208,11 @@ const SystemSetting = () => {
|
||||
let WorkerUrl = removeTrailingSlash(inputs.WorkerUrl);
|
||||
const options = [
|
||||
{ key: 'WorkerUrl', value: WorkerUrl },
|
||||
]
|
||||
{
|
||||
key: 'WorkerAllowHttpImageRequestEnabled',
|
||||
value: inputs.WorkerAllowHttpImageRequestEnabled ? 'true' : 'false',
|
||||
},
|
||||
];
|
||||
if (inputs.WorkerValidKey !== '' || WorkerUrl === '') {
|
||||
options.push({ key: 'WorkerValidKey', value: inputs.WorkerValidKey });
|
||||
}
|
||||
@@ -302,7 +308,8 @@ const SystemSetting = () => {
|
||||
const domain = emailToAdd.trim();
|
||||
|
||||
// 验证域名格式
|
||||
const domainRegex = /^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
|
||||
const domainRegex =
|
||||
/^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
|
||||
if (!domainRegex.test(domain)) {
|
||||
showError('邮箱域名格式不正确,请输入有效的域名,如 gmail.com');
|
||||
return;
|
||||
@@ -577,6 +584,12 @@ const SystemSetting = () => {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Checkbox
|
||||
field='WorkerAllowHttpImageRequestEnabled'
|
||||
noLabel
|
||||
>
|
||||
允许 HTTP 协议图片请求(适用于自部署代理)
|
||||
</Form.Checkbox>
|
||||
<Button onClick={submitWorker}>更新Worker设置</Button>
|
||||
</Form.Section>
|
||||
</Card>
|
||||
@@ -799,7 +812,13 @@ const SystemSetting = () => {
|
||||
onChange={(value) => setEmailToAdd(value)}
|
||||
style={{ marginTop: 16 }}
|
||||
suffix={
|
||||
<Button theme="solid" type="primary" onClick={handleAddEmail}>添加</Button>
|
||||
<Button
|
||||
theme='solid'
|
||||
type='primary'
|
||||
onClick={handleAddEmail}
|
||||
>
|
||||
添加
|
||||
</Button>
|
||||
}
|
||||
onEnterPress={handleAddEmail}
|
||||
/>
|
||||
|
||||
@@ -1086,7 +1086,7 @@
|
||||
"没有账户?": "No account? ",
|
||||
"请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com": "Please enter AZURE_OPENAI_ENDPOINT, e.g.: https://docs-test-001.openai.azure.com",
|
||||
"默认 API 版本": "Default API Version",
|
||||
"请输入默认 API 版本,例如:2024-12-01-preview": "Please enter default API version, e.g.: 2024-12-01-preview.",
|
||||
"请输入默认 API 版本,例如:2025-04-01-preview": "Please enter default API version, e.g.: 2025-04-01-preview.",
|
||||
"请为渠道命名": "Please name the channel",
|
||||
"请选择可以使用该渠道的分组": "Please select groups that can use this channel",
|
||||
"请在系统设置页面编辑分组倍率以添加新的分组:": "Please edit Group ratios in system settings to add new groups:",
|
||||
@@ -1374,4 +1374,4 @@
|
||||
"适用于展示系统功能的场景。": "Suitable for scenarios where the system functions are displayed.",
|
||||
"可在初始化后修改": "Can be modified after initialization",
|
||||
"初始化系统": "Initialize system"
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,8 @@ import {
|
||||
TextArea,
|
||||
Checkbox,
|
||||
Banner,
|
||||
Modal, ImagePreview
|
||||
Modal,
|
||||
ImagePreview,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { getChannelModels, loadChannelModels } from '../../components/utils.js';
|
||||
import { IconHelpCircle } from '@douyinfe/semi-icons';
|
||||
@@ -306,7 +307,7 @@ const EditChannel = (props) => {
|
||||
fetchModels().then();
|
||||
fetchGroups().then();
|
||||
if (isEdit) {
|
||||
loadChannel().then(() => { });
|
||||
loadChannel().then(() => {});
|
||||
} else {
|
||||
setInputs(originInputs);
|
||||
let localModels = getChannelModels(inputs.type);
|
||||
@@ -477,7 +478,9 @@ const EditChannel = (props) => {
|
||||
type={'warning'}
|
||||
description={
|
||||
<>
|
||||
{t('2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的"."')}
|
||||
{t(
|
||||
'2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的"."',
|
||||
)}
|
||||
{/*<br />*/}
|
||||
{/*<Typography.Text*/}
|
||||
{/* style={{*/}
|
||||
@@ -522,7 +525,7 @@ const EditChannel = (props) => {
|
||||
<Input
|
||||
label={t('默认 API 版本')}
|
||||
name='azure_other'
|
||||
placeholder={t('请输入默认 API 版本,例如:2024-12-01-preview')}
|
||||
placeholder={t('请输入默认 API 版本,例如:2025-04-01-preview')}
|
||||
onChange={(value) => {
|
||||
handleInputChange('other', value);
|
||||
}}
|
||||
@@ -584,25 +587,35 @@ const EditChannel = (props) => {
|
||||
value={inputs.name}
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
{inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && inputs.type !== 45 && (
|
||||
<>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>{t('API地址')}:</Typography.Text>
|
||||
</div>
|
||||
<Tooltip content={t('对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写')}>
|
||||
<Input
|
||||
label={t('API地址')}
|
||||
name="base_url"
|
||||
placeholder={t('此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/')}
|
||||
onChange={(value) => {
|
||||
handleInputChange('base_url', value);
|
||||
}}
|
||||
value={inputs.base_url}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
{inputs.type !== 3 &&
|
||||
inputs.type !== 8 &&
|
||||
inputs.type !== 22 &&
|
||||
inputs.type !== 36 &&
|
||||
inputs.type !== 45 && (
|
||||
<>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>{t('API地址')}:</Typography.Text>
|
||||
</div>
|
||||
<Tooltip
|
||||
content={t(
|
||||
'对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写',
|
||||
)}
|
||||
>
|
||||
<Input
|
||||
label={t('API地址')}
|
||||
name='base_url'
|
||||
placeholder={t(
|
||||
'此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/',
|
||||
)}
|
||||
onChange={(value) => {
|
||||
handleInputChange('base_url', value);
|
||||
}}
|
||||
value={inputs.base_url}
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>{t('密钥')}:</Typography.Text>
|
||||
</div>
|
||||
@@ -761,10 +774,10 @@ const EditChannel = (props) => {
|
||||
name='other'
|
||||
placeholder={t(
|
||||
'请输入部署地区,例如:us-central1\n支持使用模型映射格式\n' +
|
||||
'{\n' +
|
||||
' "default": "us-central1",\n' +
|
||||
' "claude-3-5-sonnet-20240620": "europe-west1"\n' +
|
||||
'}',
|
||||
'{\n' +
|
||||
' "default": "us-central1",\n' +
|
||||
' "claude-3-5-sonnet-20240620": "europe-west1"\n' +
|
||||
'}',
|
||||
)}
|
||||
autosize={{ minRows: 2 }}
|
||||
onChange={(value) => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
showError,
|
||||
showSuccess,
|
||||
showWarning,
|
||||
verifyJSON,
|
||||
} from '../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -18,6 +19,7 @@ export default function RequestRateLimit(props) {
|
||||
ModelRequestRateLimitCount: -1,
|
||||
ModelRequestRateLimitSuccessCount: 1000,
|
||||
ModelRequestRateLimitDurationMinutes: 1,
|
||||
ModelRequestRateLimitGroup: '',
|
||||
});
|
||||
const refForm = useRef();
|
||||
const [inputsRow, setInputsRow] = useState(inputs);
|
||||
@@ -46,6 +48,13 @@ export default function RequestRateLimit(props) {
|
||||
if (res.includes(undefined))
|
||||
return showError(t('部分保存失败,请重试'));
|
||||
}
|
||||
|
||||
for (let i = 0; i < res.length; i++) {
|
||||
if (!res[i].data.success) {
|
||||
return showError(res[i].data.message);
|
||||
}
|
||||
}
|
||||
|
||||
showSuccess(t('保存成功'));
|
||||
props.refresh();
|
||||
})
|
||||
@@ -147,6 +156,41 @@ export default function RequestRateLimit(props) {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col xs={24} sm={16}>
|
||||
<Form.TextArea
|
||||
label={t('分组速率限制')}
|
||||
placeholder={t(
|
||||
'{\n "default": [200, 100],\n "vip": [0, 1000]\n}',
|
||||
)}
|
||||
field={'ModelRequestRateLimitGroup'}
|
||||
autosize={{ minRows: 5, maxRows: 15 }}
|
||||
trigger='blur'
|
||||
stopValidateWithError
|
||||
rules={[
|
||||
{
|
||||
validator: (rule, value) => verifyJSON(value),
|
||||
message: t('不是合法的 JSON 字符串'),
|
||||
},
|
||||
]}
|
||||
extraText={
|
||||
<div>
|
||||
<p style={{ marginBottom: -15 }}>{t('说明:')}</p>
|
||||
<ul>
|
||||
<li>{t('使用 JSON 对象格式,格式为:{"组名": [最多请求次数, 最多请求完成次数]}')}</li>
|
||||
<li>{t('示例:{"default": [200, 100], "vip": [0, 1000]}。')}</li>
|
||||
<li>{t('[最多请求次数]必须大于等于0,[最多请求完成次数]必须大于等于1。')}</li>
|
||||
<li>{t('分组速率配置优先级高于全局速率限制。')}</li>
|
||||
<li>{t('限制周期统一使用上方配置的“限制周期”值。')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
onChange={(value) => {
|
||||
setInputs({ ...inputs, ModelRequestRateLimitGroup: value });
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Button size='default' onClick={onSubmit}>
|
||||
{t('保存模型速率限制')}
|
||||
|
||||
Reference in New Issue
Block a user