mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-31 11:02:21 +00:00
Compare commits
7 Commits
fix-random
...
v0.9.14-pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52d9b8cc78 | ||
|
|
f1ac5606ee | ||
|
|
b2d9771a57 | ||
|
|
c4ca9d7c3b | ||
|
|
303feafc3c | ||
|
|
b2de5e229c | ||
|
|
8297723d91 |
@@ -30,10 +30,11 @@ func GetAudioDuration(ctx context.Context, f io.ReadSeeker, ext string) (duratio
|
||||
duration, err = getFLACDuration(f)
|
||||
case ".m4a", ".mp4":
|
||||
duration, err = getM4ADuration(f)
|
||||
case ".ogg", ".oga":
|
||||
case ".ogg", ".oga", ".opus":
|
||||
duration, err = getOGGDuration(f)
|
||||
case ".opus":
|
||||
duration, err = getOpusDuration(f)
|
||||
if err != nil {
|
||||
duration, err = getOpusDuration(f)
|
||||
}
|
||||
case ".aiff", ".aif", ".aifc":
|
||||
duration, err = getAIFFDuration(f)
|
||||
case ".webm":
|
||||
|
||||
@@ -230,10 +230,6 @@ func GetUUID() string {
|
||||
|
||||
const keyChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
func init() {
|
||||
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
}
|
||||
|
||||
func GenerateRandomCharsKey(length int) (string, error) {
|
||||
b := make([]byte, length)
|
||||
maxI := big.NewInt(int64(len(keyChars)))
|
||||
|
||||
@@ -29,11 +29,11 @@ func GetUserGroups(c *gin.Context) {
|
||||
userId := c.GetInt("id")
|
||||
userGroup, _ = model.GetUserGroup(userId, false)
|
||||
userUsableGroups := service.GetUserUsableGroups(userGroup)
|
||||
for groupName, ratio := range ratio_setting.GetGroupRatioCopy() {
|
||||
for groupName, _ := range ratio_setting.GetGroupRatioCopy() {
|
||||
// UserUsableGroups contains the groups that the user can use
|
||||
if desc, ok := userUsableGroups[groupName]; ok {
|
||||
usableGroups[groupName] = map[string]interface{}{
|
||||
"ratio": ratio,
|
||||
"ratio": service.GetUserGroupRatio(userGroup, groupName),
|
||||
"desc": desc,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func Playground(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
group := c.GetString("group")
|
||||
group := common.GetContextKeyString(c, constant.ContextKeyUsingGroup)
|
||||
modelName := c.GetString("original_model")
|
||||
|
||||
userId := c.GetInt("id")
|
||||
|
||||
@@ -84,6 +84,7 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) {
|
||||
|
||||
defer func() {
|
||||
if newAPIError != nil {
|
||||
logger.LogError(c, fmt.Sprintf("relay error: %s", newAPIError.Error()))
|
||||
newAPIError.SetMessage(common.MessageWithRequestId(newAPIError.Error(), requestId))
|
||||
switch relayFormat {
|
||||
case types.RelayFormatOpenAIRealtime:
|
||||
@@ -281,7 +282,7 @@ func shouldRetry(c *gin.Context, openaiErr *types.NewAPIError, retryTimes int) b
|
||||
}
|
||||
|
||||
func processChannelError(c *gin.Context, channelError types.ChannelError, err *types.NewAPIError) {
|
||||
logger.LogError(c, fmt.Sprintf("relay error (channel #%d, status code: %d): %s", channelError.ChannelId, err.StatusCode, err.Error()))
|
||||
logger.LogError(c, fmt.Sprintf("channel error (channel #%d, status code: %d): %s", channelError.ChannelId, err.StatusCode, err.Error()))
|
||||
// 不要使用context获取渠道信息,异步处理时可能会出现渠道信息不一致的情况
|
||||
// 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 {
|
||||
|
||||
@@ -94,6 +94,7 @@ func Distribute() func(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
usingGroup = playgroundRequest.Group
|
||||
common.SetContextKey(c, constant.ContextKeyUsingGroup, usingGroup)
|
||||
}
|
||||
}
|
||||
channel, selectGroup, err = service.CacheGetRandomSatisfiedChannel(c, usingGroup, modelRequest.Model, 0)
|
||||
|
||||
@@ -142,7 +142,6 @@ func GetRandomSatisfiedChannel(group string, model string, retry int) (*Channel,
|
||||
targetPriority := int64(sortedUniquePriorities[retry])
|
||||
|
||||
// get the priority for the given retry number
|
||||
var shouldSmooth = false
|
||||
var sumWeight = 0
|
||||
var targetChannels []*Channel
|
||||
for _, channelId := range channels {
|
||||
@@ -155,38 +154,34 @@ func GetRandomSatisfiedChannel(group string, model string, retry int) (*Channel,
|
||||
return nil, fmt.Errorf("数据库一致性错误,渠道# %d 不存在,请联系管理员修复", channelId)
|
||||
}
|
||||
}
|
||||
if sumWeight/len(targetChannels) < 10 {
|
||||
shouldSmooth = true
|
||||
|
||||
if len(targetChannels) == 0 {
|
||||
return nil, errors.New(fmt.Sprintf("no channel found, group: %s, model: %s, priority: %d", group, model, targetPriority))
|
||||
}
|
||||
|
||||
// 平滑系数
|
||||
// smoothing factor and adjustment
|
||||
smoothingFactor := 1
|
||||
if shouldSmooth {
|
||||
smoothingAdjustment := 0
|
||||
|
||||
if sumWeight == 0 {
|
||||
// when all channels have weight 0, set sumWeight to the number of channels and set smoothing adjustment to 100
|
||||
// each channel's effective weight = 100
|
||||
sumWeight = len(targetChannels) * 100
|
||||
smoothingAdjustment = 100
|
||||
} else if sumWeight/len(targetChannels) < 10 {
|
||||
// when the average weight is less than 10, set smoothing factor to 100
|
||||
smoothingFactor = 100
|
||||
}
|
||||
|
||||
// Calculate the total weight of all channels up to endIdx
|
||||
totalWeight := sumWeight * smoothingFactor
|
||||
|
||||
// totalWeight 小于等于0时,给每个渠道加100的权重,然后进行随机选择
|
||||
if totalWeight <= 0 {
|
||||
if len(targetChannels) > 0 {
|
||||
totalWeight = len(targetChannels) * 100
|
||||
randomWeight := rand.Intn(totalWeight)
|
||||
for _, channel := range targetChannels {
|
||||
randomWeight -= 100
|
||||
if randomWeight <= 0 {
|
||||
return channel, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, errors.New("no available channels")
|
||||
}
|
||||
// Generate a random value in the range [0, totalWeight)
|
||||
randomWeight := rand.Intn(totalWeight)
|
||||
|
||||
// Find a channel based on its weight
|
||||
for _, channel := range targetChannels {
|
||||
randomWeight -= channel.GetWeight() * smoothingFactor
|
||||
randomWeight -= channel.GetWeight()*smoothingFactor + smoothingAdjustment
|
||||
if randomWeight < 0 {
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
@@ -15,9 +15,11 @@ import (
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/dto"
|
||||
"github.com/QuantumNous/new-api/logger"
|
||||
"github.com/QuantumNous/new-api/relay/channel"
|
||||
"github.com/QuantumNous/new-api/relay/channel/ai360"
|
||||
"github.com/QuantumNous/new-api/relay/channel/lingyiwanwu"
|
||||
|
||||
//"github.com/QuantumNous/new-api/relay/channel/minimax"
|
||||
"github.com/QuantumNous/new-api/relay/channel/openrouter"
|
||||
"github.com/QuantumNous/new-api/relay/channel/xinference"
|
||||
@@ -352,27 +354,43 @@ func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInf
|
||||
|
||||
writer.WriteField("model", request.Model)
|
||||
|
||||
// 获取所有表单字段
|
||||
formData := c.Request.PostForm
|
||||
formData, err2 := common.ParseMultipartFormReusable(c)
|
||||
if err2 != nil {
|
||||
return nil, fmt.Errorf("error parsing multipart form: %w", err2)
|
||||
}
|
||||
|
||||
// 打印类似 curl 命令格式的信息
|
||||
logger.LogDebug(c.Request.Context(), fmt.Sprintf("--form 'model=\"%s\"'", request.Model))
|
||||
|
||||
// 遍历表单字段并打印输出
|
||||
for key, values := range formData {
|
||||
for key, values := range formData.Value {
|
||||
if key == "model" {
|
||||
continue
|
||||
}
|
||||
for _, value := range values {
|
||||
writer.WriteField(key, value)
|
||||
logger.LogDebug(c.Request.Context(), fmt.Sprintf("--form '%s=\"%s\"'", key, value))
|
||||
}
|
||||
}
|
||||
|
||||
// 添加文件字段
|
||||
file, header, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
// 从 formData 中获取文件
|
||||
fileHeaders := formData.File["file"]
|
||||
if len(fileHeaders) == 0 {
|
||||
return nil, errors.New("file is required")
|
||||
}
|
||||
|
||||
// 使用 formData 中的第一个文件
|
||||
fileHeader := fileHeaders[0]
|
||||
logger.LogDebug(c.Request.Context(), fmt.Sprintf("--form 'file=@\"%s\"' (size: %d bytes, content-type: %s)",
|
||||
fileHeader.Filename, fileHeader.Size, fileHeader.Header.Get("Content-Type")))
|
||||
|
||||
file, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error opening audio file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
part, err := writer.CreateFormFile("file", header.Filename)
|
||||
part, err := writer.CreateFormFile("file", fileHeader.Filename)
|
||||
if err != nil {
|
||||
return nil, errors.New("create form file failed")
|
||||
}
|
||||
@@ -383,6 +401,7 @@ func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInf
|
||||
// 关闭 multipart 编写器以设置分界线
|
||||
writer.Close()
|
||||
c.Request.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
logger.LogDebug(c.Request.Context(), fmt.Sprintf("--header 'Content-Type: %s'", writer.FormDataContentType()))
|
||||
return &requestBody, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,8 +31,8 @@ func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayIn
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
|
||||
//TODO implement me
|
||||
return nil, errors.New("not supported")
|
||||
adaptor := openai.Adaptor{}
|
||||
return adaptor.ConvertAudioRequest(c, info, request)
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) {
|
||||
@@ -65,16 +65,8 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
|
||||
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
|
||||
if info.RelayMode == constant.RelayModeRerank {
|
||||
return fmt.Sprintf("%s/v1/rerank", info.ChannelBaseUrl), nil
|
||||
} else if info.RelayMode == constant.RelayModeEmbeddings {
|
||||
return fmt.Sprintf("%s/v1/embeddings", info.ChannelBaseUrl), nil
|
||||
} else if info.RelayMode == constant.RelayModeChatCompletions {
|
||||
return fmt.Sprintf("%s/v1/chat/completions", info.ChannelBaseUrl), nil
|
||||
} else if info.RelayMode == constant.RelayModeCompletions {
|
||||
return fmt.Sprintf("%s/v1/completions", info.ChannelBaseUrl), nil
|
||||
} else if info.RelayMode == constant.RelayModeImagesGenerations {
|
||||
return fmt.Sprintf("%s/v1/images/generations", info.ChannelBaseUrl), nil
|
||||
}
|
||||
return fmt.Sprintf("%s/v1/chat/completions", info.ChannelBaseUrl), nil
|
||||
return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, info.RequestURLPath, info.ChannelType), nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error {
|
||||
@@ -103,7 +95,8 @@ func (a *Adaptor) ConvertOpenAIResponsesRequest(c *gin.Context, info *relaycommo
|
||||
}
|
||||
|
||||
func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) {
|
||||
return channel.DoApiRequest(a, c, info, requestBody)
|
||||
adaptor := openai.Adaptor{}
|
||||
return adaptor.DoRequest(c, info, requestBody)
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) {
|
||||
@@ -118,21 +111,9 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
|
||||
switch info.RelayMode {
|
||||
case constant.RelayModeRerank:
|
||||
usage, err = siliconflowRerankHandler(c, info, resp)
|
||||
case constant.RelayModeEmbeddings:
|
||||
usage, err = openai.OpenaiHandler(c, info, resp)
|
||||
case constant.RelayModeCompletions:
|
||||
fallthrough
|
||||
case constant.RelayModeChatCompletions:
|
||||
fallthrough
|
||||
case constant.RelayModeImagesGenerations:
|
||||
fallthrough
|
||||
default:
|
||||
if info.IsStream {
|
||||
usage, err = openai.OaiStreamHandler(c, info, resp)
|
||||
} else {
|
||||
usage, err = openai.OpenaiHandler(c, info, resp)
|
||||
}
|
||||
|
||||
adaptor := openai.Adaptor{}
|
||||
usage, err = adaptor.DoResponse(c, resp, info)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -52,3 +52,14 @@ func GetUserAutoGroup(userGroup string) []string {
|
||||
}
|
||||
return autoGroups
|
||||
}
|
||||
|
||||
// GetUserGroupRatio 获取用户使用某个分组的倍率
|
||||
// userGroup 用户分组
|
||||
// group 需要获取倍率的分组
|
||||
func GetUserGroupRatio(userGroup, group string) float64 {
|
||||
ratio, ok := ratio_setting.GetGroupGroupRatio(userGroup, group)
|
||||
if ok {
|
||||
return ratio
|
||||
}
|
||||
return ratio_setting.GetGroupRatio(group)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
import React from 'react';
|
||||
import { Button, Dropdown } from '@douyinfe/semi-ui';
|
||||
import { Languages } from 'lucide-react';
|
||||
import { CN, GB, FR, RU } from 'country-flag-icons/react/3x2';
|
||||
import { CN, GB, FR, RU, JP } from 'country-flag-icons/react/3x2';
|
||||
|
||||
const LanguageSelector = ({ currentLang, onLanguageChange, t }) => {
|
||||
return (
|
||||
@@ -28,6 +28,7 @@ const LanguageSelector = ({ currentLang, onLanguageChange, t }) => {
|
||||
position='bottomRight'
|
||||
render={
|
||||
<Dropdown.Menu className='!bg-semi-color-bg-overlay !border-semi-color-border !shadow-lg !rounded-lg dark:!bg-gray-700 dark:!border-gray-600'>
|
||||
{/* Language sorting: Order by English name (Chinese, English, French, Japanese, Russian) */}
|
||||
<Dropdown.Item
|
||||
onClick={() => onLanguageChange('zh')}
|
||||
className={`!flex !items-center !gap-2 !px-3 !py-1.5 !text-sm !text-semi-color-text-0 dark:!text-gray-200 ${currentLang === 'zh' ? '!bg-semi-color-primary-light-default dark:!bg-blue-600 !font-semibold' : 'hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-600'}`}
|
||||
@@ -49,6 +50,14 @@ const LanguageSelector = ({ currentLang, onLanguageChange, t }) => {
|
||||
<FR title='Français' className='!w-5 !h-auto' />
|
||||
<span>Français</span>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
onClick={() => onLanguageChange('ja')}
|
||||
className={`!flex !items-center !gap-2 !px-3 !py-1.5 !text-sm !text-semi-color-text-0 dark:!text-gray-200 ${currentLang === 'ja' ? '!bg-semi-color-primary-light-default dark:!bg-blue-600 !font-semibold' : 'hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-600'}`}
|
||||
>
|
||||
{/* Japanese flag using emoji as country-flag-icons/react/3x2 does not export JP */}
|
||||
<JP title='日本語' className='!w-5 !h-auto' />
|
||||
<span>日本語</span>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
onClick={() => onLanguageChange('ru')}
|
||||
className={`!flex !items-center !gap-2 !px-3 !py-1.5 !text-sm !text-semi-color-text-0 dark:!text-gray-200 ${currentLang === 'ru' ? '!bg-semi-color-primary-light-default dark:!bg-blue-600 !font-semibold' : 'hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-600'}`}
|
||||
|
||||
@@ -25,6 +25,7 @@ import enTranslation from './locales/en.json';
|
||||
import frTranslation from './locales/fr.json';
|
||||
import zhTranslation from './locales/zh.json';
|
||||
import ruTranslation from './locales/ru.json';
|
||||
import jaTranslation from './locales/ja.json';
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
@@ -36,6 +37,7 @@ i18n
|
||||
zh: zhTranslation,
|
||||
fr: frTranslation,
|
||||
ru: ruTranslation,
|
||||
ja: jaTranslation,
|
||||
},
|
||||
fallbackLng: 'zh',
|
||||
interpolation: {
|
||||
|
||||
@@ -2100,6 +2100,8 @@
|
||||
"例如:4.99": "e.g.: 4.99",
|
||||
"例如:100000": "e.g.: 100000",
|
||||
"请填写完整的产品信息": "Please fill in complete product information",
|
||||
"产品ID已存在": "Product ID already exists"
|
||||
"产品ID已存在": "Product ID already exists",
|
||||
"统一的": "The Unified",
|
||||
"大模型接口网关": "LLM API Gateway"
|
||||
}
|
||||
}
|
||||
@@ -2080,6 +2080,8 @@
|
||||
"默认区域,如: us-central1": "Région par défaut, ex: us-central1",
|
||||
"默认折叠侧边栏": "Réduire la barre latérale par défaut",
|
||||
"默认测试模型": "Modèle de test par défaut",
|
||||
"默认补全倍率": "Taux de complétion par défaut"
|
||||
"默认补全倍率": "Taux de complétion par défaut",
|
||||
"统一的": "La Passerelle",
|
||||
"大模型接口网关": "API LLM Unifiée"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2071,6 +2071,8 @@
|
||||
"默认区域,如: us-central1": "デフォルトリージョン(例:us-central1)",
|
||||
"默认折叠侧边栏": "サイドバーをデフォルトで折りたたむ",
|
||||
"默认测试模型": "デフォルトテストモデル",
|
||||
"默认补全倍率": "デフォルト補完倍率"
|
||||
"默认补全倍率": "デフォルト補完倍率",
|
||||
"统一的": "統合型",
|
||||
"大模型接口网关": "LLM APIゲートウェイ"
|
||||
}
|
||||
}
|
||||
@@ -2089,6 +2089,8 @@
|
||||
"默认区域,如: us-central1": "Регион по умолчанию, например: us-central1",
|
||||
"默认折叠侧边栏": "Сворачивать боковую панель по умолчанию",
|
||||
"默认测试模型": "Модель для тестирования по умолчанию",
|
||||
"默认补全倍率": "Коэффициент вывода по умолчанию"
|
||||
"默认补全倍率": "Коэффициент вывода по умолчанию",
|
||||
"统一的": "Единый",
|
||||
"大模型接口网关": "Шлюз API LLM"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,19 +169,11 @@ const Home = () => {
|
||||
<h1
|
||||
className={`text-4xl md:text-5xl lg:text-6xl xl:text-7xl font-bold text-semi-color-text-0 leading-tight ${isChinese ? 'tracking-wide md:tracking-wider' : ''}`}
|
||||
>
|
||||
{i18n.language === 'en' ? (
|
||||
<>
|
||||
The Unified
|
||||
<br />
|
||||
<span className='shine-text'>LLMs API Gateway</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
统一的
|
||||
<br />
|
||||
<span className='shine-text'>大模型接口网关</span>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
{t('统一的')}
|
||||
<br />
|
||||
<span className='shine-text'>{t('大模型接口网关')}</span>
|
||||
</>
|
||||
</h1>
|
||||
<p className='text-base md:text-lg lg:text-xl text-semi-color-text-1 mt-4 md:mt-6 max-w-xl'>
|
||||
{t('更好的价格,更好的稳定性,只需要将模型基址替换为:')}
|
||||
|
||||
Reference in New Issue
Block a user