Compare commits

...

30 Commits

Author SHA1 Message Date
Calcium-Ion
4937a6d1ed Merge pull request #591 from Calcium-Ion/no-cache
feat: add Cache-Control header to API requests
2024-12-04 20:53:22 +08:00
CalciumIon
de9a0d65ae feat: add Cache-Control header to API requests 2024-12-04 20:51:55 +08:00
Calcium-Ion
ee99041910 Merge pull request #590 from iszcz/new512
realtime令牌额度检测和http
2024-12-04 19:49:57 +08:00
iszcz
c8a29251ac 1 2024-12-04 16:20:42 +08:00
CalciumIon
07b1c9a4db Update docker-compose.yml 2024-12-03 16:48:38 +08:00
Calcium-Ion
5d8de46e4c Merge pull request #589 from mrhaoji/main
fix: 360智脑接口地址更新
2024-12-03 13:41:14 +08:00
Benny
28885feea2 fix: 360智能接口地址更新 2024-12-02 15:59:08 +00:00
Calcium-Ion
f693c13ce6 Merge pull request #588 from iszcz/new512
渠道tag编辑名称
2024-12-01 23:14:35 +08:00
iszcz
89cd0db28c Update EditTagModal.js 2024-12-01 22:36:51 +08:00
Calcium-Ion
ae57dd7b8b Update README.md 2024-12-01 21:58:36 +08:00
CalciumIon
87d763e641 Merge remote-tracking branch 'origin/main' 2024-12-01 13:59:13 +08:00
Calcium-Ion
08f3562e53 Merge pull request #587 from Calcium-Ion/channel-tag
feat: add tag aggregation mode to channels API and UI
2024-12-01 09:25:43 +08:00
CalciumIon
88b0e6a768 feat: add tag aggregation mode to channels API and UI 2024-12-01 09:24:43 +08:00
CalciumIon
a9f739a7e2 refactor: improve validation logic and error handling in relay-text.go
- Simplified validation checks for MaxTokens and Messages fields.
- Enhanced error messages for better clarity.
- Updated goroutine to avoid passing context unnecessarily.
2024-12-01 08:24:41 +08:00
CalciumIon
6d4edc1f5b fix: realtime 2024-11-30 23:32:42 +08:00
CalciumIon
2d1b2676f7 Update docker-compose.yml 2024-11-30 21:36:34 +08:00
CalciumIon
1035a8e0df Update README.md 2024-11-30 20:47:26 +08:00
Calcium-Ion
ea433b2ed6 Merge pull request #586 from Calcium-Ion/channel-tag
fix: tag channel copy
2024-11-30 19:52:58 +08:00
CalciumIon
bb0c504709 fix: tag channel copy 2024-11-30 19:52:36 +08:00
Calcium-Ion
48abfd055c Merge pull request #574 from Calcium-Ion/channel-tag
feat: 初步集成渠道标签分组功能
2024-11-30 17:45:16 +08:00
CalciumIon
6693072c49 feat: 完善标签编辑(优先级,权重) 2024-11-30 17:43:03 +08:00
CalciumIon
3053d94170 feat: 完善标签编辑 2024-11-30 16:57:58 +08:00
CalciumIon
1774be8536 fix: xAI missing finish_reason #572 2024-11-30 16:57:57 +08:00
Calcium-Ion
821f3a7522 Merge pull request #582 from prnake/patch-1
feat: add claude-3-5-haiku-20241022
2024-11-30 15:07:31 +08:00
CalciumIon
9c4d30602c feat: 完善标签编辑 2024-11-29 23:58:31 +08:00
CalciumIon
7b3394d863 chore: update default STREAMING_TIMEOUT 2024-11-28 23:59:10 +08:00
papersnake
999ba11363 feat: add claude-3-5-haiku-20241022 2024-11-27 13:33:37 +08:00
CalciumIon
6e6e390f6f feat: 一键编辑标签下渠道重定向 2024-11-19 01:43:05 +08:00
CalciumIon
807385d3d1 fix: search channel #442 2024-11-19 01:39:27 +08:00
CalciumIon
0ce600ed49 feat: 渠道标签分组 2024-11-19 01:13:18 +08:00
24 changed files with 1592 additions and 646 deletions

View File

@@ -68,10 +68,10 @@
## 比原版One API多出的配置
- `GENERATE_DEFAULT_TOKEN`:是否为新注册用户生成初始令牌,默认为 `false`
- `STREAMING_TIMEOUT`:设置流式一次回复的超时时间,默认为 30 秒。
- `STREAMING_TIMEOUT`:设置流式一次回复的超时时间,默认为 60 秒。
- `DIFY_DEBUG`:设置 Dify 渠道是否输出工作流和节点信息到客户端,默认为 `true`
- `FORCE_STREAM_OPTION`是否覆盖客户端stream_options参数请求上游返回流模式usage默认为 `true`建议开启不影响客户端传入stream_options参数返回结果。
- `GET_MEDIA_TOKEN`是统计图片token默认为 `true`关闭后将不再在本地计算图片token可能会导致和上游计费不同此项覆盖 `GET_MEDIA_TOKEN_NOT_STREAM` 选项作用。
- `GET_MEDIA_TOKEN`:是统计图片token默认为 `true`关闭后将不再在本地计算图片token可能会导致和上游计费不同此项覆盖 `GET_MEDIA_TOKEN_NOT_STREAM` 选项作用。
- `GET_MEDIA_TOKEN_NOT_STREAM`:是否在非流(`stream=false`情况下统计图片token默认为 `true`
- `UPDATE_TASK`是否更新异步任务Midjourney、Suno默认为 `true`,关闭后将不会更新任务进度。
- `GEMINI_MODEL_MAP`Gemini模型指定版本(v1/v1beta),使用“模型:版本”指定,","分隔,例如:-e GEMINI_MODEL_MAP="gemini-1.5-pro-latest:v1beta,gemini-1.5-pro-001:v1beta",为空则使用默认配置(v1beta)
@@ -88,6 +88,17 @@
[图文教程](BT.md)
### 基于 Docker 进行部署
### 使用 Docker Compose 部署(推荐)
```shell
# 下载项目
git clone https://github.com/Calcium-Ion/new-api.git
cd new-api
# 按需编辑 docker-compose.yml
# 启动
docker-compose up -d
```
### 直接使用 Docker 镜像
```shell
# 使用 SQLite 的部署命令:
docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest
@@ -125,6 +136,7 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
![796df8d287b7b7bd7853b2497e7df511](https://github.com/user-attachments/assets/255b5e97-2d3a-4434-b4fa-e922ad88ff5a)
![image](https://github.com/Calcium-Ion/new-api/assets/61247483/ad0e7aae-0203-471c-9716-2d83768927d4)
![image](https://github.com/user-attachments/assets/29f81de5-33fc-4fc5-a5ff-f9b54b653c7c)
![image](https://github.com/Calcium-Ion/new-api/assets/61247483/3ca0b282-00ff-4c96-bf9d-e29ef615c605)
夜间模式

View File

@@ -254,7 +254,7 @@ var ChannelBaseURLs = []string{
"https://open.bigmodel.cn", // 16
"https://dashscope.aliyuncs.com", // 17
"", // 18
"https://ai.360.cn", // 19
"https://api.360.cn", // 19
"https://openrouter.ai/api", // 20
"https://api.aiproxy.io", // 21
"https://fastgpt.run/api/openapi", // 22

View File

@@ -150,6 +150,7 @@ var defaultModelRatio = map[string]float64{
"360gpt-turbo": 0.0858, // ¥0.0012 / 1k tokens
"360gpt-turbo-responsibility-8k": 0.8572, // ¥0.012 / 1k tokens
"360gpt-pro": 0.8572, // ¥0.012 / 1k tokens
"360gpt2-pro": 0.8572, // ¥0.012 / 1k tokens
"embedding-bert-512-v1": 0.0715, // ¥0.001 / 1k tokens
"embedding_s1_v1": 0.0715, // ¥0.001 / 1k tokens
"semantic_similarity_s1_v1": 0.0715, // ¥0.001 / 1k tokens

View File

@@ -7,7 +7,7 @@ import (
"strings"
)
var StreamingTimeout = common.GetEnvOrDefault("STREAMING_TIMEOUT", 30)
var StreamingTimeout = common.GetEnvOrDefault("STREAMING_TIMEOUT", 60)
var DifyDebug = common.GetEnvOrDefaultBool("DIFY_DEBUG", true)
// ForceStreamOption 覆盖请求参数强制返回usage信息
@@ -20,7 +20,7 @@ var GetMediaTokenNotStream = common.GetEnvOrDefaultBool("GET_MEDIA_TOKEN_NOT_STR
var UpdateTask = common.GetEnvOrDefaultBool("UPDATE_TASK", true)
var GeminiModelMap = map[string]string{
"gemini-1.0-pro": "v1",
"gemini-1.0-pro": "v1",
}
func InitEnv() {

View File

@@ -3,12 +3,13 @@ package controller
import (
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"one-api/common"
"one-api/model"
"strconv"
"strings"
"github.com/gin-gonic/gin"
)
type OpenAIModel struct {
@@ -48,19 +49,41 @@ func GetAllChannels(c *gin.Context) {
if pageSize < 0 {
pageSize = common.ItemsPerPage
}
channelData := make([]*model.Channel, 0)
idSort, _ := strconv.ParseBool(c.Query("id_sort"))
channels, err := model.GetAllChannels(p*pageSize, pageSize, false, idSort)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
enableTagMode, _ := strconv.ParseBool(c.Query("tag_mode"))
if enableTagMode {
tags, err := model.GetPaginatedTags(p*pageSize, pageSize)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
for _, tag := range tags {
if tag != nil && *tag != "" {
tagChannel, err := model.GetChannelsByTag(*tag)
if err == nil {
channelData = append(channelData, tagChannel...)
}
}
}
} else {
channels, err := model.GetAllChannels(p*pageSize, pageSize, false, idSort)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
channelData = channels
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": channels,
"data": channelData,
})
return
}
@@ -144,8 +167,8 @@ func SearchChannels(c *gin.Context) {
keyword := c.Query("keyword")
group := c.Query("group")
modelKeyword := c.Query("model")
//idSort, _ := strconv.ParseBool(c.Query("id_sort"))
channels, err := model.SearchChannels(keyword, group, modelKeyword)
idSort, _ := strconv.ParseBool(c.Query("id_sort"))
channels, err := model.SearchChannels(keyword, group, modelKeyword, idSort)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
@@ -279,6 +302,98 @@ func DeleteDisabledChannel(c *gin.Context) {
return
}
type ChannelTag struct {
Tag string `json:"tag"`
NewTag *string `json:"new_tag"`
Priority *int64 `json:"priority"`
Weight *uint `json:"weight"`
ModelMapping *string `json:"model_mapping"`
Models *string `json:"models"`
Groups *string `json:"groups"`
}
func DisableTagChannels(c *gin.Context) {
channelTag := ChannelTag{}
err := c.ShouldBindJSON(&channelTag)
if err != nil || channelTag.Tag == "" {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "参数错误",
})
return
}
err = model.DisableChannelByTag(channelTag.Tag)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
})
return
}
func EnableTagChannels(c *gin.Context) {
channelTag := ChannelTag{}
err := c.ShouldBindJSON(&channelTag)
if err != nil || channelTag.Tag == "" {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "参数错误",
})
return
}
err = model.EnableChannelByTag(channelTag.Tag)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
})
return
}
func EditTagChannels(c *gin.Context) {
channelTag := ChannelTag{}
err := c.ShouldBindJSON(&channelTag)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "参数错误",
})
return
}
if channelTag.Tag == "" {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "tag不能为空",
})
return
}
err = model.EditChannelByTag(channelTag.Tag, channelTag.NewTag, channelTag.ModelMapping, channelTag.Models, channelTag.Groups, channelTag.Priority, channelTag.Weight)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
})
return
}
type ChannelBatch struct {
Ids []int `json:"ids"`
}

View File

@@ -14,8 +14,8 @@ services:
environment:
- SQL_DSN=root:123456@tcp(mysql:3306)/new-api # Point to the mysql service
- REDIS_CONN_STRING=redis://redis
- SESSION_SECRET=random_string # 修改为随机字符串
- TZ=Asia/Shanghai
# - SESSION_SECRET=random_string # 多机部署时设置,必须修改这个随机字符串!!!!!!!
# - NODE_TYPE=slave # Uncomment for slave node in multi-node deployment
# - SYNC_FREQUENCY=60 # Uncomment if regular database syncing is needed
# - FRONTEND_BASE_URL=https://openai.justsong.cn # Uncomment for multi-node deployment with front-end URL
@@ -43,8 +43,8 @@ services:
MYSQL_DATABASE: new-api
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306" # If you want to access MySQL from outside Docker, uncomment
# ports:
# - "3306:3306" # If you want to access MySQL from outside Docker, uncomment
volumes:
mysql_data:

View File

@@ -212,6 +212,7 @@ func TokenAuth() func(c *gin.Context) {
}
c.Set("id", token.UserId)
c.Set("token_id", token.Id)
c.Set("token_key", token.Key)
c.Set("token_name", token.Name)
c.Set("token_unlimited_quota", token.UnlimitedQuota)
if !token.UnlimitedQuota {

View File

@@ -10,12 +10,13 @@ import (
)
type Ability struct {
Group string `json:"group" gorm:"type:varchar(64);primaryKey;autoIncrement:false"`
Model string `json:"model" gorm:"type:varchar(64);primaryKey;autoIncrement:false"`
ChannelId int `json:"channel_id" gorm:"primaryKey;autoIncrement:false;index"`
Enabled bool `json:"enabled"`
Priority *int64 `json:"priority" gorm:"bigint;default:0;index"`
Weight uint `json:"weight" gorm:"default:0;index"`
Group string `json:"group" gorm:"type:varchar(64);primaryKey;autoIncrement:false"`
Model string `json:"model" gorm:"type:varchar(64);primaryKey;autoIncrement:false"`
ChannelId int `json:"channel_id" gorm:"primaryKey;autoIncrement:false;index"`
Enabled bool `json:"enabled"`
Priority *int64 `json:"priority" gorm:"bigint;default:0;index"`
Weight uint `json:"weight" gorm:"default:0;index"`
Tag *string `json:"tag" gorm:"index"`
}
func GetGroupModels(group string) []string {
@@ -149,6 +150,7 @@ func (channel *Channel) AddAbilities() error {
Enabled: channel.Status == common.ChannelStatusEnabled,
Priority: channel.Priority,
Weight: uint(channel.GetWeight()),
Tag: channel.Tag,
}
abilities = append(abilities, ability)
}
@@ -190,6 +192,24 @@ func UpdateAbilityStatus(channelId int, status bool) error {
return DB.Model(&Ability{}).Where("channel_id = ?", channelId).Select("enabled").Update("enabled", status).Error
}
func UpdateAbilityStatusByTag(tag string, status bool) error {
return DB.Model(&Ability{}).Where("tag = ?", tag).Select("enabled").Update("enabled", status).Error
}
func UpdateAbilityByTag(tag string, newTag *string, priority *int64, weight *uint) error {
ability := Ability{}
if newTag != nil {
ability.Tag = newTag
}
if priority != nil {
ability.Priority = priority
}
if weight != nil {
ability.Weight = *weight
}
return DB.Model(&Ability{}).Where("tag = ?", tag).Updates(ability).Error
}
func FixAbility() (int, error) {
var channelIds []int
count := 0

View File

@@ -2,9 +2,10 @@ package model
import (
"encoding/json"
"gorm.io/gorm"
"one-api/common"
"strings"
"gorm.io/gorm"
)
type Channel struct {
@@ -32,6 +33,7 @@ type Channel struct {
Priority *int64 `json:"priority" gorm:"bigint;default:0"`
AutoBan *int `json:"auto_ban" gorm:"default:1"`
OtherInfo string `json:"other_info"`
Tag *string `json:"tag" gorm:"index"`
}
func (channel *Channel) GetModels() []string {
@@ -61,6 +63,17 @@ func (channel *Channel) SetOtherInfo(otherInfo map[string]interface{}) {
channel.OtherInfo = string(otherInfoBytes)
}
func (channel *Channel) GetTag() string {
if channel.Tag == nil {
return ""
}
return *channel.Tag
}
func (channel *Channel) SetTag(tag string) {
channel.Tag = &tag
}
func (channel *Channel) GetAutoBan() bool {
if channel.AutoBan == nil {
return false
@@ -87,7 +100,13 @@ func GetAllChannels(startIdx int, num int, selectAll bool, idSort bool) ([]*Chan
return channels, err
}
func SearchChannels(keyword string, group string, model string) ([]*Channel, error) {
func GetChannelsByTag(tag string) ([]*Channel, error) {
var channels []*Channel
err := DB.Where("tag = ?", tag).Find(&channels).Error
return channels, err
}
func SearchChannels(keyword string, group string, model string, idSort bool) ([]*Channel, error) {
var channels []*Channel
keyCol := "`key`"
groupCol := "`group`"
@@ -100,6 +119,11 @@ func SearchChannels(keyword string, group string, model string) ([]*Channel, err
modelsCol = `"models"`
}
order := "priority desc"
if idSort {
order = "id desc"
}
// 构造基础查询
baseQuery := DB.Model(&Channel{}).Omit(keyCol)
@@ -122,7 +146,7 @@ func SearchChannels(keyword string, group string, model string) ([]*Channel, err
}
// 执行查询
err := baseQuery.Where(whereClause, args...).Order("priority desc").Find(&channels).Error
err := baseQuery.Where(whereClause, args...).Order(order).Find(&channels).Error
if err != nil {
return nil, err
}
@@ -288,6 +312,74 @@ func UpdateChannelStatusById(id int, status int, reason string) {
}
func EnableChannelByTag(tag string) error {
err := DB.Model(&Channel{}).Where("tag = ?", tag).Update("status", common.ChannelStatusEnabled).Error
if err != nil {
return err
}
err = UpdateAbilityStatusByTag(tag, true)
return err
}
func DisableChannelByTag(tag string) error {
err := DB.Model(&Channel{}).Where("tag = ?", tag).Update("status", common.ChannelStatusManuallyDisabled).Error
if err != nil {
return err
}
err = UpdateAbilityStatusByTag(tag, false)
return err
}
func EditChannelByTag(tag string, newTag *string, modelMapping *string, models *string, group *string, priority *int64, weight *uint) error {
updateData := Channel{}
shouldReCreateAbilities := false
updatedTag := tag
// 如果 newTag 不为空且不等于 tag则更新 tag
if newTag != nil && *newTag != tag {
updateData.Tag = newTag
updatedTag = *newTag
}
if modelMapping != nil && *modelMapping != "" {
updateData.ModelMapping = modelMapping
}
if models != nil && *models != "" {
shouldReCreateAbilities = true
updateData.Models = *models
}
if group != nil && *group != "" {
shouldReCreateAbilities = true
updateData.Group = *group
}
if priority != nil {
updateData.Priority = priority
}
if weight != nil {
updateData.Weight = weight
}
err := DB.Model(&Channel{}).Where("tag = ?", tag).Updates(updateData).Error
if err != nil {
return err
}
if shouldReCreateAbilities {
channels, err := GetChannelsByTag(updatedTag)
if err == nil {
for _, channel := range channels {
err = channel.UpdateAbilities()
if err != nil {
common.SysError("failed to update abilities: " + err.Error())
}
}
}
} else {
err := UpdateAbilityByTag(tag, newTag, priority, weight)
if err != nil {
return err
}
}
return nil
}
func UpdateChannelUsedQuota(id int, quota int) {
if common.BatchUpdateEnabled {
addNewRecord(BatchUpdateTypeChannelUsedQuota, id, quota)
@@ -312,3 +404,9 @@ func DeleteDisabledChannel() (int64, error) {
result := DB.Where("status = ? or status = ?", common.ChannelStatusAutoDisabled, common.ChannelStatusManuallyDisabled).Delete(&Channel{})
return result.RowsAffected, result.Error
}
func GetPaginatedTags(offset int, limit int) ([]*string, error) {
var tags []*string
err := DB.Model(&Channel{}).Select("DISTINCT tag").Where("tag != ''").Offset(offset).Limit(limit).Find(&tags).Error
return tags, err
}

View File

@@ -4,6 +4,7 @@ var ModelList = []string{
"360gpt-turbo",
"360gpt-turbo-responsibility-8k",
"360gpt-pro",
"360gpt2-pro",
"360GPT_S2_V9",
"embedding-bert-512-v1",
"embedding_s1_v1",

View File

@@ -9,6 +9,7 @@ var awsModelIDMap = map[string]string{
"claude-3-haiku-20240307": "anthropic.claude-3-haiku-20240307-v1:0",
"claude-3-5-sonnet-20240620": "anthropic.claude-3-5-sonnet-20240620-v1:0",
"claude-3-5-sonnet-20241022": "anthropic.claude-3-5-sonnet-20241022-v2:0",
"claude-3-5-haiku-20241022": "anthropic.claude-3-5-haiku-20241022-v1:0",
}
var ChannelName = "aws"

View File

@@ -32,11 +32,15 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
if info.RelayMode == constant.RelayModeRealtime {
// trim https
baseUrl := strings.TrimPrefix(info.BaseUrl, "https://")
baseUrl = strings.TrimPrefix(baseUrl, "http://")
baseUrl = "wss://" + baseUrl
info.BaseUrl = baseUrl
if strings.HasPrefix(info.BaseUrl, "https://") {
baseUrl := strings.TrimPrefix(info.BaseUrl, "https://")
baseUrl = "wss://" + baseUrl
info.BaseUrl = baseUrl
} else if strings.HasPrefix(info.BaseUrl, "http://") {
baseUrl := strings.TrimPrefix(info.BaseUrl, "http://")
baseUrl = "ws://" + baseUrl
info.BaseUrl = baseUrl
}
}
switch info.ChannelType {
case common.ChannelTypeAzure:

View File

@@ -98,6 +98,11 @@ func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
shouldSendLastResp = false
}
}
for _, choice := range lastStreamResponse.Choices {
if choice.FinishReason != nil {
shouldSendLastResp = true
}
}
}
if shouldSendLastResp {
service.StringData(c, lastStreamData)

View File

@@ -14,6 +14,7 @@ type RelayInfo struct {
ChannelType int
ChannelId int
TokenId int
TokenKey string
UserId int
Group string
TokenUnlimited bool
@@ -58,6 +59,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
channelId := c.GetInt("channel_id")
tokenId := c.GetInt("token_id")
tokenKey := c.GetString("token_key")
userId := c.GetInt("id")
group := c.GetString("group")
tokenUnlimited := c.GetBool("token_unlimited_quota")
@@ -73,6 +75,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
ChannelType: channelType,
ChannelId: channelId,
TokenId: tokenId,
TokenKey: tokenKey,
UserId: userId,
Group: group,
TokenUnlimited: tokenUnlimited,

View File

@@ -2,11 +2,9 @@ package relay
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/bytedance/sonic"
"io"
"math"
"net/http"
@@ -20,6 +18,8 @@ import (
"strings"
"time"
"github.com/bytedance/sonic"
"github.com/gin-gonic/gin"
)
@@ -36,7 +36,7 @@ func getAndValidateTextRequest(c *gin.Context, relayInfo *relaycommon.RelayInfo)
textRequest.Model = c.Param("model")
}
if textRequest.MaxTokens < 0 || textRequest.MaxTokens > math.MaxInt32/2 {
if textRequest.MaxTokens > math.MaxInt32/2 {
return nil, errors.New("max_tokens is invalid")
}
if textRequest.Model == "" {
@@ -48,12 +48,12 @@ func getAndValidateTextRequest(c *gin.Context, relayInfo *relaycommon.RelayInfo)
return nil, errors.New("field prompt is required")
}
case relayconstant.RelayModeChatCompletions:
if textRequest.Messages == nil || len(textRequest.Messages) == 0 {
if len(textRequest.Messages) == 0 {
return nil, errors.New("field messages is required")
}
case relayconstant.RelayModeEmbeddings:
case relayconstant.RelayModeModerations:
if textRequest.Input == "" || textRequest.Input == nil {
if textRequest.Input == nil || textRequest.Input == "" {
return nil, errors.New("field input is required")
}
case relayconstant.RelayModeEdits:
@@ -264,7 +264,7 @@ func preConsumeQuota(c *gin.Context, preConsumedQuota int, relayInfo *relaycommo
return 0, 0, service.OpenAIErrorWrapperLocal(errors.New("user quota is not enough"), "insufficient_user_quota", http.StatusForbidden)
}
if userQuota-preConsumedQuota < 0 {
return 0, 0, service.OpenAIErrorWrapperLocal(errors.New(fmt.Sprintf("chat pre-consumed quota failed, user quota: %d, need quota: %d", userQuota, preConsumedQuota)), "insufficient_user_quota", http.StatusBadRequest)
return 0, 0, service.OpenAIErrorWrapperLocal(fmt.Errorf("chat pre-consumed quota failed, user quota: %d, need quota: %d", userQuota, preConsumedQuota), "insufficient_user_quota", http.StatusBadRequest)
}
err = model.CacheDecreaseUserQuota(relayInfo.UserId, preConsumedQuota)
if err != nil {
@@ -298,13 +298,14 @@ func preConsumeQuota(c *gin.Context, preConsumedQuota int, relayInfo *relaycommo
func returnPreConsumedQuota(c *gin.Context, relayInfo *relaycommon.RelayInfo, userQuota int, preConsumedQuota int) {
if preConsumedQuota != 0 {
go func(ctx context.Context) {
// return pre-consumed quota
err := model.PostConsumeTokenQuota(relayInfo, userQuota, -preConsumedQuota, 0, false)
go func() {
relayInfoCopy := *relayInfo
err := model.PostConsumeTokenQuota(&relayInfoCopy, userQuota, -preConsumedQuota, 0, false)
if err != nil {
common.SysError("error return pre-consumed quota: " + err.Error())
}
}(c)
}()
}
}

View File

@@ -91,6 +91,9 @@ func SetApiRouter(router *gin.Engine) {
channelRoute.POST("/", controller.AddChannel)
channelRoute.PUT("/", controller.UpdateChannel)
channelRoute.DELETE("/disabled", controller.DeleteDisabledChannel)
channelRoute.POST("/tag/disabled", controller.DisableTagChannels)
channelRoute.POST("/tag/enabled", controller.EnableTagChannels)
channelRoute.PUT("/tag", controller.EditTagChannels)
channelRoute.DELETE("/:id", controller.DeleteChannel)
channelRoute.POST("/batch", controller.DeleteChannelBatch)
channelRoute.POST("/fix", controller.FixChannelsAbilities)

View File

@@ -22,7 +22,7 @@ func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usag
return err
}
token, err := model.CacheGetTokenByKey(strings.TrimLeft(relayInfo.ApiKey, "sk-"))
token, err := model.CacheGetTokenByKey(strings.TrimLeft(relayInfo.TokenKey, "sk-"))
if err != nil {
return err
}
@@ -53,7 +53,7 @@ func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usag
return errors.New(fmt.Sprintf("用户额度不足,剩余额度为 %d", userQuota))
}
if token.RemainQuota < quota {
if !token.UnlimitedQuota && token.RemainQuota < quota {
return errors.New(fmt.Sprintf("令牌额度不足,剩余额度为 %d", token.RemainQuota))
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
import { Input, Typography } from '@douyinfe/semi-ui';
import React from 'react';
const TextInput = ({ label, name, value, onChange, placeholder, type = 'text' }) => {
return (
<>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>{label}</Typography.Text>
</div>
<Input
name={name}
placeholder={placeholder}
onChange={(value) => onChange(value)}
value={value}
autoComplete="new-password"
/>
</>
);
}
export default TextInput;

View File

@@ -0,0 +1,21 @@
import { Input, InputNumber, Typography } from '@douyinfe/semi-ui';
import React from 'react';
const TextNumberInput = ({ label, name, value, onChange, placeholder }) => {
return (
<>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>{label}</Typography.Text>
</div>
<InputNumber
name={name}
placeholder={placeholder}
onChange={(value) => onChange(value)}
value={value}
autoComplete="new-password"
/>
</>
);
}
export default TextNumberInput;

View File

@@ -6,7 +6,8 @@ export let API = axios.create({
? import.meta.env.VITE_REACT_APP_SERVER_URL
: '',
headers: {
'New-API-User': getUserIdFromLocalStorage()
'New-API-User': getUserIdFromLocalStorage(),
'Cache-Control': 'no-store'
}
});
@@ -16,7 +17,8 @@ export function updateAPI() {
? import.meta.env.VITE_REACT_APP_SERVER_URL
: '',
headers: {
'New-API-User': getUserIdFromLocalStorage()
'New-API-User': getUserIdFromLocalStorage(),
'Cache-Control': 'no-store'
}
});
}

View File

@@ -67,6 +67,8 @@ export function renderQuotaNumberWithDigit(num, digits = 2) {
}
export function renderNumberWithPoint(num) {
if (num === undefined)
return '';
num = num.toFixed(2);
if (num >= 100000) {
// Convert number to string to manipulate it

View File

@@ -6,7 +6,7 @@ import {
showError,
showInfo,
showSuccess,
verifyJSON,
verifyJSON
} from '../../helpers';
import { CHANNEL_OPTIONS } from '../../constants';
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
@@ -21,28 +21,26 @@ import {
Select,
TextArea,
Checkbox,
Banner,
Banner
} from '@douyinfe/semi-ui';
import { Divider } from 'semantic-ui-react';
import { getChannelModels, loadChannelModels } from '../../components/utils.js';
import axios from 'axios';
const MODEL_MAPPING_EXAMPLE = {
'gpt-3.5-turbo-0301': 'gpt-3.5-turbo',
'gpt-4-0314': 'gpt-4',
'gpt-4-32k-0314': 'gpt-4-32k',
'gpt-3.5-turbo': 'gpt-3.5-turbo-0125'
};
const STATUS_CODE_MAPPING_EXAMPLE = {
400: '500',
400: '500'
};
const REGION_EXAMPLE = {
"default": "us-central1",
"claude-3-5-sonnet-20240620": "europe-west1"
}
'default': 'us-central1',
'claude-3-5-sonnet-20240620': 'europe-west1'
};
const fetchButtonTips = "1. 新建渠道时请求通过当前浏览器发出2. 编辑已有渠道,请求通过后端服务器发出"
const fetchButtonTips = '1. 新建渠道时请求通过当前浏览器发出2. 编辑已有渠道,请求通过后端服务器发出';
function type2secretPrompt(type) {
// inputs.type === 15 ? '按照如下格式输入APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
@@ -84,6 +82,9 @@ const EditChannel = (props) => {
auto_ban: 1,
test_model: '',
groups: ['default'],
priority: 0,
weight: 0,
tag: ''
};
const [batch, setBatch] = useState(false);
const [autoBan, setAutoBan] = useState(true);
@@ -108,7 +109,7 @@ const EditChannel = (props) => {
'mj_blend',
'mj_upscale',
'mj_describe',
'mj_uploads',
'mj_uploads'
];
break;
case 5:
@@ -128,13 +129,13 @@ const EditChannel = (props) => {
'mj_high_variation',
'mj_low_variation',
'mj_pan',
'mj_uploads',
'mj_uploads'
];
break;
case 36:
localModels = [
'suno_music',
'suno_lyrics',
'suno_lyrics'
];
break;
default:
@@ -171,7 +172,7 @@ const EditChannel = (props) => {
data.model_mapping = JSON.stringify(
JSON.parse(data.model_mapping),
null,
2,
2
);
}
setInputs(data);
@@ -190,70 +191,69 @@ const EditChannel = (props) => {
const fetchUpstreamModelList = async (name) => {
if (inputs["type"] !== 1) {
showError("仅支持 OpenAI 接口格式")
if (inputs['type'] !== 1) {
showError('仅支持 OpenAI 接口格式');
return;
}
setLoading(true)
const models = inputs["models"] || []
setLoading(true);
const models = inputs['models'] || [];
let err = false;
if (isEdit) {
const res = await API.get("/api/channel/fetch_models/" + channelId)
const res = await API.get('/api/channel/fetch_models/' + channelId);
if (res.data && res.data?.success) {
models.push(...res.data.data)
models.push(...res.data.data);
} else {
err = true
err = true;
}
} else {
if (!inputs?.["key"]) {
showError("请填写密钥")
err = true
if (!inputs?.['key']) {
showError('请填写密钥');
err = true;
} else {
try {
const host = new URL((inputs["base_url"] || "https://api.openai.com"))
const host = new URL((inputs['base_url'] || 'https://api.openai.com'));
const url = `https://${host.hostname}/v1/models`;
const key = inputs["key"];
const key = inputs['key'];
const res = await axios.get(url, {
headers: {
'Authorization': `Bearer ${key}`
}
})
});
if (res.data && res.data?.success) {
models.push(...res.data.data.map((model) => model.id))
models.push(...res.data.data.map((model) => model.id));
} else {
err = true
err = true;
}
}
catch (error) {
err = true
} catch (error) {
err = true;
}
}
}
if (!err) {
handleInputChange(name, Array.from(new Set(models)));
showSuccess("获取模型列表成功");
showSuccess('获取模型列表成功');
} else {
showError('获取模型列表失败');
}
setLoading(false);
}
};
const fetchModels = async () => {
try {
let res = await API.get(`/api/channel/models`);
let localModelOptions = res.data.data.map((model) => ({
label: model.id,
value: model.id,
value: model.id
}));
setOriginModelOptions(localModelOptions);
setFullModels(res.data.data.map((model) => model.id));
setBasicModels(
res.data.data
.filter((model) => {
return model.id.startsWith('gpt-3') || model.id.startsWith('text-');
return model.id.startsWith('gpt-') || model.id.startsWith('text-');
})
.map((model) => model.id),
.map((model) => model.id)
);
} catch (error) {
showError(error.message);
@@ -269,8 +269,8 @@ const EditChannel = (props) => {
setGroupOptions(
res.data.data.map((group) => ({
label: group,
value: group,
})),
value: group
}))
);
} catch (error) {
showError(error.message);
@@ -280,10 +280,10 @@ const EditChannel = (props) => {
useEffect(() => {
let localModelOptions = [...originModelOptions];
inputs.models.forEach((model) => {
if (!localModelOptions.find((option) => option.key === model)) {
if (!localModelOptions.find((option) => option.label === model)) {
localModelOptions.push({
label: model,
value: model,
value: model
});
}
});
@@ -320,7 +320,7 @@ const EditChannel = (props) => {
if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
localInputs.base_url = localInputs.base_url.slice(
0,
localInputs.base_url.length - 1,
localInputs.base_url.length - 1
);
}
if (localInputs.type === 3 && localInputs.other === '') {
@@ -341,7 +341,7 @@ const EditChannel = (props) => {
if (isEdit) {
res = await API.put(`/api/channel/`, {
...localInputs,
id: parseInt(channelId),
id: parseInt(channelId)
});
} else {
res = await API.post(`/api/channel/`, localInputs);
@@ -378,7 +378,7 @@ const EditChannel = (props) => {
// 添加到下拉选项
key: model,
text: model,
value: model,
value: model
});
} else if (model) {
showError('某些模型已存在!');
@@ -409,11 +409,11 @@ const EditChannel = (props) => {
footer={
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Space>
<Button theme='solid' size={'large'} onClick={submit}>
<Button theme="solid" size={'large'} onClick={submit}>
提交
</Button>
<Button
theme='solid'
theme="solid"
size={'large'}
type={'tertiary'}
onClick={handleCancel}
@@ -432,7 +432,7 @@ const EditChannel = (props) => {
<Typography.Text strong>类型</Typography.Text>
</div>
<Select
name='type'
name="type"
required
optionList={CHANNEL_OPTIONS}
value={inputs.type}
@@ -450,8 +450,8 @@ const EditChannel = (props) => {
因为 One API 会把请求体中的 model
参数替换为你的部署名称模型名称中的点会被剔除
<a
target='_blank'
href='https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271'
target="_blank"
href="https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271"
>
图片演示
</a>
@@ -466,8 +466,8 @@ const EditChannel = (props) => {
</Typography.Text>
</div>
<Input
label='AZURE_OPENAI_ENDPOINT'
name='azure_base_url'
label="AZURE_OPENAI_ENDPOINT"
name="azure_base_url"
placeholder={
'请输入 AZURE_OPENAI_ENDPOINT例如https://docs-test-001.openai.azure.com'
}
@@ -475,14 +475,14 @@ const EditChannel = (props) => {
handleInputChange('base_url', value);
}}
value={inputs.base_url}
autoComplete='new-password'
autoComplete="new-password"
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>默认 API 版本</Typography.Text>
</div>
<Input
label='默认 API 版本'
name='azure_other'
label="默认 API 版本"
name="azure_other"
placeholder={
'请输入默认 API 版本例如2023-06-01-preview该配置可以被实际的请求查询参数所覆盖'
}
@@ -490,7 +490,7 @@ const EditChannel = (props) => {
handleInputChange('other', value);
}}
value={inputs.other}
autoComplete='new-password'
autoComplete="new-password"
/>
</>
)}
@@ -512,7 +512,7 @@ const EditChannel = (props) => {
</Typography.Text>
</div>
<Input
name='base_url'
name="base_url"
placeholder={
'请输入完整的URL例如https://api.openai.com/v1/chat/completions'
}
@@ -520,49 +520,84 @@ const EditChannel = (props) => {
handleInputChange('base_url', value);
}}
value={inputs.base_url}
autoComplete='new-password'
autoComplete="new-password"
/>
</>
)}
{inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && (
<>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>代理</Typography.Text>
</div>
<Input
label="代理"
name="base_url"
placeholder={'此项可选,用于通过代理站来进行 API 调用'}
onChange={(value) => {
handleInputChange('base_url', value);
}}
value={inputs.base_url}
autoComplete="new-password"
/>
</>
)}
{inputs.type === 22 && (
<>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>私有部署地址</Typography.Text>
</div>
<Input
name="base_url"
placeholder={
'请输入私有部署地址格式为https://fastgpt.run/api/openapi'
}
onChange={(value) => {
handleInputChange('base_url', value);
}}
value={inputs.base_url}
autoComplete="new-password"
/>
</>
)}
{inputs.type === 36 && (
<>
<div style={{marginTop: 10}}>
<Typography.Text strong>
注意非Chat API请务必填写正确的API地址否则可能导致无法使用
</Typography.Text>
</div>
<Input
name='base_url'
placeholder={
'请输入到 /suno 前的路径通常就是域名例如https://api.example.com '
}
onChange={(value) => {
handleInputChange('base_url', value);
}}
value={inputs.base_url}
autoComplete='new-password'
/>
</>
<>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>
注意非Chat API请务必填写正确的API地址否则可能导致无法使用
</Typography.Text>
</div>
<Input
name="base_url"
placeholder={
'请输入到 /suno 前的路径通常就是域名例如https://api.example.com '
}
onChange={(value) => {
handleInputChange('base_url', value);
}}
value={inputs.base_url}
autoComplete="new-password"
/>
</>
)}
<div style={{marginTop: 10}}>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>名称</Typography.Text>
</div>
<Input
required
name='name'
required
name="name"
placeholder={'请为渠道命名'}
onChange={(value) => {
handleInputChange('name', value);
}}
value={inputs.name}
autoComplete='new-password'
autoComplete="new-password"
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>分组</Typography.Text>
</div>
<Select
placeholder={'请选择可以使用该渠道的分组'}
name='groups'
name="groups"
required
multiple
selection
@@ -572,7 +607,7 @@ const EditChannel = (props) => {
handleInputChange('groups', value);
}}
value={inputs.groups}
autoComplete='new-password'
autoComplete="new-password"
optionList={groupOptions}
/>
{inputs.type === 18 && (
@@ -581,7 +616,7 @@ const EditChannel = (props) => {
<Typography.Text strong>模型版本</Typography.Text>
</div>
<Input
name='other'
name="other"
placeholder={
'请输入星火大模型版本注意是接口地址中的版本号例如v2.1'
}
@@ -589,7 +624,7 @@ const EditChannel = (props) => {
handleInputChange('other', value);
}}
value={inputs.other}
autoComplete='new-password'
autoComplete="new-password"
/>
</>
)}
@@ -599,7 +634,7 @@ const EditChannel = (props) => {
<Typography.Text strong>部署地区</Typography.Text>
</div>
<TextArea
name='other'
name="other"
placeholder={
'请输入部署地区例如us-central1\n支持使用模型映射格式\n' +
'{\n' +
@@ -612,18 +647,18 @@ const EditChannel = (props) => {
handleInputChange('other', value);
}}
value={inputs.other}
autoComplete='new-password'
autoComplete="new-password"
/>
<Typography.Text
style={{
color: 'rgba(var(--semi-blue-5), 1)',
userSelect: 'none',
cursor: 'pointer',
cursor: 'pointer'
}}
onClick={() => {
handleInputChange(
'other',
JSON.stringify(REGION_EXAMPLE, null, 2),
JSON.stringify(REGION_EXAMPLE, null, 2)
);
}}
>
@@ -637,14 +672,14 @@ const EditChannel = (props) => {
<Typography.Text strong>知识库 ID</Typography.Text>
</div>
<Input
label='知识库 ID'
name='other'
label="知识库 ID"
name="other"
placeholder={'请输入知识库 ID例如123456'}
onChange={(value) => {
handleInputChange('other', value);
}}
value={inputs.other}
autoComplete='new-password'
autoComplete="new-password"
/>
</>
)}
@@ -654,7 +689,7 @@ const EditChannel = (props) => {
<Typography.Text strong>Account ID</Typography.Text>
</div>
<Input
name='other'
name="other"
placeholder={
'请输入Account ID例如d6b5da8hk1awo8nap34ube6gh'
}
@@ -662,7 +697,7 @@ const EditChannel = (props) => {
handleInputChange('other', value);
}}
value={inputs.other}
autoComplete='new-password'
autoComplete="new-password"
/>
</>
)}
@@ -671,7 +706,7 @@ const EditChannel = (props) => {
</div>
<Select
placeholder={'请选择该渠道所支持的模型'}
name='models'
name="models"
required
multiple
selection
@@ -679,13 +714,13 @@ const EditChannel = (props) => {
handleInputChange('models', value);
}}
value={inputs.models}
autoComplete='new-password'
autoComplete="new-password"
optionList={modelOptions}
/>
<div style={{ lineHeight: '40px', marginBottom: '12px' }}>
<Space>
<Button
type='primary'
type="primary"
onClick={() => {
handleInputChange('models', basicModels);
}}
@@ -693,7 +728,7 @@ const EditChannel = (props) => {
填入相关模型
</Button>
<Button
type='secondary'
type="secondary"
onClick={() => {
handleInputChange('models', fullModels);
}}
@@ -702,7 +737,7 @@ const EditChannel = (props) => {
</Button>
<Tooltip content={fetchButtonTips}>
<Button
type='tertiary'
type="tertiary"
onClick={() => {
fetchUpstreamModelList('models');
}}
@@ -711,7 +746,7 @@ const EditChannel = (props) => {
</Button>
</Tooltip>
<Button
type='warning'
type="warning"
onClick={() => {
handleInputChange('models', []);
}}
@@ -721,11 +756,11 @@ const EditChannel = (props) => {
</Space>
<Input
addonAfter={
<Button type='primary' onClick={addCustomModels}>
<Button type="primary" onClick={addCustomModels}>
填入
</Button>
}
placeholder='输入自定义模型名称'
placeholder="输入自定义模型名称"
value={customModel}
onChange={(value) => {
setCustomModel(value.trim());
@@ -737,24 +772,24 @@ const EditChannel = (props) => {
</div>
<TextArea
placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
name='model_mapping'
name="model_mapping"
onChange={(value) => {
handleInputChange('model_mapping', value);
}}
autosize
value={inputs.model_mapping}
autoComplete='new-password'
autoComplete="new-password"
/>
<Typography.Text
style={{
color: 'rgba(var(--semi-blue-5), 1)',
userSelect: 'none',
cursor: 'pointer',
cursor: 'pointer'
}}
onClick={() => {
handleInputChange(
'model_mapping',
JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2),
JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)
);
}}
>
@@ -765,8 +800,8 @@ const EditChannel = (props) => {
</div>
{batch ? (
<TextArea
label='密钥'
name='key'
label="密钥"
name="key"
required
placeholder={'请输入密钥,一行一个'}
onChange={(value) => {
@@ -774,14 +809,14 @@ const EditChannel = (props) => {
}}
value={inputs.key}
style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
autoComplete='new-password'
autoComplete="new-password"
/>
) : (
<>
{inputs.type === 41 ? (
<TextArea
label='鉴权json'
name='key'
label="鉴权json"
name="key"
required
placeholder={'{\n' +
' "type": "service_account",\n' +
@@ -801,23 +836,36 @@ const EditChannel = (props) => {
}}
autosize={{ minRows: 10 }}
value={inputs.key}
autoComplete='new-password'
autoComplete="new-password"
/>
) : (
<Input
label='密钥'
name='key'
label="密钥"
name="key"
required
placeholder={type2secretPrompt(inputs.type)}
onChange={(value) => {
handleInputChange('key', value);
}}
value={inputs.key}
autoComplete='new-password'
autoComplete="new-password"
/>
)
}
</>
</>
)}
{!isEdit && (
<div style={{ marginTop: 10, display: 'flex' }}>
<Space>
<Checkbox
checked={batch}
label="批量创建"
name="batch"
onChange={() => setBatch(!batch)}
/>
<Typography.Text strong>批量创建</Typography.Text>
</Space>
</div>
)}
{inputs.type === 1 && (
<>
@@ -825,9 +873,9 @@ const EditChannel = (props) => {
<Typography.Text strong>组织</Typography.Text>
</div>
<Input
label='组织,可选,不填则为默认组织'
name='openai_organization'
placeholder='请输入组织org-xxx'
label="组织,可选,不填则为默认组织"
name="openai_organization"
placeholder="请输入组织org-xxx"
onChange={(value) => {
handleInputChange('openai_organization', value);
}}
@@ -839,8 +887,8 @@ const EditChannel = (props) => {
<Typography.Text strong>默认测试模型</Typography.Text>
</div>
<Input
name='test_model'
placeholder='不填则为模型列表第一个'
name="test_model"
placeholder="不填则为模型列表第一个"
onChange={(value) => {
handleInputChange('test_model', value);
}}
@@ -849,7 +897,7 @@ const EditChannel = (props) => {
<div style={{ marginTop: 10, display: 'flex' }}>
<Space>
<Checkbox
name='auto_ban'
name="auto_ban"
checked={autoBan}
onChange={() => {
setAutoBan(!autoBan);
@@ -861,55 +909,6 @@ const EditChannel = (props) => {
</Typography.Text>
</Space>
</div>
{!isEdit && (
<div style={{ marginTop: 10, display: 'flex' }}>
<Space>
<Checkbox
checked={batch}
label='批量创建'
name='batch'
onChange={() => setBatch(!batch)}
/>
<Typography.Text strong>批量创建</Typography.Text>
</Space>
</div>
)}
{inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && (
<>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>代理</Typography.Text>
</div>
<Input
label='代理'
name='base_url'
placeholder={'此项可选,用于通过代理站来进行 API 调用'}
onChange={(value) => {
handleInputChange('base_url', value);
}}
value={inputs.base_url}
autoComplete='new-password'
/>
</>
)}
{inputs.type === 22 && (
<>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>私有部署地址</Typography.Text>
</div>
<Input
name='base_url'
placeholder={
'请输入私有部署地址格式为https://fastgpt.run/api/openapi'
}
onChange={(value) => {
handleInputChange('base_url', value);
}}
value={inputs.base_url}
autoComplete='new-password'
/>
</>
)}
<div style={{ marginTop: 10 }}>
<Typography.Text strong>
状态码复写仅影响本地判断不修改返回到上游的状态码
@@ -917,43 +916,74 @@ const EditChannel = (props) => {
</div>
<TextArea
placeholder={`此项可选用于复写返回的状态码比如将claude渠道的400错误复写为500用于重试请勿滥用该功能例如\n${JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)}`}
name='status_code_mapping'
name="status_code_mapping"
onChange={(value) => {
handleInputChange('status_code_mapping', value);
}}
autosize
value={inputs.status_code_mapping}
autoComplete='new-password'
autoComplete="new-password"
/>
<Typography.Text
style={{
color: 'rgba(var(--semi-blue-5), 1)',
userSelect: 'none',
cursor: 'pointer',
cursor: 'pointer'
}}
onClick={() => {
handleInputChange(
'status_code_mapping',
JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2),
JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)
);
}}
>
填入模板
</Typography.Text>
{/*<div style={{ marginTop: 10 }}>*/}
{/* <Typography.Text strong>*/}
{/* 最大请求token0表示不限制*/}
{/* </Typography.Text>*/}
{/*</div>*/}
{/*<Input*/}
{/* label='最大请求token'*/}
{/* name='max_input_tokens'*/}
{/* placeholder='默认为0表示不限制'*/}
{/* onChange={(value) => {*/}
{/* handleInputChange('max_input_tokens', value);*/}
{/* }}*/}
{/* value={inputs.max_input_tokens}*/}
{/*/>*/}
<div style={{ marginTop: 10 }}>
<Typography.Text strong>
渠道标签
</Typography.Text>
</div>
<Input
label="渠道标签"
name="tag"
placeholder={'渠道标签'}
onChange={(value) => {
handleInputChange('tag', value);
}}
value={inputs.tag}
autoComplete="new-password"
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>
渠道优先级
</Typography.Text>
</div>
<Input
label="渠道优先级"
name="priority"
placeholder={'渠道优先级'}
onChange={(value) => {
handleInputChange('priority', parseInt(value));
}}
value={inputs.priority}
autoComplete="new-password"
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>
渠道权重
</Typography.Text>
</div>
<Input
label="渠道权重"
name="weight"
placeholder={'渠道权重'}
onChange={(value) => {
handleInputChange('weight', parseInt(value));
}}
value={inputs.weight}
autoComplete="new-password"
/>
</Spin>
</SideSheet>
</>

View File

@@ -0,0 +1,317 @@
import React, { useState, useEffect } from 'react';
import { API, showError, showInfo, showSuccess, showWarning, verifyJSON } from '../../helpers';
import { SideSheet, Space, Button, Input, Typography, Spin, Modal, Select, Banner, TextArea } from '@douyinfe/semi-ui';
import TextInput from '../../components/custom/TextInput.js';
import { getChannelModels } from '../../components/utils.js';
const MODEL_MAPPING_EXAMPLE = {
'gpt-3.5-turbo': 'gpt-3.5-turbo-0125'
};
const EditTagModal = (props) => {
const { visible, tag, handleClose, refresh } = props;
const [loading, setLoading] = useState(false);
const [originModelOptions, setOriginModelOptions] = useState([]);
const [modelOptions, setModelOptions] = useState([]);
const [groupOptions, setGroupOptions] = useState([]);
const [basicModels, setBasicModels] = useState([]);
const [fullModels, setFullModels] = useState([]);
const originInputs = {
tag: '',
new_tag: null,
model_mapping: null,
groups: [],
models: [],
}
const [inputs, setInputs] = useState(originInputs);
const handleInputChange = (name, value) => {
setInputs((inputs) => ({ ...inputs, [name]: value }));
if (name === 'type') {
let localModels = [];
switch (value) {
case 2:
localModels = [
'mj_imagine',
'mj_variation',
'mj_reroll',
'mj_blend',
'mj_upscale',
'mj_describe',
'mj_uploads'
];
break;
case 5:
localModels = [
'swap_face',
'mj_imagine',
'mj_variation',
'mj_reroll',
'mj_blend',
'mj_upscale',
'mj_describe',
'mj_zoom',
'mj_shorten',
'mj_modal',
'mj_inpaint',
'mj_custom_zoom',
'mj_high_variation',
'mj_low_variation',
'mj_pan',
'mj_uploads'
];
break;
case 36:
localModels = [
'suno_music',
'suno_lyrics'
];
break;
default:
localModels = getChannelModels(value);
break;
}
if (inputs.models.length === 0) {
setInputs((inputs) => ({ ...inputs, models: localModels }));
}
setBasicModels(localModels);
}
};
const fetchModels = async () => {
try {
let res = await API.get(`/api/channel/models`);
let localModelOptions = res.data.data.map((model) => ({
label: model.id,
value: model.id
}));
setOriginModelOptions(localModelOptions);
setFullModels(res.data.data.map((model) => model.id));
setBasicModels(
res.data.data
.filter((model) => {
return model.id.startsWith('gpt-') || model.id.startsWith('text-');
})
.map((model) => model.id)
);
} catch (error) {
showError(error.message);
}
};
const fetchGroups = async () => {
try {
let res = await API.get(`/api/group/`);
if (res === undefined) {
return;
}
setGroupOptions(
res.data.data.map((group) => ({
label: group,
value: group
}))
);
} catch (error) {
showError(error.message);
}
};
const handleSave = async () => {
setLoading(true);
let data = {
tag: tag,
}
if (inputs.model_mapping !== null && inputs.model_mapping !== '') {
if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) {
showInfo('模型映射必须是合法的 JSON 格式!');
setLoading(false);
return;
}
data.model_mapping = inputs.model_mapping
}
if (inputs.groups.length > 0) {
data.groups = inputs.groups.join(',');
}
if (inputs.models.length > 0) {
data.models = inputs.models.join(',');
}
data.new_tag = inputs.new_tag;
// check have any change
if (data.model_mapping === undefined && data.groups === undefined && data.models === undefined && data.new_tag === undefined) {
showWarning('没有任何修改!');
setLoading(false);
return;
}
await submit(data);
setLoading(false);
};
const submit = async (data) => {
try {
const res = await API.put('/api/channel/tag', data);
if (res?.data?.success) {
showSuccess('标签更新成功!');
refresh();
handleClose();
}
} catch (error) {
showError(error);
}
}
useEffect(() => {
let localModelOptions = [...originModelOptions];
inputs.models.forEach((model) => {
if (!localModelOptions.find((option) => option.label === model)) {
localModelOptions.push({
label: model,
value: model
});
}
});
setModelOptions(localModelOptions);
}, [originModelOptions, inputs.models]);
useEffect(() => {
setInputs({
...originInputs,
tag: tag,
new_tag: tag,
})
fetchModels().then();
fetchGroups().then();
}, [visible]);
return (
<SideSheet
title="编辑标签"
visible={visible}
onCancel={handleClose}
footer={
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Space>
<Button onClick={handleClose}>取消</Button>
<Button type="primary" onClick={handleSave} loading={loading}>保存</Button>
</Space>
</div>
}
>
<div style={{ marginTop: 10 }}>
<Banner
type={'warning'}
description={
<>
所有编辑均为覆盖操作留空则不更改
</>
}
></Banner>
</div>
<Spin spinning={loading}>
<TextInput
label="新标签,留空则不更改"
name="newTag"
value={inputs.new_tag}
onChange={(value) => setInputs({ ...inputs, new_tag: value })}
placeholder="请输入新标签"
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>模型留空则不更改</Typography.Text>
</div>
<Select
placeholder={'请选择该渠道所支持的模型,留空则不更改'}
name="models"
required
multiple
selection
onChange={(value) => {
handleInputChange('models', value);
}}
value={inputs.models}
autoComplete="new-password"
optionList={modelOptions}
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>分组留空则不更改</Typography.Text>
</div>
<Select
placeholder={'请选择可以使用该渠道的分组,留空则不更改'}
name="groups"
required
multiple
selection
allowAdditions
additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'}
onChange={(value) => {
handleInputChange('groups', value);
}}
value={inputs.groups}
autoComplete="new-password"
optionList={groupOptions}
/>
<div style={{ marginTop: 10 }}>
<Typography.Text strong>模型重定向</Typography.Text>
</div>
<TextArea
placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,留空则不更改`}
name="model_mapping"
onChange={(value) => {
handleInputChange('model_mapping', value);
}}
autosize
value={inputs.model_mapping}
autoComplete="new-password"
/>
<Space>
<Typography.Text
style={{
color: 'rgba(var(--semi-blue-5), 1)',
userSelect: 'none',
cursor: 'pointer'
}}
onClick={() => {
handleInputChange(
'model_mapping',
JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)
);
}}
>
填入模板
</Typography.Text>
<Typography.Text
style={{
color: 'rgba(var(--semi-blue-5), 1)',
userSelect: 'none',
cursor: 'pointer'
}}
onClick={() => {
handleInputChange(
'model_mapping',
JSON.stringify({}, null, 2)
);
}}
>
清空重定向
</Typography.Text>
<Typography.Text
style={{
color: 'rgba(var(--semi-blue-5), 1)',
userSelect: 'none',
cursor: 'pointer'
}}
onClick={() => {
handleInputChange(
'model_mapping',
""
);
}}
>
不更改
</Typography.Text>
</Space>
</Spin>
</SideSheet>
);
};
export default EditTagModal;