diff --git a/README.md b/README.md
index 7f85bcc0..cb641947 100644
--- a/README.md
+++ b/README.md
@@ -59,6 +59,9 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用
> **Warning**
> 使用 Docker 拉取的最新镜像可能是 `alpha` 版本,如果追求稳定性请手动指定版本。
+> **Warning**
+> 使用 root 用户初次登录系统后,务必修改默认密码 `123456`!
+
## 功能
1. 支持多种大模型:
+ [x] [OpenAI ChatGPT 系列模型](https://platform.openai.com/docs/guides/gpt/chat-completions-api)(支持 [Azure OpenAI API](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference))
@@ -69,6 +72,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用
+ [x] [讯飞星火认知大模型](https://www.xfyun.cn/doc/spark/Web.html)
+ [x] [智谱 ChatGLM 系列模型](https://bigmodel.cn)
+ [x] [360 智脑](https://ai.360.cn)
+ + [x] [腾讯混元大模型](https://cloud.tencent.com/document/product/1729)
2. 支持配置镜像以及众多第三方代理服务:
+ [x] [OpenAI-SB](https://openai-sb.com)
+ [x] [CloseAI](https://console.closeai-asia.com/r/2412)
@@ -103,11 +107,17 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用
## 部署
### 基于 Docker 进行部署
-部署命令:`docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api`
+```shell
+# 使用 SQLite 的部署命令:
+docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api
+# 使用 MySQL 的部署命令,在上面的基础上添加 `-e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi"`,请自行修改数据库连接参数,不清楚如何修改请参见下面环境变量一节。
+# 例如:
+docker run --name one-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api
+```
其中,`-p 3000:3000` 中的第一个 `3000` 是宿主机的端口,可以根据需要进行修改。
-数据将会保存在宿主机的 `/home/ubuntu/data/one-api` 目录,请确保该目录存在且具有写入权限,或者更改为合适的目录。
+数据和日志将会保存在宿主机的 `/home/ubuntu/data/one-api` 目录,请确保该目录存在且具有写入权限,或者更改为合适的目录。
如果启动失败,请添加 `--privileged=true`,具体参考 https://github.com/songquanpeng/one-api/issues/482 。
@@ -236,7 +246,7 @@ docker run --name chatgpt-web -d -p 3002:3002 -e OPENAI_API_BASE_URL=https://ope
部署到 Zeabur
-> Zeabur 的服务器在国外,自动解决了网络的问题,同时免费的额度也足够个人使用。
+> Zeabur 的服务器在国外,自动解决了网络的问题,同时免费的额度也足够个人使用
1. 首先 fork 一份代码。
2. 进入 [Zeabur](https://zeabur.com?referralCode=songquanpeng),登录,进入控制台。
@@ -251,6 +261,17 @@ docker run --name chatgpt-web -d -p 3002:3002 -e OPENAI_API_BASE_URL=https://ope
+
+部署到 Render
+
+
+> Render 提供免费额度,绑卡后可以进一步提升额度
+
+Render 可以直接部署 docker 镜像,不需要 fork 仓库:https://dashboard.render.com
+
+
+
+
## 配置
系统本身开箱即用。
@@ -278,10 +299,11 @@ OPENAI_API_BASE="https://:/v1"
```mermaid
graph LR
A(用户)
- A --->|请求| B(One API)
+ A --->|使用 One API 分发的 key 进行请求| B(One API)
B -->|中继请求| C(OpenAI)
B -->|中继请求| D(Azure)
- B -->|中继请求| E(其他下游渠道)
+ B -->|中继请求| E(其他 OpenAI API 格式下游渠道)
+ B -->|中继并修改请求体和返回体| F(非 OpenAI API 格式下游渠道)
```
可以通过在令牌后面添加渠道 ID 的方式指定使用哪一个渠道处理本次请求,例如:`Authorization: Bearer ONE_API_KEY-CHANNEL_ID`。
@@ -309,22 +331,24 @@ graph LR
+ `SQL_CONN_MAX_LIFETIME`:连接的最大生命周期,默认为 `60`,单位分钟。
4. `FRONTEND_BASE_URL`:设置之后将重定向页面请求到指定的地址,仅限从服务器设置。
+ 例子:`FRONTEND_BASE_URL=https://openai.justsong.cn`
-5. `SYNC_FREQUENCY`:设置之后将定期与数据库同步配置,单位为秒,未设置则不进行同步。
+5. `MEMORY_CACHE_ENABLED`:启用内存缓存,会导致用户额度的更新存在一定的延迟,可选值为 `true` 和 `false`,未设置则默认为 `false`。
+ + 例子:`MEMORY_CACHE_ENABLED=true`
+6. `SYNC_FREQUENCY`:在启用缓存的情况下与数据库同步配置的频率,单位为秒,默认为 `600` 秒。
+ 例子:`SYNC_FREQUENCY=60`
-6. `NODE_TYPE`:设置之后将指定节点类型,可选值为 `master` 和 `slave`,未设置则默认为 `master`。
+7. `NODE_TYPE`:设置之后将指定节点类型,可选值为 `master` 和 `slave`,未设置则默认为 `master`。
+ 例子:`NODE_TYPE=slave`
-7. `CHANNEL_UPDATE_FREQUENCY`:设置之后将定期更新渠道余额,单位为分钟,未设置则不进行更新。
+8. `CHANNEL_UPDATE_FREQUENCY`:设置之后将定期更新渠道余额,单位为分钟,未设置则不进行更新。
+ 例子:`CHANNEL_UPDATE_FREQUENCY=1440`
-8. `CHANNEL_TEST_FREQUENCY`:设置之后将定期检查渠道,单位为分钟,未设置则不进行检查。
+9. `CHANNEL_TEST_FREQUENCY`:设置之后将定期检查渠道,单位为分钟,未设置则不进行检查。
+ 例子:`CHANNEL_TEST_FREQUENCY=1440`
-9. `POLLING_INTERVAL`:批量更新渠道余额以及测试可用性时的请求间隔,单位为秒,默认无间隔。
- + 例子:`POLLING_INTERVAL=5`
-10. `BATCH_UPDATE_ENABLED`:启用数据库批量更新聚合,会导致用户额度的更新存在一定的延迟可选值为 `true` 和 `false`,未设置则默认为 `false`。
+10. `POLLING_INTERVAL`:批量更新渠道余额以及测试可用性时的请求间隔,单位为秒,默认无间隔。
+ + 例子:`POLLING_INTERVAL=5`
+11. `BATCH_UPDATE_ENABLED`:启用数据库批量更新聚合,会导致用户额度的更新存在一定的延迟可选值为 `true` 和 `false`,未设置则默认为 `false`。
+ 例子:`BATCH_UPDATE_ENABLED=true`
+ 如果你遇到了数据库连接数过多的问题,可以尝试启用该选项。
-11. `BATCH_UPDATE_INTERVAL=5`:批量更新聚合的时间间隔,单位为秒,默认为 `5`。
+12. `BATCH_UPDATE_INTERVAL=5`:批量更新聚合的时间间隔,单位为秒,默认为 `5`。
+ 例子:`BATCH_UPDATE_INTERVAL=5`
-12. 请求频率限制:
+13. 请求频率限制:
+ `GLOBAL_API_RATE_LIMIT`:全局 API 速率限制(除中继请求外),单 ip 三分钟内的最大请求数,默认为 `180`。
+ `GLOBAL_WEB_RATE_LIMIT`:全局 Web 速率限制,单 ip 三分钟内的最大请求数,默认为 `60`。
@@ -366,6 +390,12 @@ https://openai.justsong.cn
+ 检查是否启用了 HTTPS,浏览器会拦截 HTTPS 域名下的 HTTP 请求。
6. 报错:`当前分组负载已饱和,请稍后再试`
+ 上游通道 429 了。
+7. 升级之后我的数据会丢失吗?
+ + 如果使用 MySQL,不会。
+ + 如果使用 SQLite,需要按照我所给的部署命令挂载 volume 持久化 one-api.db 数据库文件,否则容器重启后数据会丢失。
+8. 升级之前数据库需要做变更吗?
+ + 一般情况下不需要,系统将在初始化的时候自动调整。
+ + 如果需要的话,我会在更新日志中说明,并给出脚本。
## 相关项目
* [FastGPT](https://github.com/labring/FastGPT): 基于 LLM 大语言模型的知识库问答系统
diff --git a/common/constants.go b/common/constants.go
index 794a795f..a0361c35 100644
--- a/common/constants.go
+++ b/common/constants.go
@@ -56,6 +56,7 @@ var EmailDomainWhitelist = []string{
}
var DebugEnabled = os.Getenv("DEBUG") == "true"
+var MemoryCacheEnabled = os.Getenv("MEMORY_CACHE_ENABLED") == "true"
var LogConsumeEnabled = true
@@ -92,7 +93,7 @@ var IsMasterNode = os.Getenv("NODE_TYPE") != "slave"
var requestInterval, _ = strconv.Atoi(os.Getenv("POLLING_INTERVAL"))
var RequestInterval = time.Duration(requestInterval) * time.Second
-var SyncFrequency = 10 * 60 // unit is second, will be overwritten by SYNC_FREQUENCY
+var SyncFrequency = GetOrDefault("SYNC_FREQUENCY", 10*60) // unit is second
var BatchUpdateEnabled = false
var BatchUpdateInterval = GetOrDefault("BATCH_UPDATE_INTERVAL", 5)
@@ -155,9 +156,10 @@ const (
)
const (
- ChannelStatusUnknown = 0
- ChannelStatusEnabled = 1 // don't use 0, 0 is the default value!
- ChannelStatusDisabled = 2 // also don't use 0
+ ChannelStatusUnknown = 0
+ ChannelStatusEnabled = 1 // don't use 0, 0 is the default value!
+ ChannelStatusManuallyDisabled = 2 // also don't use 0
+ ChannelStatusAutoDisabled = 3
)
const (
@@ -184,30 +186,32 @@ const (
ChannelTypeOpenRouter = 20
ChannelTypeAIProxyLibrary = 21
ChannelTypeFastGPT = 22
+ ChannelTypeTencent = 23
)
var ChannelBaseURLs = []string{
- "", // 0
- "https://api.openai.com", // 1
- "https://oa.api2d.net", // 2
- "", // 3
- "https://api.closeai-proxy.xyz", // 4
- "https://api.openai-sb.com", // 5
- "https://api.openaimax.com", // 6
- "https://api.ohmygpt.com", // 7
- "", // 8
- "https://api.caipacity.com", // 9
- "https://api.aiproxy.io", // 10
- "", // 11
- "https://api.api2gpt.com", // 12
- "https://api.aigc2d.com", // 13
- "https://api.anthropic.com", // 14
- "https://aip.baidubce.com", // 15
- "https://open.bigmodel.cn", // 16
- "https://dashscope.aliyuncs.com", // 17
- "", // 18
- "https://ai.360.cn", // 19
- "https://openrouter.ai/api", // 20
- "https://api.aiproxy.io", // 21
- "https://fastgpt.run/api/openapi", // 22
+ "", // 0
+ "https://api.openai.com", // 1
+ "https://oa.api2d.net", // 2
+ "", // 3
+ "https://api.closeai-proxy.xyz", // 4
+ "https://api.openai-sb.com", // 5
+ "https://api.openaimax.com", // 6
+ "https://api.ohmygpt.com", // 7
+ "", // 8
+ "https://api.caipacity.com", // 9
+ "https://api.aiproxy.io", // 10
+ "", // 11
+ "https://api.api2gpt.com", // 12
+ "https://api.aigc2d.com", // 13
+ "https://api.anthropic.com", // 14
+ "https://aip.baidubce.com", // 15
+ "https://open.bigmodel.cn", // 16
+ "https://dashscope.aliyuncs.com", // 17
+ "", // 18
+ "https://ai.360.cn", // 19
+ "https://openrouter.ai/api", // 20
+ "https://api.aiproxy.io", // 21
+ "https://fastgpt.run/api/openapi", // 22
+ "https://hunyuan.cloud.tencent.com", //23
}
diff --git a/common/model-ratio.go b/common/model-ratio.go
index 9cfe74ca..b121ba4f 100644
--- a/common/model-ratio.go
+++ b/common/model-ratio.go
@@ -24,6 +24,7 @@ var ModelRatio = map[string]float64{
"gpt-3.5-turbo-0613": 0.75,
"gpt-3.5-turbo-16k": 1.5, // $0.003 / 1K tokens
"gpt-3.5-turbo-16k-0613": 1.5,
+ "gpt-3.5-turbo-instruct": 0.75, // $0.0015 / 1K tokens
"text-ada-001": 0.2,
"text-babbage-001": 0.25,
"text-curie-001": 1,
@@ -51,15 +52,15 @@ var ModelRatio = map[string]float64{
"chatglm_std": 0.3572, // ¥0.005 / 1k tokens
"chatglm_lite": 0.1429, // ¥0.002 / 1k tokens
"text_embedding": 0.0357, // ¥0.0005 / 1k tokens
- "qwen-v1": 0.8572, // ¥0.012 / 1k tokens
- "qwen-plus-v1": 1, // ¥0.014 / 1k tokens
+ "qwen-turbo": 0.8572, // ¥0.012 / 1k tokens
+ "qwen-plus": 10, // ¥0.14 / 1k tokens
"text-embedding-v1": 0.05, // ¥0.0007 / 1k tokens
"SparkDesk": 1.2858, // ¥0.018 / 1k tokens
"360GPT_S2_V9": 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
- "360GPT_S2_V9.4": 0.8572, // ¥0.012 / 1k tokens
+ "hunyuan": 7.143, // ¥0.1 / 1k tokens // https://cloud.tencent.com/document/product/1729/97731#e0e6be58-60c8-469f-bdeb-6c264ce3b4d0
}
func ModelRatio2JSONString() string {
diff --git a/controller/channel-test.go b/controller/channel-test.go
index f7a565a2..1974ef6e 100644
--- a/controller/channel-test.go
+++ b/controller/channel-test.go
@@ -141,7 +141,7 @@ func disableChannel(channelId int, channelName string, reason string) {
if common.RootUserEmail == "" {
common.RootUserEmail = model.GetRootUserEmail()
}
- model.UpdateChannelStatusById(channelId, common.ChannelStatusDisabled)
+ model.UpdateChannelStatusById(channelId, common.ChannelStatusAutoDisabled)
subject := fmt.Sprintf("通道「%s」(#%d)已被禁用", channelName, channelId)
content := fmt.Sprintf("通道「%s」(#%d)已被禁用,原因:%s", channelName, channelId, reason)
err := common.SendEmail(subject, common.RootUserEmail, content)
diff --git a/controller/channel.go b/controller/channel.go
index 50b2b5f6..41a55550 100644
--- a/controller/channel.go
+++ b/controller/channel.go
@@ -127,6 +127,23 @@ func DeleteChannel(c *gin.Context) {
return
}
+func DeleteManuallyDisabledChannel(c *gin.Context) {
+ rows, err := model.DeleteChannelByStatus(common.ChannelStatusManuallyDisabled)
+ if err != nil {
+ c.JSON(http.StatusOK, gin.H{
+ "success": false,
+ "message": err.Error(),
+ })
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{
+ "success": true,
+ "message": "",
+ "data": rows,
+ })
+ return
+}
+
func UpdateChannel(c *gin.Context) {
channel := model.Channel{}
err := c.ShouldBindJSON(&channel)
diff --git a/controller/model.go b/controller/model.go
index 47af070c..a8e71db7 100644
--- a/controller/model.go
+++ b/controller/model.go
@@ -117,6 +117,15 @@ func init() {
Root: "gpt-3.5-turbo-16k-0613",
Parent: nil,
},
+ {
+ Id: "gpt-3.5-turbo-instruct",
+ Object: "model",
+ Created: 1677649963,
+ OwnedBy: "openai",
+ Permission: permission,
+ Root: "gpt-3.5-turbo-instruct",
+ Parent: nil,
+ },
{
Id: "gpt-4",
Object: "model",
@@ -352,21 +361,21 @@ func init() {
Parent: nil,
},
{
- Id: "qwen-v1",
+ Id: "qwen-turbo",
Object: "model",
Created: 1677649963,
OwnedBy: "ali",
Permission: permission,
- Root: "qwen-v1",
+ Root: "qwen-turbo",
Parent: nil,
},
{
- Id: "qwen-plus-v1",
+ Id: "qwen-plus",
Object: "model",
Created: 1677649963,
OwnedBy: "ali",
Permission: permission,
- Root: "qwen-plus-v1",
+ Root: "qwen-plus",
Parent: nil,
},
{
@@ -424,12 +433,12 @@ func init() {
Parent: nil,
},
{
- Id: "360GPT_S2_V9.4",
+ Id: "hunyuan",
Object: "model",
Created: 1677649963,
- OwnedBy: "360",
+ OwnedBy: "tencent",
Permission: permission,
- Root: "360GPT_S2_V9.4",
+ Root: "hunyuan",
Parent: nil,
},
}
diff --git a/controller/option.go b/controller/option.go
index 9cf4ff1b..bbf83578 100644
--- a/controller/option.go
+++ b/controller/option.go
@@ -46,7 +46,7 @@ func UpdateOption(c *gin.Context) {
if option.Value == "true" && common.GitHubClientId == "" {
c.JSON(http.StatusOK, gin.H{
"success": false,
- "message": "无法启用 GitHub OAuth,请先填入 GitHub Client ID 以及 GitHub Client Secret!",
+ "message": "无法启用 GitHub OAuth,请先填入 GitHub Client Id 以及 GitHub Client Secret!",
})
return
}
diff --git a/controller/relay-audio.go b/controller/relay-audio.go
index e6f54f01..381c6feb 100644
--- a/controller/relay-audio.go
+++ b/controller/relay-audio.go
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
+ "errors"
"fmt"
"io"
"net/http"
@@ -31,6 +32,9 @@ func relayAudioHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
if err != nil {
return errorWrapper(err, "get_user_quota_failed", http.StatusInternalServerError)
}
+ if userQuota-preConsumedQuota < 0 {
+ return errorWrapper(errors.New("user quota is not enough"), "insufficient_user_quota", http.StatusForbidden)
+ }
err = model.CacheDecreaseUserQuota(userId, preConsumedQuota)
if err != nil {
return errorWrapper(err, "decrease_user_quota_failed", http.StatusInternalServerError)
diff --git a/controller/relay-image.go b/controller/relay-image.go
index fb30895c..998a7851 100644
--- a/controller/relay-image.go
+++ b/controller/relay-image.go
@@ -99,7 +99,7 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
quota := int(ratio*sizeRatio*1000) * imageRequest.N
if consumeQuota && userQuota-quota < 0 {
- return errorWrapper(err, "insufficient_user_quota", http.StatusForbidden)
+ return errorWrapper(errors.New("user quota is not enough"), "insufficient_user_quota", http.StatusForbidden)
}
req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody)
diff --git a/controller/relay-tencent.go b/controller/relay-tencent.go
new file mode 100644
index 00000000..024468bc
--- /dev/null
+++ b/controller/relay-tencent.go
@@ -0,0 +1,287 @@
+package controller
+
+import (
+ "bufio"
+ "crypto/hmac"
+ "crypto/sha1"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "io"
+ "net/http"
+ "one-api/common"
+ "sort"
+ "strconv"
+ "strings"
+)
+
+// https://cloud.tencent.com/document/product/1729/97732
+
+type TencentMessage struct {
+ Role string `json:"role"`
+ Content string `json:"content"`
+}
+
+type TencentChatRequest struct {
+ AppId int64 `json:"app_id"` // 腾讯云账号的 APPID
+ SecretId string `json:"secret_id"` // 官网 SecretId
+ // Timestamp当前 UNIX 时间戳,单位为秒,可记录发起 API 请求的时间。
+ // 例如1529223702,如果与当前时间相差过大,会引起签名过期错误
+ Timestamp int64 `json:"timestamp"`
+ // Expired 签名的有效期,是一个符合 UNIX Epoch 时间戳规范的数值,
+ // 单位为秒;Expired 必须大于 Timestamp 且 Expired-Timestamp 小于90天
+ Expired int64 `json:"expired"`
+ QueryID string `json:"query_id"` //请求 Id,用于问题排查
+ // Temperature 较高的数值会使输出更加随机,而较低的数值会使其更加集中和确定
+ // 默认 1.0,取值区间为[0.0,2.0],非必要不建议使用,不合理的取值会影响效果
+ // 建议该参数和 top_p 只设置1个,不要同时更改 top_p
+ Temperature float64 `json:"temperature"`
+ // TopP 影响输出文本的多样性,取值越大,生成文本的多样性越强
+ // 默认1.0,取值区间为[0.0, 1.0],非必要不建议使用, 不合理的取值会影响效果
+ // 建议该参数和 temperature 只设置1个,不要同时更改
+ TopP float64 `json:"top_p"`
+ // Stream 0:同步,1:流式 (默认,协议:SSE)
+ // 同步请求超时:60s,如果内容较长建议使用流式
+ Stream int `json:"stream"`
+ // Messages 会话内容, 长度最多为40, 按对话时间从旧到新在数组中排列
+ // 输入 content 总数最大支持 3000 token。
+ Messages []TencentMessage `json:"messages"`
+}
+
+type TencentError struct {
+ Code int `json:"code"`
+ Message string `json:"message"`
+}
+
+type TencentUsage struct {
+ InputTokens int `json:"input_tokens"`
+ OutputTokens int `json:"output_tokens"`
+ TotalTokens int `json:"total_tokens"`
+}
+
+type TencentResponseChoices struct {
+ FinishReason string `json:"finish_reason,omitempty"` // 流式结束标志位,为 stop 则表示尾包
+ Messages TencentMessage `json:"messages,omitempty"` // 内容,同步模式返回内容,流模式为 null 输出 content 内容总数最多支持 1024token。
+ Delta TencentMessage `json:"delta,omitempty"` // 内容,流模式返回内容,同步模式为 null 输出 content 内容总数最多支持 1024token。
+}
+
+type TencentChatResponse struct {
+ Choices []TencentResponseChoices `json:"choices,omitempty"` // 结果
+ Created string `json:"created,omitempty"` // unix 时间戳的字符串
+ Id string `json:"id,omitempty"` // 会话 id
+ Usage Usage `json:"usage,omitempty"` // token 数量
+ Error TencentError `json:"error,omitempty"` // 错误信息 注意:此字段可能返回 null,表示取不到有效值
+ Note string `json:"note,omitempty"` // 注释
+ ReqID string `json:"req_id,omitempty"` // 唯一请求 Id,每次请求都会返回。用于反馈接口入参
+}
+
+func requestOpenAI2Tencent(request GeneralOpenAIRequest) *TencentChatRequest {
+ messages := make([]TencentMessage, 0, len(request.Messages))
+ for i := 0; i < len(request.Messages); i++ {
+ message := request.Messages[i]
+ if message.Role == "system" {
+ messages = append(messages, TencentMessage{
+ Role: "user",
+ Content: message.Content,
+ })
+ messages = append(messages, TencentMessage{
+ Role: "assistant",
+ Content: "Okay",
+ })
+ continue
+ }
+ messages = append(messages, TencentMessage{
+ Content: message.Content,
+ Role: message.Role,
+ })
+ }
+ stream := 0
+ if request.Stream {
+ stream = 1
+ }
+ return &TencentChatRequest{
+ Timestamp: common.GetTimestamp(),
+ Expired: common.GetTimestamp() + 24*60*60,
+ QueryID: common.GetUUID(),
+ Temperature: request.Temperature,
+ TopP: request.TopP,
+ Stream: stream,
+ Messages: messages,
+ }
+}
+
+func responseTencent2OpenAI(response *TencentChatResponse) *OpenAITextResponse {
+ fullTextResponse := OpenAITextResponse{
+ Object: "chat.completion",
+ Created: common.GetTimestamp(),
+ Usage: response.Usage,
+ }
+ if len(response.Choices) > 0 {
+ choice := OpenAITextResponseChoice{
+ Index: 0,
+ Message: Message{
+ Role: "assistant",
+ Content: response.Choices[0].Messages.Content,
+ },
+ FinishReason: response.Choices[0].FinishReason,
+ }
+ fullTextResponse.Choices = append(fullTextResponse.Choices, choice)
+ }
+ return &fullTextResponse
+}
+
+func streamResponseTencent2OpenAI(TencentResponse *TencentChatResponse) *ChatCompletionsStreamResponse {
+ response := ChatCompletionsStreamResponse{
+ Object: "chat.completion.chunk",
+ Created: common.GetTimestamp(),
+ Model: "tencent-hunyuan",
+ }
+ if len(TencentResponse.Choices) > 0 {
+ var choice ChatCompletionsStreamResponseChoice
+ choice.Delta.Content = TencentResponse.Choices[0].Delta.Content
+ if TencentResponse.Choices[0].FinishReason == "stop" {
+ choice.FinishReason = &stopFinishReason
+ }
+ response.Choices = append(response.Choices, choice)
+ }
+ return &response
+}
+
+func tencentStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, string) {
+ var responseText string
+ scanner := bufio.NewScanner(resp.Body)
+ scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
+ if atEOF && len(data) == 0 {
+ return 0, nil, nil
+ }
+ if i := strings.Index(string(data), "\n"); i >= 0 {
+ return i + 1, data[0:i], nil
+ }
+ if atEOF {
+ return len(data), data, nil
+ }
+ return 0, nil, nil
+ })
+ dataChan := make(chan string)
+ stopChan := make(chan bool)
+ go func() {
+ for scanner.Scan() {
+ data := scanner.Text()
+ if len(data) < 5 { // ignore blank line or wrong format
+ continue
+ }
+ if data[:5] != "data:" {
+ continue
+ }
+ data = data[5:]
+ dataChan <- data
+ }
+ stopChan <- true
+ }()
+ setEventStreamHeaders(c)
+ c.Stream(func(w io.Writer) bool {
+ select {
+ case data := <-dataChan:
+ var TencentResponse TencentChatResponse
+ err := json.Unmarshal([]byte(data), &TencentResponse)
+ if err != nil {
+ common.SysError("error unmarshalling stream response: " + err.Error())
+ return true
+ }
+ response := streamResponseTencent2OpenAI(&TencentResponse)
+ if len(response.Choices) != 0 {
+ responseText += response.Choices[0].Delta.Content
+ }
+ jsonResponse, err := json.Marshal(response)
+ if err != nil {
+ common.SysError("error marshalling stream response: " + err.Error())
+ return true
+ }
+ c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)})
+ return true
+ case <-stopChan:
+ c.Render(-1, common.CustomEvent{Data: "data: [DONE]"})
+ return false
+ }
+ })
+ err := resp.Body.Close()
+ if err != nil {
+ return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), ""
+ }
+ return nil, responseText
+}
+
+func tencentHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) {
+ var TencentResponse TencentChatResponse
+ responseBody, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
+ }
+ err = resp.Body.Close()
+ if err != nil {
+ return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
+ }
+ err = json.Unmarshal(responseBody, &TencentResponse)
+ if err != nil {
+ return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+ }
+ if TencentResponse.Error.Code != 0 {
+ return &OpenAIErrorWithStatusCode{
+ OpenAIError: OpenAIError{
+ Message: TencentResponse.Error.Message,
+ Code: TencentResponse.Error.Code,
+ },
+ StatusCode: resp.StatusCode,
+ }, nil
+ }
+ fullTextResponse := responseTencent2OpenAI(&TencentResponse)
+ jsonResponse, err := json.Marshal(fullTextResponse)
+ if err != nil {
+ return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
+ }
+ c.Writer.Header().Set("Content-Type", "application/json")
+ c.Writer.WriteHeader(resp.StatusCode)
+ _, err = c.Writer.Write(jsonResponse)
+ return nil, &fullTextResponse.Usage
+}
+
+func parseTencentConfig(config string) (appId int64, secretId string, secretKey string, err error) {
+ parts := strings.Split(config, "|")
+ if len(parts) != 3 {
+ err = errors.New("invalid tencent config")
+ return
+ }
+ appId, err = strconv.ParseInt(parts[0], 10, 64)
+ secretId = parts[1]
+ secretKey = parts[2]
+ return
+}
+
+func getTencentSign(req TencentChatRequest, secretKey string) string {
+ params := make([]string, 0)
+ params = append(params, "app_id="+strconv.FormatInt(req.AppId, 10))
+ params = append(params, "secret_id="+req.SecretId)
+ params = append(params, "timestamp="+strconv.FormatInt(req.Timestamp, 10))
+ params = append(params, "query_id="+req.QueryID)
+ params = append(params, "temperature="+strconv.FormatFloat(req.Temperature, 'f', -1, 64))
+ params = append(params, "top_p="+strconv.FormatFloat(req.TopP, 'f', -1, 64))
+ params = append(params, "stream="+strconv.Itoa(req.Stream))
+ params = append(params, "expired="+strconv.FormatInt(req.Expired, 10))
+
+ var messageStr string
+ for _, msg := range req.Messages {
+ messageStr += fmt.Sprintf(`{"role":"%s","content":"%s"},`, msg.Role, msg.Content)
+ }
+ messageStr = strings.TrimSuffix(messageStr, ",")
+ params = append(params, "messages=["+messageStr+"]")
+
+ sort.Sort(sort.StringSlice(params))
+ url := "hunyuan.cloud.tencent.com/hyllm/v1/chat/completions?" + strings.Join(params, "&")
+ mac := hmac.New(sha1.New, []byte(secretKey))
+ signURL := url
+ mac.Write([]byte(signURL))
+ sign := mac.Sum([]byte(nil))
+ return base64.StdEncoding.EncodeToString(sign)
+}
diff --git a/controller/relay-text.go b/controller/relay-text.go
index f8e612f0..24d09e5a 100644
--- a/controller/relay-text.go
+++ b/controller/relay-text.go
@@ -24,6 +24,7 @@ const (
APITypeAli
APITypeXunfei
APITypeAIProxyLibrary
+ APITypeTencent
)
var httpClient *http.Client
@@ -109,6 +110,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
apiType = APITypeXunfei
case common.ChannelTypeAIProxyLibrary:
apiType = APITypeAIProxyLibrary
+ case common.ChannelTypeTencent:
+ apiType = APITypeTencent
}
baseURL := common.ChannelBaseURLs[channelType]
requestURL := c.Request.URL.String()
@@ -182,6 +185,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
if relayMode == RelayModeEmbeddings {
fullRequestURL = "https://dashscope.aliyuncs.com/api/v1/services/embeddings/text-embedding/text-embedding"
}
+ case APITypeTencent:
+ fullRequestURL = "https://hunyuan.cloud.tencent.com/hyllm/v1/chat/completions"
case APITypeAIProxyLibrary:
fullRequestURL = fmt.Sprintf("%s/api/library/ask", baseURL)
}
@@ -207,6 +212,9 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
if err != nil {
return errorWrapper(err, "get_user_quota_failed", http.StatusInternalServerError)
}
+ if userQuota-preConsumedQuota < 0 {
+ return errorWrapper(errors.New("user quota is not enough"), "insufficient_user_quota", http.StatusForbidden)
+ }
err = model.CacheDecreaseUserQuota(userId, preConsumedQuota)
if err != nil {
return errorWrapper(err, "decrease_user_quota_failed", http.StatusInternalServerError)
@@ -293,6 +301,23 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
}
requestBody = bytes.NewBuffer(jsonStr)
+ case APITypeTencent:
+ apiKey := c.Request.Header.Get("Authorization")
+ apiKey = strings.TrimPrefix(apiKey, "Bearer ")
+ appId, secretId, secretKey, err := parseTencentConfig(apiKey)
+ if err != nil {
+ return errorWrapper(err, "invalid_tencent_config", http.StatusInternalServerError)
+ }
+ tencentRequest := requestOpenAI2Tencent(textRequest)
+ tencentRequest.AppId = appId
+ tencentRequest.SecretId = secretId
+ jsonStr, err := json.Marshal(tencentRequest)
+ if err != nil {
+ return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError)
+ }
+ sign := getTencentSign(*tencentRequest, secretKey)
+ c.Request.Header.Set("Authorization", sign)
+ requestBody = bytes.NewBuffer(jsonStr)
case APITypeAIProxyLibrary:
aiProxyLibraryRequest := requestOpenAI2AIProxyLibrary(textRequest)
aiProxyLibraryRequest.LibraryId = c.GetString("library_id")
@@ -340,6 +365,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
if textRequest.Stream {
req.Header.Set("X-DashScope-SSE", "enable")
}
+ case APITypeTencent:
+ req.Header.Set("Authorization", apiKey)
default:
req.Header.Set("Authorization", "Bearer "+apiKey)
}
@@ -599,6 +626,25 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode {
}
return nil
}
+ case APITypeTencent:
+ if isStream {
+ err, responseText := tencentStreamHandler(c, resp)
+ if err != nil {
+ return err
+ }
+ textResponse.Usage.PromptTokens = promptTokens
+ textResponse.Usage.CompletionTokens = countTokenText(responseText, textRequest.Model)
+ return nil
+ } else {
+ err, usage := tencentHandler(c, resp)
+ if err != nil {
+ return err
+ }
+ if usage != nil {
+ textResponse.Usage = *usage
+ }
+ return nil
+ }
default:
return errorWrapper(errors.New("unknown api type"), "unknown_api_type", http.StatusInternalServerError)
}
diff --git a/controller/relay-utils.go b/controller/relay-utils.go
index 3d5948fc..4775ec88 100644
--- a/controller/relay-utils.go
+++ b/controller/relay-utils.go
@@ -9,44 +9,53 @@ import (
"net/http"
"one-api/common"
"strconv"
+ "strings"
)
var stopFinishReason = "stop"
+// tokenEncoderMap won't grow after initialization
var tokenEncoderMap = map[string]*tiktoken.Tiktoken{}
+var defaultTokenEncoder *tiktoken.Tiktoken
func InitTokenEncoders() {
common.SysLog("initializing token encoders")
- fallbackTokenEncoder, err := tiktoken.EncodingForModel("gpt-3.5-turbo")
+ gpt35TokenEncoder, err := tiktoken.EncodingForModel("gpt-3.5-turbo")
if err != nil {
- common.FatalLog(fmt.Sprintf("failed to get fallback token encoder: %s", err.Error()))
+ common.FatalLog(fmt.Sprintf("failed to get gpt-3.5-turbo token encoder: %s", err.Error()))
+ }
+ defaultTokenEncoder = gpt35TokenEncoder
+ gpt4TokenEncoder, err := tiktoken.EncodingForModel("gpt-4")
+ if err != nil {
+ common.FatalLog(fmt.Sprintf("failed to get gpt-4 token encoder: %s", err.Error()))
}
for model, _ := range common.ModelRatio {
- tokenEncoder, err := tiktoken.EncodingForModel(model)
- if err != nil {
- common.SysError(fmt.Sprintf("using fallback encoder for model %s", model))
- tokenEncoderMap[model] = fallbackTokenEncoder
- continue
+ if strings.HasPrefix(model, "gpt-3.5") {
+ tokenEncoderMap[model] = gpt35TokenEncoder
+ } else if strings.HasPrefix(model, "gpt-4") {
+ tokenEncoderMap[model] = gpt4TokenEncoder
+ } else {
+ tokenEncoderMap[model] = nil
}
- tokenEncoderMap[model] = tokenEncoder
}
common.SysLog("token encoders initialized")
}
func getTokenEncoder(model string) *tiktoken.Tiktoken {
- if tokenEncoder, ok := tokenEncoderMap[model]; ok {
+ tokenEncoder, ok := tokenEncoderMap[model]
+ if ok && tokenEncoder != nil {
return tokenEncoder
}
- tokenEncoder, err := tiktoken.EncodingForModel(model)
- if err != nil {
- common.SysError(fmt.Sprintf("failed to get token encoder for model %s: %s, using encoder for gpt-3.5-turbo", model, err.Error()))
- tokenEncoder, err = tiktoken.EncodingForModel("gpt-3.5-turbo")
+ if ok {
+ tokenEncoder, err := tiktoken.EncodingForModel(model)
if err != nil {
- common.FatalLog(fmt.Sprintf("failed to get token encoder for model gpt-3.5-turbo: %s", err.Error()))
+ common.SysError(fmt.Sprintf("failed to get token encoder for model %s: %s, using encoder for gpt-3.5-turbo", model, err.Error()))
+ tokenEncoder = defaultTokenEncoder
}
+ tokenEncoderMap[model] = tokenEncoder
+ return tokenEncoder
}
- tokenEncoderMap[model] = tokenEncoder
- return tokenEncoder
+ return defaultTokenEncoder
}
func getTokenNum(tokenEncoder *tiktoken.Tiktoken, text string) int {
diff --git a/main.go b/main.go
index e8ef4c20..88938516 100644
--- a/main.go
+++ b/main.go
@@ -2,6 +2,7 @@ package main
import (
"embed"
+ "fmt"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
@@ -50,18 +51,17 @@ func main() {
// Initialize options
model.InitOptionMap()
if common.RedisEnabled {
+ // for compatibility with old versions
+ common.MemoryCacheEnabled = true
+ }
+ if common.MemoryCacheEnabled {
+ common.SysLog("memory cache enabled")
+ common.SysError(fmt.Sprintf("sync frequency: %d seconds", common.SyncFrequency))
model.InitChannelCache()
}
- if os.Getenv("SYNC_FREQUENCY") != "" {
- frequency, err := strconv.Atoi(os.Getenv("SYNC_FREQUENCY"))
- if err != nil {
- common.FatalLog("failed to parse SYNC_FREQUENCY: " + err.Error())
- }
- common.SyncFrequency = frequency
- go model.SyncOptions(frequency)
- if common.RedisEnabled {
- go model.SyncChannelCache(frequency)
- }
+ if common.MemoryCacheEnabled {
+ go model.SyncOptions(common.SyncFrequency)
+ go model.SyncChannelCache(common.SyncFrequency)
}
if os.Getenv("CHANNEL_UPDATE_FREQUENCY") != "" {
frequency, err := strconv.Atoi(os.Getenv("CHANNEL_UPDATE_FREQUENCY"))
diff --git a/middleware/auth.go b/middleware/auth.go
index dfbc7dbd..b0803612 100644
--- a/middleware/auth.go
+++ b/middleware/auth.go
@@ -94,7 +94,7 @@ func TokenAuth() func(c *gin.Context) {
abortWithMessage(c, http.StatusUnauthorized, err.Error())
return
}
- userEnabled, err := model.IsUserEnabled(token.UserId)
+ userEnabled, err := model.CacheIsUserEnabled(token.UserId)
if err != nil {
abortWithMessage(c, http.StatusInternalServerError, err.Error())
return
diff --git a/middleware/distributor.go b/middleware/distributor.go
index 9ded3231..d80945fc 100644
--- a/middleware/distributor.go
+++ b/middleware/distributor.go
@@ -25,12 +25,12 @@ func Distribute() func(c *gin.Context) {
if ok {
id, err := strconv.Atoi(channelId.(string))
if err != nil {
- abortWithMessage(c, http.StatusBadRequest, "无效的渠道 ID")
+ abortWithMessage(c, http.StatusBadRequest, "无效的渠道 Id")
return
}
channel, err = model.GetChannelById(id, true)
if err != nil {
- abortWithMessage(c, http.StatusBadRequest, "无效的渠道 ID")
+ abortWithMessage(c, http.StatusBadRequest, "无效的渠道 Id")
return
}
if channel.Status != common.ChannelStatusEnabled {
diff --git a/model/cache.go b/model/cache.go
index b9d6b612..a7f5c06f 100644
--- a/model/cache.go
+++ b/model/cache.go
@@ -186,7 +186,7 @@ func SyncChannelCache(frequency int) {
}
func CacheGetRandomSatisfiedChannel(group string, model string) (*Channel, error) {
- if !common.RedisEnabled {
+ if !common.MemoryCacheEnabled {
return GetRandomSatisfiedChannel(group, model)
}
channelSyncLock.RLock()
diff --git a/model/channel.go b/model/channel.go
index aa3b8a10..36bb78a5 100644
--- a/model/channel.go
+++ b/model/channel.go
@@ -11,7 +11,7 @@ type Channel struct {
Key string `json:"key" gorm:"not null;index"`
Status int `json:"status" gorm:"default:1"`
Name string `json:"name" gorm:"index"`
- Weight int `json:"weight"`
+ Weight *uint `json:"weight" gorm:"default:0"`
CreatedTime int64 `json:"created_time" gorm:"bigint"`
TestTime int64 `json:"test_time" gorm:"bigint"`
ResponseTime int `json:"response_time"` // in milliseconds
@@ -176,3 +176,8 @@ func updateChannelUsedQuota(id int, quota int) {
common.SysError("failed to update channel used quota: " + err.Error())
}
}
+
+func DeleteChannelByStatus(status int64) (int64, error) {
+ result := DB.Where("status = ?", status).Delete(&Channel{})
+ return result.RowsAffected, result.Error
+}
diff --git a/model/log.go b/model/log.go
index 8e177258..d26da9a2 100644
--- a/model/log.go
+++ b/model/log.go
@@ -8,18 +8,18 @@ import (
)
type Log struct {
- Id int `json:"id"`
- UserId int `json:"user_id"`
- CreatedAt int64 `json:"created_at" gorm:"bigint;index"`
- Type int `json:"type" gorm:"index"`
+ Id int `json:"id;index:idx_created_at_id,priority:1"`
+ UserId int `json:"user_id" gorm:"index"`
+ CreatedAt int64 `json:"created_at" gorm:"bigint;index:idx_created_at_id,priority:2;index:idx_created_at_type"`
+ Type int `json:"type" gorm:"index:idx_created_at_type"`
Content string `json:"content"`
- Username string `json:"username" gorm:"index;default:''"`
+ Username string `json:"username" gorm:"index:index_username_model_name,priority:2;default:''"`
TokenName string `json:"token_name" gorm:"index;default:''"`
- ModelName string `json:"model_name" gorm:"index;default:''"`
+ ModelName string `json:"model_name" gorm:"index;index:index_username_model_name,priority:1;default:''"`
Quota int `json:"quota" gorm:"default:0"`
PromptTokens int `json:"prompt_tokens" gorm:"default:0"`
CompletionTokens int `json:"completion_tokens" gorm:"default:0"`
- Channel int `json:"channel" gorm:"default:0"`
+ ChannelId int `json:"channel" gorm:"index"`
}
const (
@@ -47,7 +47,6 @@ func RecordLog(userId int, logType int, content string) {
}
}
-
func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptTokens int, completionTokens int, modelName string, tokenName string, quota int, content string) {
common.LogInfo(ctx, fmt.Sprintf("record consume log: userId=%d, channelId=%d, promptTokens=%d, completionTokens=%d, modelName=%s, tokenName=%s, quota=%d, content=%s", userId, channelId, promptTokens, completionTokens, modelName, tokenName, quota, content))
if !common.LogConsumeEnabled {
@@ -64,7 +63,7 @@ func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptToke
TokenName: tokenName,
ModelName: modelName,
Quota: quota,
- Channel: channelId,
+ ChannelId: channelId,
}
err := DB.Create(log).Error
if err != nil {
diff --git a/model/main.go b/model/main.go
index d422c4e0..0e962049 100644
--- a/model/main.go
+++ b/model/main.go
@@ -81,6 +81,7 @@ func InitDB() (err error) {
if !common.IsMasterNode {
return nil
}
+ common.SysLog("database migration started")
err = db.AutoMigrate(&Channel{})
if err != nil {
return err
diff --git a/router/api-router.go b/router/api-router.go
index d12bc54b..5ec385dc 100644
--- a/router/api-router.go
+++ b/router/api-router.go
@@ -74,6 +74,7 @@ func SetApiRouter(router *gin.Engine) {
channelRoute.GET("/update_balance/:id", controller.UpdateChannelBalance)
channelRoute.POST("/", controller.AddChannel)
channelRoute.PUT("/", controller.UpdateChannel)
+ channelRoute.DELETE("/manually_disabled", controller.DeleteManuallyDisabledChannel)
channelRoute.DELETE("/:id", controller.DeleteChannel)
}
tokenRoute := apiRouter.Group("/token")
diff --git a/web/src/components/ChannelsTable.js b/web/src/components/ChannelsTable.js
index 7c8457d0..57d45c55 100644
--- a/web/src/components/ChannelsTable.js
+++ b/web/src/components/ChannelsTable.js
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
-import {Button, Form, Input, Label, Pagination, Popup, Table} from 'semantic-ui-react';
+import { Button, Form, Input, Label, Pagination, Popup, Table } from 'semantic-ui-react';
import { Link } from 'react-router-dom';
import { API, showError, showInfo, showNotice, showSuccess, timestamp2string } from '../helpers';
@@ -96,7 +96,7 @@ const ChannelsTable = () => {
});
}, []);
- const manageChannel = async (id, action, idx, priority) => {
+ const manageChannel = async (id, action, idx, value) => {
let data = { id };
let res;
switch (action) {
@@ -112,10 +112,20 @@ const ChannelsTable = () => {
res = await API.put('/api/channel/', data);
break;
case 'priority':
- if (priority === '') {
+ if (value === '') {
return;
}
- data.priority = parseInt(priority);
+ data.priority = parseInt(value);
+ res = await API.put('/api/channel/', data);
+ break;
+ case 'weight':
+ if (value === '') {
+ return;
+ }
+ data.weight = parseInt(value);
+ if (data.weight < 0) {
+ data.weight = 0;
+ }
res = await API.put('/api/channel/', data);
break;
}
@@ -142,9 +152,23 @@ const ChannelsTable = () => {
return ;
case 2:
return (
-
+
+ 已禁用
+ }
+ content='本渠道被手动禁用'
+ basic
+ />
+ );
+ case 3:
+ return (
+
+ 已禁用
+ }
+ content='本渠道被程序自动禁用'
+ basic
+ />
);
default:
return (
@@ -202,7 +226,7 @@ const ChannelsTable = () => {
showInfo(`通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。`);
} else {
showError(message);
- showNotice("当前版本测试是通过按照 OpenAI API 格式使用 gpt-3.5-turbo 模型进行非流式请求实现的,因此测试报错并不一定代表通道不可用,该功能后续会修复。")
+ showNotice('当前版本测试是通过按照 OpenAI API 格式使用 gpt-3.5-turbo 模型进行非流式请求实现的,因此测试报错并不一定代表通道不可用,该功能后续会修复。');
}
};
@@ -216,6 +240,17 @@ const ChannelsTable = () => {
}
};
+ const deleteAllManuallyDisabledChannels = async () => {
+ const res = await API.delete(`/api/channel/manually_disabled`);
+ const { success, message, data } = res.data;
+ if (success) {
+ showSuccess(`已删除所有手动禁用渠道,共计 ${data} 个`);
+ await refresh();
+ } else {
+ showError(message);
+ }
+ };
+
const updateChannelBalance = async (id, name, idx) => {
const res = await API.get(`/api/channel/update_balance/${id}/`);
const { success, message, balance } = res.data;
@@ -343,10 +378,10 @@ const ChannelsTable = () => {
余额
{
- sortChannel('priority');
- }}
+ style={{ cursor: 'pointer' }}
+ onClick={() => {
+ sortChannel('priority');
+ }}
>
优先级
@@ -390,18 +425,18 @@ const ChannelsTable = () => {
{
- manageChannel(
- channel.id,
- 'priority',
- idx,
- event.target.value,
- );
- }}>
-
- }
- content='渠道选择优先级,越高越优先'
- basic
+ trigger={ {
+ manageChannel(
+ channel.id,
+ 'priority',
+ idx,
+ event.target.value
+ );
+ }}>
+
+ }
+ content='渠道选择优先级,越高越优先'
+ basic
/>
@@ -481,6 +516,20 @@ const ChannelsTable = () => {
+
+ 删除所有手动禁用渠道
+
+ }
+ on='click'
+ flowing
+ hoverable
+ >
+
+
{
const [inputs, setInputs] = useState({
@@ -68,8 +68,14 @@ const LoginForm = () => {
if (success) {
userDispatch({ type: 'login', payload: data });
localStorage.setItem('user', JSON.stringify(data));
- navigate('/');
- showSuccess('登录成功!');
+ if (username === 'root' && password === '123456') {
+ navigate('/user/edit');
+ showSuccess('登录成功!');
+ showWarning('请立刻修改默认密码!');
+ } else {
+ navigate('/token');
+ showSuccess('登录成功!');
+ }
} else {
showError(message);
}
@@ -126,7 +132,7 @@ const LoginForm = () => {
circular
color='black'
icon='github'
- onClick={()=>onGitHubOAuthClicked(status.github_client_id)}
+ onClick={() => onGitHubOAuthClicked(status.github_client_id)}
/>
) : (
<>>
diff --git a/web/src/constants/channel.constants.js b/web/src/constants/channel.constants.js
index e42afc6e..76407745 100644
--- a/web/src/constants/channel.constants.js
+++ b/web/src/constants/channel.constants.js
@@ -8,6 +8,7 @@ export const CHANNEL_OPTIONS = [
{ key: 18, text: '讯飞星火认知', value: 18, color: 'blue' },
{ key: 16, text: '智谱 ChatGLM', value: 16, color: 'violet' },
{ key: 19, text: '360 智脑', value: 19, color: 'blue' },
+ { key: 23, text: '腾讯混元', value: 23, color: 'teal' },
{ key: 8, text: '自定义渠道', value: 8, color: 'pink' },
{ key: 22, text: '知识库:FastGPT', value: 22, color: 'blue' },
{ key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple' },
diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js
index 70802607..caee3c61 100644
--- a/web/src/pages/Channel/EditChannel.js
+++ b/web/src/pages/Channel/EditChannel.js
@@ -19,6 +19,8 @@ function type2secretPrompt(type) {
return '按照如下格式输入:APPID|APISecret|APIKey';
case 22:
return '按照如下格式输入:APIKey-AppId,例如:fastgpt-0sp2gtvfdgyi4k30jwlgwf1i-64f335d84283f05518e9e041';
+ case 23:
+ return '按照如下格式输入:AppId|SecretId|SecretKey';
default:
return '请输入渠道对应的鉴权密钥';
}
@@ -67,7 +69,7 @@ const EditChannel = () => {
localModels = ['ERNIE-Bot', 'ERNIE-Bot-turbo', 'Embedding-V1'];
break;
case 17:
- localModels = ['qwen-v1', 'qwen-plus-v1', 'text-embedding-v1'];
+ localModels = ['qwen-turbo', 'qwen-plus', 'text-embedding-v1'];
break;
case 16:
localModels = ['chatglm_pro', 'chatglm_std', 'chatglm_lite', 'text_embedding'];
@@ -76,7 +78,10 @@ const EditChannel = () => {
localModels = ['SparkDesk'];
break;
case 19:
- localModels = ['360GPT_S2_V9', 'embedding-bert-512-v1', 'embedding_s1_v1', 'semantic_similarity_s1_v1', '360GPT_S2_V9.4'];
+ localModels = ['360GPT_S2_V9', 'embedding-bert-512-v1', 'embedding_s1_v1', 'semantic_similarity_s1_v1'];
+ break;
+ case 23:
+ localModels = ['hunyuan'];
break;
}
setInputs((inputs) => ({ ...inputs, models: localModels }));
diff --git a/web/src/pages/User/EditUser.js b/web/src/pages/User/EditUser.js
index e8f96027..8ae0e556 100644
--- a/web/src/pages/User/EditUser.js
+++ b/web/src/pages/User/EditUser.js
@@ -102,7 +102,7 @@ const EditUser = () => {
label='密码'
name='password'
type={'password'}
- placeholder={'请输入新的密码'}
+ placeholder={'请输入新的密码,最短 8 位'}
onChange={handleInputChange}
value={password}
autoComplete='new-password'