mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-05 11:22:14 +00:00
Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6e8887482 | ||
|
|
a29f4d88c5 | ||
|
|
a6bb30af41 | ||
|
|
424424c160 | ||
|
|
e5baa6ee1c | ||
|
|
9207d729ca | ||
|
|
27933da884 | ||
|
|
454dac17ea | ||
|
|
1921ac3692 | ||
|
|
42a2418d9a | ||
|
|
5cb317bdbd | ||
|
|
37dd1ef099 | ||
|
|
5fa6462412 | ||
|
|
a882e680ae | ||
|
|
552e2850c5 | ||
|
|
c418d9ed9a | ||
|
|
1dc2284d57 | ||
|
|
f4cc90c8d6 | ||
|
|
140d3a974b | ||
|
|
2ecb742e47 | ||
|
|
9066cfa8a0 | ||
|
|
4f437f30e0 | ||
|
|
3c2a86f94d | ||
|
|
1b07282153 | ||
|
|
af7f886c39 | ||
|
|
9cfa138796 | ||
|
|
dc132655a6 | ||
|
|
a378665b8c | ||
|
|
3516aad349 | ||
|
|
58525c574b | ||
|
|
1df39e5a7f | ||
|
|
be6ffd3c60 | ||
|
|
a9522075c6 | ||
|
|
983d31bfd3 | ||
|
|
20c043f584 | ||
|
|
73263e02d6 | ||
|
|
7143b0f160 | ||
|
|
dd82618c05 | ||
|
|
19935ee8ac | ||
|
|
6fef5aaf22 | ||
|
|
b5aa3c129b | ||
|
|
8c7c39550c | ||
|
|
962e803d8a | ||
|
|
ff57ced2bb | ||
|
|
2223806c00 | ||
|
|
d1c62a583d | ||
|
|
892d014c26 |
51
README.md
51
README.md
@@ -36,8 +36,8 @@
|
||||
> 本项目为开源项目,在[One API](https://github.com/songquanpeng/one-api)的基础上进行二次开发
|
||||
|
||||
> [!IMPORTANT]
|
||||
> - 使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。
|
||||
> - 本项目仅供个人学习使用,不保证稳定性,且不提供任何技术支持。
|
||||
> - 使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。
|
||||
> - 根据[《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。
|
||||
|
||||
## 📚 文档
|
||||
@@ -46,35 +46,32 @@
|
||||
|
||||
## ✨ 主要特性
|
||||
|
||||
New API提供了丰富的功能,详细特性请参考[维基百科-特性说明](https://docs.newapi.pro/wiki/features-introduction):
|
||||
New API提供了丰富的功能,详细特性请参考[特性说明](https://docs.newapi.pro/wiki/features-introduction):
|
||||
|
||||
1. 🎨 全新的UI界面
|
||||
2. 🌍 多语言支持
|
||||
3. 🎨 支持[Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口,[对接文档](https://docs.newapi.pro/api/relay/image/midjourney)
|
||||
4. 💰 支持在线充值功能(易支付)
|
||||
5. 🔍 支持用key查询使用额度(配合[neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool))
|
||||
6. 📑 分页支持选择每页显示数量
|
||||
7. 🔄 兼容原版One API的数据库
|
||||
8. 💵 支持模型按次数收费
|
||||
9. ⚖️ 支持渠道加权随机
|
||||
10. 📈 数据看板(控制台)
|
||||
11. 🔒 可设置令牌能调用的模型
|
||||
12. 🤖 支持Telegram授权登录
|
||||
13. 🎵 支持[Suno API](https://github.com/Suno-API/Suno-API)接口,[接口文档](https://docs.newapi.pro/api/suno-music)
|
||||
14. 🔄 支持Rerank模型(Cohere和Jina),[接口文档](https://docs.newapi.pro/api/jinaai-rerank)
|
||||
15. ⚡ 支持OpenAI Realtime API(包括Azure渠道),[接口文档](https://docs.newapi.pro/api/openai-realtime)
|
||||
16. ⚡ 支持Claude Messages 格式,[接口文档](https://docs.newapi.pro/api/anthropic-chat)
|
||||
17. 支持使用路由/chat2link进入聊天界面
|
||||
18. 🧠 支持通过模型名称后缀设置 reasoning effort:
|
||||
3. 💰 支持在线充值功能(易支付)
|
||||
4. 🔍 支持用key查询使用额度(配合[neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool))
|
||||
5. 🔄 兼容原版One API的数据库
|
||||
6. 💵 支持模型按次数收费
|
||||
7. ⚖️ 支持渠道加权随机
|
||||
8. 📈 数据看板(控制台)
|
||||
9. 🔒 令牌分组、模型限制
|
||||
10. 🤖 支持更多授权登陆方式(LinuxDO,Telegram、OIDC)
|
||||
11. 🔄 支持Rerank模型(Cohere和Jina),[接口文档](https://docs.newapi.pro/api/jinaai-rerank)
|
||||
12. ⚡ 支持OpenAI Realtime API(包括Azure渠道),[接口文档](https://docs.newapi.pro/api/openai-realtime)
|
||||
13. ⚡ 支持Claude Messages 格式,[接口文档](https://docs.newapi.pro/api/anthropic-chat)
|
||||
14. 支持使用路由/chat2link进入聊天界面
|
||||
15. 🧠 支持通过模型名称后缀设置 reasoning effort:
|
||||
1. OpenAI o系列模型
|
||||
- 添加后缀 `-high` 设置为 high reasoning effort (例如: `o3-mini-high`)
|
||||
- 添加后缀 `-medium` 设置为 medium reasoning effort (例如: `o3-mini-medium`)
|
||||
- 添加后缀 `-low` 设置为 low reasoning effort (例如: `o3-mini-low`)
|
||||
2. Claude 思考模型
|
||||
- 添加后缀 `-thinking` 启用思考模式 (例如: `claude-3-7-sonnet-20250219-thinking`)
|
||||
19. 🔄 思考转内容功能
|
||||
20. 🔄 模型限流功能
|
||||
20. 💰 缓存计费支持,开启后可以在缓存命中时按照设定的比例计费:
|
||||
16. 🔄 思考转内容功能
|
||||
17. 🔄 针对用户的模型限流功能
|
||||
18. 💰 缓存计费支持,开启后可以在缓存命中时按照设定的比例计费:
|
||||
1. 在 `系统设置-运营设置` 中设置 `提示缓存倍率` 选项
|
||||
2. 在渠道中设置 `提示缓存倍率`,范围 0-1,例如设置为 0.5 表示缓存命中时按照 50% 计费
|
||||
3. 支持的渠道:
|
||||
@@ -88,12 +85,12 @@ New API提供了丰富的功能,详细特性请参考[维基百科-特性说
|
||||
此版本支持多种模型,详情请参考[接口文档-中继接口](https://docs.newapi.pro/api):
|
||||
|
||||
1. 第三方模型 **gpts** (gpt-4-gizmo-*)
|
||||
2. [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口,[接口文档](https://docs.newapi.pro/api/midjourney-proxy-image)
|
||||
3. 自定义渠道,支持填入完整调用地址
|
||||
4. [Suno API](https://github.com/Suno-API/Suno-API)接口,[接口文档](https://docs.newapi.pro/api/suno-music)
|
||||
2. 第三方渠道[Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口,[接口文档](https://docs.newapi.pro/api/midjourney-proxy-image)
|
||||
3. 第三方渠道[Suno API](https://github.com/Suno-API/Suno-API)接口,[接口文档](https://docs.newapi.pro/api/suno-music)
|
||||
4. 自定义渠道,支持填入完整调用地址
|
||||
5. Rerank模型([Cohere](https://cohere.ai/)和[Jina](https://jina.ai/)),[接口文档](https://docs.newapi.pro/api/jinaai-rerank)
|
||||
6. Claude Messages 格式,[接口文档](https://docs.newapi.pro/api/anthropic-chat)
|
||||
7. Dify
|
||||
7. Dify,当前仅支持chatflow
|
||||
|
||||
## 环境变量配置
|
||||
|
||||
@@ -120,7 +117,6 @@ New API提供了丰富的功能,详细特性请参考[维基百科-特性说
|
||||
|
||||
> [!TIP]
|
||||
> 最新版Docker镜像:`calciumion/new-api:latest`
|
||||
> 默认账号root 密码123456
|
||||
|
||||
### 多机部署注意事项
|
||||
- 必须设置环境变量 `SESSION_SECRET`,否则会导致多机部署时登录状态不一致
|
||||
@@ -168,9 +164,6 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234
|
||||
|
||||
- [聊天接口(Chat)](https://docs.newapi.pro/api/openai-chat)
|
||||
- [图像接口(Image)](https://docs.newapi.pro/api/openai-image)
|
||||
- [Midjourney接口](https://docs.newapi.pro/api/midjourney-proxy-image)
|
||||
- [音乐接口(Music)](https://docs.newapi.pro/api/relay/music)
|
||||
- [Suno接口](https://docs.newapi.pro/api/suno-music)
|
||||
- [重排序接口(Rerank)](https://docs.newapi.pro/api/jinaai-rerank)
|
||||
- [实时对话接口(Realtime)](https://docs.newapi.pro/api/openai-realtime)
|
||||
- [Claude聊天接口(messages)](https://docs.newapi.pro/api/anthropic-chat)
|
||||
|
||||
3
constant/setup.go
Normal file
3
constant/setup.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package constant
|
||||
|
||||
var Setup = false
|
||||
@@ -1,11 +1,12 @@
|
||||
package constant
|
||||
|
||||
var (
|
||||
UserSettingNotifyType = "notify_type" // QuotaWarningType 额度预警类型
|
||||
UserSettingQuotaWarningThreshold = "quota_warning_threshold" // QuotaWarningThreshold 额度预警阈值
|
||||
UserSettingWebhookUrl = "webhook_url" // WebhookUrl webhook地址
|
||||
UserSettingWebhookSecret = "webhook_secret" // WebhookSecret webhook密钥
|
||||
UserSettingNotificationEmail = "notification_email" // NotificationEmail 通知邮箱地址
|
||||
UserSettingNotifyType = "notify_type" // QuotaWarningType 额度预警类型
|
||||
UserSettingQuotaWarningThreshold = "quota_warning_threshold" // QuotaWarningThreshold 额度预警阈值
|
||||
UserSettingWebhookUrl = "webhook_url" // WebhookUrl webhook地址
|
||||
UserSettingWebhookSecret = "webhook_secret" // WebhookSecret webhook密钥
|
||||
UserSettingNotificationEmail = "notification_email" // NotificationEmail 通知邮箱地址
|
||||
UserAcceptUnsetRatioModel = "accept_unset_model_ratio_model" // AcceptUnsetRatioModel 是否接受未设置价格的模型
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -105,6 +105,11 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
|
||||
request := buildTestRequest(testModel)
|
||||
common.SysLog(fmt.Sprintf("testing channel %d with model %s , info %v ", channel.Id, testModel, info))
|
||||
|
||||
priceData, err := helper.ModelPriceHelper(c, info, 0, int(request.MaxTokens))
|
||||
if err != nil {
|
||||
return err, nil
|
||||
}
|
||||
|
||||
adaptor.Init(info)
|
||||
|
||||
convertedRequest, err := adaptor.ConvertOpenAIRequest(c, info, request)
|
||||
@@ -143,10 +148,7 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
|
||||
return err, nil
|
||||
}
|
||||
info.PromptTokens = usage.PromptTokens
|
||||
priceData, err := helper.ModelPriceHelper(c, info, usage.PromptTokens, int(request.MaxTokens))
|
||||
if err != nil {
|
||||
return err, nil
|
||||
}
|
||||
|
||||
quota := 0
|
||||
if !priceData.UsePrice {
|
||||
quota = usage.PromptTokens + int(math.Round(float64(usage.CompletionTokens)*priceData.CompletionRatio))
|
||||
@@ -187,7 +189,9 @@ func buildTestRequest(model string) *dto.GeneralOpenAIRequest {
|
||||
if strings.HasPrefix(model, "o1") || strings.HasPrefix(model, "o3") {
|
||||
testRequest.MaxCompletionTokens = 10
|
||||
} else if strings.Contains(model, "thinking") {
|
||||
testRequest.MaxTokens = 50
|
||||
if !strings.Contains(model, "claude") {
|
||||
testRequest.MaxTokens = 50
|
||||
}
|
||||
} else {
|
||||
testRequest.MaxTokens = 10
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/model"
|
||||
"one-api/setting"
|
||||
"one-api/setting/operation_setting"
|
||||
@@ -72,6 +73,7 @@ func GetStatus(c *gin.Context) {
|
||||
"oidc_enabled": system_setting.GetOIDCSettings().Enabled,
|
||||
"oidc_client_id": system_setting.GetOIDCSettings().ClientId,
|
||||
"oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint,
|
||||
"setup": constant.Setup,
|
||||
},
|
||||
})
|
||||
return
|
||||
|
||||
@@ -53,11 +53,12 @@ func UpdateOption(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
case "oidc.enabled":
|
||||
if option.Value == "true" && system_setting.GetOIDCSettings().Enabled {
|
||||
if option.Value == "true" && system_setting.GetOIDCSettings().ClientId == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无法启用 OIDC 登录,请先填入 OIDC Client Id 以及 OIDC Client Secret!",
|
||||
})
|
||||
return
|
||||
}
|
||||
case "LinuxDOOAuthEnabled":
|
||||
if option.Value == "true" && common.LinuxDOClientId == "" {
|
||||
@@ -89,6 +90,15 @@ func UpdateOption(c *gin.Context) {
|
||||
"success": false,
|
||||
"message": "无法启用 Turnstile 校验,请先填入 Turnstile 校验相关配置信息!",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
case "TelegramOAuthEnabled":
|
||||
if option.Value == "true" && common.TelegramBotToken == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "无法启用 Telegram OAuth,请先填入 Telegram Bot Token!",
|
||||
})
|
||||
return
|
||||
}
|
||||
case "GroupRatio":
|
||||
@@ -100,6 +110,7 @@ func UpdateOption(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
err = model.UpdateOption(option.Key, option.Value)
|
||||
if err != nil {
|
||||
|
||||
173
controller/setup.go
Normal file
173
controller/setup.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"one-api/model"
|
||||
"one-api/setting/operation_setting"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Setup struct {
|
||||
Status bool `json:"status"`
|
||||
RootInit bool `json:"root_init"`
|
||||
DatabaseType string `json:"database_type"`
|
||||
}
|
||||
|
||||
type SetupRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
ConfirmPassword string `json:"confirmPassword"`
|
||||
SelfUseModeEnabled bool `json:"SelfUseModeEnabled"`
|
||||
DemoSiteEnabled bool `json:"DemoSiteEnabled"`
|
||||
}
|
||||
|
||||
func GetSetup(c *gin.Context) {
|
||||
setup := Setup{
|
||||
Status: constant.Setup,
|
||||
}
|
||||
if constant.Setup {
|
||||
c.JSON(200, gin.H{
|
||||
"success": true,
|
||||
"data": setup,
|
||||
})
|
||||
return
|
||||
}
|
||||
setup.RootInit = model.RootUserExists()
|
||||
if common.UsingMySQL {
|
||||
setup.DatabaseType = "mysql"
|
||||
}
|
||||
if common.UsingPostgreSQL {
|
||||
setup.DatabaseType = "postgres"
|
||||
}
|
||||
if common.UsingSQLite {
|
||||
setup.DatabaseType = "sqlite"
|
||||
}
|
||||
c.JSON(200, gin.H{
|
||||
"success": true,
|
||||
"data": setup,
|
||||
})
|
||||
}
|
||||
|
||||
func PostSetup(c *gin.Context) {
|
||||
// Check if setup is already completed
|
||||
if constant.Setup {
|
||||
c.JSON(400, gin.H{
|
||||
"success": false,
|
||||
"message": "系统已经初始化完成",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if root user already exists
|
||||
rootExists := model.RootUserExists()
|
||||
|
||||
var req SetupRequest
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{
|
||||
"success": false,
|
||||
"message": "请求参数有误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// If root doesn't exist, validate and create admin account
|
||||
if !rootExists {
|
||||
// Validate password
|
||||
if req.Password != req.ConfirmPassword {
|
||||
c.JSON(400, gin.H{
|
||||
"success": false,
|
||||
"message": "两次输入的密码不一致",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Password) < 8 {
|
||||
c.JSON(400, gin.H{
|
||||
"success": false,
|
||||
"message": "密码长度至少为8个字符",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Create root user
|
||||
hashedPassword, err := common.Password2Hash(req.Password)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{
|
||||
"success": false,
|
||||
"message": "系统错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
rootUser := model.User{
|
||||
Username: req.Username,
|
||||
Password: hashedPassword,
|
||||
Role: common.RoleRootUser,
|
||||
Status: common.UserStatusEnabled,
|
||||
DisplayName: "Root User",
|
||||
AccessToken: nil,
|
||||
Quota: 100000000,
|
||||
}
|
||||
err = model.DB.Create(&rootUser).Error
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{
|
||||
"success": false,
|
||||
"message": "创建管理员账号失败: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Set operation modes
|
||||
operation_setting.SelfUseModeEnabled = req.SelfUseModeEnabled
|
||||
operation_setting.DemoSiteEnabled = req.DemoSiteEnabled
|
||||
|
||||
// Save operation modes to database for persistence
|
||||
err = model.UpdateOption("SelfUseModeEnabled", boolToString(req.SelfUseModeEnabled))
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{
|
||||
"success": false,
|
||||
"message": "保存自用模式设置失败: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err = model.UpdateOption("DemoSiteEnabled", boolToString(req.DemoSiteEnabled))
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{
|
||||
"success": false,
|
||||
"message": "保存演示站点模式设置失败: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Update setup status
|
||||
constant.Setup = true
|
||||
|
||||
setup := model.Setup{
|
||||
Version: common.Version,
|
||||
InitializedAt: time.Now().Unix(),
|
||||
}
|
||||
err = model.DB.Create(&setup).Error
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{
|
||||
"success": false,
|
||||
"message": "系统初始化失败: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"success": true,
|
||||
"message": "系统初始化成功",
|
||||
})
|
||||
}
|
||||
|
||||
func boolToString(b bool) string {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
@@ -913,11 +913,12 @@ func TopUp(c *gin.Context) {
|
||||
}
|
||||
|
||||
type UpdateUserSettingRequest struct {
|
||||
QuotaWarningType string `json:"notify_type"`
|
||||
QuotaWarningThreshold float64 `json:"quota_warning_threshold"`
|
||||
WebhookUrl string `json:"webhook_url,omitempty"`
|
||||
WebhookSecret string `json:"webhook_secret,omitempty"`
|
||||
NotificationEmail string `json:"notification_email,omitempty"`
|
||||
QuotaWarningType string `json:"notify_type"`
|
||||
QuotaWarningThreshold float64 `json:"quota_warning_threshold"`
|
||||
WebhookUrl string `json:"webhook_url,omitempty"`
|
||||
WebhookSecret string `json:"webhook_secret,omitempty"`
|
||||
NotificationEmail string `json:"notification_email,omitempty"`
|
||||
AcceptUnsetModelRatioModel bool `json:"accept_unset_model_ratio_model"`
|
||||
}
|
||||
|
||||
func UpdateUserSetting(c *gin.Context) {
|
||||
@@ -993,6 +994,7 @@ func UpdateUserSetting(c *gin.Context) {
|
||||
settings := map[string]interface{}{
|
||||
constant.UserSettingNotifyType: req.QuotaWarningType,
|
||||
constant.UserSettingQuotaWarningThreshold: req.QuotaWarningThreshold,
|
||||
"accept_unset_model_ratio_model": req.AcceptUnsetModelRatioModel,
|
||||
}
|
||||
|
||||
// 如果是webhook类型,添加webhook相关设置
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
- 类型为字符串,填写代理地址(例如 socks5 协议的代理地址)
|
||||
|
||||
3. thinking_to_content
|
||||
- 用于标识是否将思考内容`reasoning_conetnt`转换为`<think>`标签拼接到内容中返回
|
||||
- 用于标识是否将思考内容`reasoning_content`转换为`<think>`标签拼接到内容中返回
|
||||
- 类型为布尔值,设置为 true 时启用思考内容转换
|
||||
|
||||
--------------------------------------------------------------
|
||||
@@ -30,4 +30,4 @@
|
||||
|
||||
--------------------------------------------------------------
|
||||
|
||||
通过调整上述 JSON 配置中的值,可以灵活控制渠道的额外行为,比如是否进行格式化以及使用特定的网络代理。
|
||||
通过调整上述 JSON 配置中的值,可以灵活控制渠道的额外行为,比如是否进行格式化以及使用特定的网络代理。
|
||||
|
||||
@@ -183,7 +183,7 @@ type ClaudeResponse struct {
|
||||
Completion string `json:"completion,omitempty"`
|
||||
StopReason string `json:"stop_reason,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Error ClaudeError `json:"error,omitempty"`
|
||||
Error *ClaudeError `json:"error,omitempty"`
|
||||
Usage *ClaudeUsage `json:"usage,omitempty"`
|
||||
Index *int `json:"index,omitempty"`
|
||||
ContentBlock *ClaudeMediaMessage `json:"content_block,omitempty"`
|
||||
|
||||
@@ -5,18 +5,29 @@ type RerankRequest struct {
|
||||
Query string `json:"query"`
|
||||
Model string `json:"model"`
|
||||
TopN int `json:"top_n"`
|
||||
ReturnDocuments bool `json:"return_documents,omitempty"`
|
||||
ReturnDocuments *bool `json:"return_documents,omitempty"`
|
||||
MaxChunkPerDoc int `json:"max_chunk_per_doc,omitempty"`
|
||||
OverLapTokens int `json:"overlap_tokens,omitempty"`
|
||||
}
|
||||
|
||||
type RerankResponseDocument struct {
|
||||
func (r *RerankRequest) GetReturnDocuments() bool {
|
||||
if r.ReturnDocuments == nil {
|
||||
return false
|
||||
}
|
||||
return *r.ReturnDocuments
|
||||
}
|
||||
|
||||
type RerankResponseResult struct {
|
||||
Document any `json:"document,omitempty"`
|
||||
Index int `json:"index"`
|
||||
RelevanceScore float64 `json:"relevance_score"`
|
||||
}
|
||||
|
||||
type RerankResponse struct {
|
||||
Results []RerankResponseDocument `json:"results"`
|
||||
Usage Usage `json:"usage"`
|
||||
type RerankDocument struct {
|
||||
Text any `json:"text"`
|
||||
}
|
||||
|
||||
type RerankResponse struct {
|
||||
Results []RerankResponseResult `json:"results"`
|
||||
Usage Usage `json:"usage"`
|
||||
}
|
||||
|
||||
@@ -212,6 +212,7 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode
|
||||
c.Set("channel_name", channel.Name)
|
||||
c.Set("channel_type", channel.Type)
|
||||
c.Set("channel_setting", channel.GetSetting())
|
||||
c.Set("param_override", channel.GetParamOverride())
|
||||
if nil != channel.OpenAIOrganization && "" != *channel.OpenAIOrganization {
|
||||
c.Set("channel_organization", *channel.OpenAIOrganization)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ type Channel struct {
|
||||
OtherInfo string `json:"other_info"`
|
||||
Tag *string `json:"tag" gorm:"index"`
|
||||
Setting *string `json:"setting" gorm:"type:text"`
|
||||
ParamOverride *string `json:"param_override" gorm:"type:text"`
|
||||
}
|
||||
|
||||
func (channel *Channel) GetModels() []string {
|
||||
@@ -511,6 +512,17 @@ func (channel *Channel) SetSetting(setting map[string]interface{}) {
|
||||
channel.Setting = common.GetPointer[string](string(settingBytes))
|
||||
}
|
||||
|
||||
func (channel *Channel) GetParamOverride() map[string]interface{} {
|
||||
paramOverride := make(map[string]interface{})
|
||||
if channel.ParamOverride != nil && *channel.ParamOverride != "" {
|
||||
err := json.Unmarshal([]byte(*channel.ParamOverride), ¶mOverride)
|
||||
if err != nil {
|
||||
common.SysError("failed to unmarshal param override: " + err.Error())
|
||||
}
|
||||
}
|
||||
return paramOverride
|
||||
}
|
||||
|
||||
func GetChannelsByIds(ids []int) ([]*Channel, error) {
|
||||
var channels []*Channel
|
||||
err := DB.Where("id in (?)", ids).Find(&channels).Error
|
||||
|
||||
@@ -3,6 +3,7 @@ package model
|
||||
import (
|
||||
"log"
|
||||
"one-api/common"
|
||||
"one-api/constant"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -55,6 +56,33 @@ func createRootAccountIfNeed() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkSetup() {
|
||||
setup := GetSetup()
|
||||
if setup == nil {
|
||||
// No setup record exists, check if we have a root user
|
||||
if RootUserExists() {
|
||||
common.SysLog("system is not initialized, but root user exists")
|
||||
// Create setup record
|
||||
newSetup := Setup{
|
||||
Version: common.Version,
|
||||
InitializedAt: time.Now().Unix(),
|
||||
}
|
||||
err := DB.Create(&newSetup).Error
|
||||
if err != nil {
|
||||
common.SysLog("failed to create setup record: " + err.Error())
|
||||
}
|
||||
constant.Setup = true
|
||||
} else {
|
||||
common.SysLog("system is not initialized and no root user exists")
|
||||
constant.Setup = false
|
||||
}
|
||||
} else {
|
||||
// Setup record exists, system is initialized
|
||||
common.SysLog("system is already initialized at: " + time.Unix(setup.InitializedAt, 0).String())
|
||||
constant.Setup = true
|
||||
}
|
||||
}
|
||||
|
||||
func chooseDB(envName string) (*gorm.DB, error) {
|
||||
defer func() {
|
||||
initCol()
|
||||
@@ -214,8 +242,10 @@ func migrateDB() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = DB.AutoMigrate(&Setup{})
|
||||
common.SysLog("database migrated")
|
||||
err = createRootAccountIfNeed()
|
||||
checkSetup()
|
||||
//err = createRootAccountIfNeed()
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
16
model/setup.go
Normal file
16
model/setup.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package model
|
||||
|
||||
type Setup struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Version string `json:"version" gorm:"type:varchar(50);not null"`
|
||||
InitializedAt int64 `json:"initialized_at" gorm:"type:bigint;not null"`
|
||||
}
|
||||
|
||||
func GetSetup() *Setup {
|
||||
var setup Setup
|
||||
err := DB.First(&setup).Error
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &setup
|
||||
}
|
||||
@@ -808,3 +808,12 @@ func (user *User) FillUserByLinuxDOId() error {
|
||||
err := DB.Where("linux_do_id = ?", user.LinuxDOId).First(user).Error
|
||||
return err
|
||||
}
|
||||
|
||||
func RootUserExists() bool {
|
||||
var user User
|
||||
err := DB.Where("role = ?", common.RoleRootUser).First(&user).Error
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ type Adaptor struct {
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) {
|
||||
c.Set("request_model", request.Model)
|
||||
c.Set("converted_request", request)
|
||||
return request, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -13,4 +13,41 @@ var awsModelIDMap = map[string]string{
|
||||
"claude-3-7-sonnet-20250219": "anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
}
|
||||
|
||||
var awsModelCanCrossRegionMap = map[string]map[string]bool{
|
||||
"anthropic.claude-3-sonnet-20240229-v1:0": {
|
||||
"us": true,
|
||||
"eu": true,
|
||||
"ap": true,
|
||||
},
|
||||
"anthropic.claude-3-opus-20240229-v1:0": {
|
||||
"us": true,
|
||||
},
|
||||
"anthropic.claude-3-haiku-20240307-v1:0": {
|
||||
"us": true,
|
||||
"eu": true,
|
||||
"ap": true,
|
||||
},
|
||||
"anthropic.claude-3-5-sonnet-20240620-v1:0": {
|
||||
"us": true,
|
||||
"eu": true,
|
||||
"ap": true,
|
||||
},
|
||||
"anthropic.claude-3-5-sonnet-20241022-v2:0": {
|
||||
"us": true,
|
||||
"ap": true,
|
||||
},
|
||||
"anthropic.claude-3-5-haiku-20241022-v1:0": {
|
||||
"us": true,
|
||||
},
|
||||
"anthropic.claude-3-7-sonnet-20250219-v1:0": {
|
||||
"us": true,
|
||||
},
|
||||
}
|
||||
|
||||
var awsRegionCrossModelPrefixMap = map[string]string{
|
||||
"us": "us",
|
||||
"eu": "eu",
|
||||
"ap": "apac",
|
||||
}
|
||||
|
||||
var ChannelName = "aws"
|
||||
|
||||
@@ -43,6 +43,28 @@ func wrapErr(err error) *dto.OpenAIErrorWithStatusCode {
|
||||
}
|
||||
}
|
||||
|
||||
func awsRegionPrefix(awsRegionId string) string {
|
||||
parts := strings.Split(awsRegionId, "-")
|
||||
regionPrefix := ""
|
||||
if len(parts) > 0 {
|
||||
regionPrefix = parts[0]
|
||||
}
|
||||
return regionPrefix
|
||||
}
|
||||
|
||||
func awsModelCanCrossRegion(awsModelId, awsRegionPrefix string) bool {
|
||||
regionSet, exists := awsModelCanCrossRegionMap[awsModelId]
|
||||
return exists && regionSet[awsRegionPrefix]
|
||||
}
|
||||
|
||||
func awsModelCrossRegion(awsModelId, awsRegionPrefix string) string {
|
||||
modelPrefix, find := awsRegionCrossModelPrefixMap[awsRegionPrefix]
|
||||
if !find {
|
||||
return awsModelId
|
||||
}
|
||||
return modelPrefix + "." + awsModelId
|
||||
}
|
||||
|
||||
func awsModelID(requestModel string) (string, error) {
|
||||
if awsModelID, ok := awsModelIDMap[requestModel]; ok {
|
||||
return awsModelID, nil
|
||||
@@ -62,6 +84,12 @@ func awsHandler(c *gin.Context, info *relaycommon.RelayInfo, requestMode int) (*
|
||||
return wrapErr(errors.Wrap(err, "awsModelID")), nil
|
||||
}
|
||||
|
||||
awsRegionPrefix := awsRegionPrefix(awsCli.Options().Region)
|
||||
canCrossRegion := awsModelCanCrossRegion(awsModelId, awsRegionPrefix)
|
||||
if canCrossRegion {
|
||||
awsModelId = awsModelCrossRegion(awsModelId, awsRegionPrefix)
|
||||
}
|
||||
|
||||
awsReq := &bedrockruntime.InvokeModelInput{
|
||||
ModelId: aws.String(awsModelId),
|
||||
Accept: aws.String("application/json"),
|
||||
@@ -107,6 +135,12 @@ func awsStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel
|
||||
return wrapErr(errors.Wrap(err, "awsModelID")), nil
|
||||
}
|
||||
|
||||
awsRegionPrefix := awsRegionPrefix(awsCli.Options().Region)
|
||||
canCrossRegion := awsModelCanCrossRegion(awsModelId, awsRegionPrefix)
|
||||
if canCrossRegion {
|
||||
awsModelId = awsModelCrossRegion(awsModelId, awsRegionPrefix)
|
||||
}
|
||||
|
||||
awsReq := &bedrockruntime.InvokeModelWithResponseStreamInput{
|
||||
ModelId: aws.String(awsModelId),
|
||||
Accept: aws.String("application/json"),
|
||||
|
||||
@@ -70,7 +70,9 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*dto.Cla
|
||||
Description: tool.Function.Description,
|
||||
}
|
||||
claudeTool.InputSchema = make(map[string]interface{})
|
||||
claudeTool.InputSchema["type"] = params["type"].(string)
|
||||
if params["type"] != nil {
|
||||
claudeTool.InputSchema["type"] = params["type"].(string)
|
||||
}
|
||||
claudeTool.InputSchema["properties"] = params["properties"]
|
||||
claudeTool.InputSchema["required"] = params["required"]
|
||||
for s, a := range params {
|
||||
@@ -485,7 +487,7 @@ func HandleStreamResponseData(c *gin.Context, info *relaycommon.RelayInfo, claud
|
||||
common.SysError("error unmarshalling stream response: " + err.Error())
|
||||
return service.OpenAIErrorWrapper(err, "stream_response_error", http.StatusInternalServerError)
|
||||
}
|
||||
if claudeResponse.Error.Type != "" {
|
||||
if claudeResponse.Error != nil && claudeResponse.Error.Type != "" {
|
||||
return &dto.OpenAIErrorWithStatusCode{
|
||||
Error: dto.OpenAIError{
|
||||
Code: "stream_response_error",
|
||||
@@ -598,7 +600,7 @@ func HandleClaudeResponseData(c *gin.Context, info *relaycommon.RelayInfo, claud
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapper(err, "unmarshal_claude_response_failed", http.StatusInternalServerError)
|
||||
}
|
||||
if claudeResponse.Error.Type != "" {
|
||||
if claudeResponse.Error != nil && claudeResponse.Error.Type != "" {
|
||||
return &dto.OpenAIErrorWithStatusCode{
|
||||
Error: dto.OpenAIError{
|
||||
Message: claudeResponse.Error.Message,
|
||||
|
||||
@@ -40,8 +40,8 @@ type CohereRerankRequest struct {
|
||||
}
|
||||
|
||||
type CohereRerankResponseResult struct {
|
||||
Results []dto.RerankResponseDocument `json:"results"`
|
||||
Meta CohereMeta `json:"meta"`
|
||||
Results []dto.RerankResponseResult `json:"results"`
|
||||
Meta CohereMeta `json:"meta"`
|
||||
}
|
||||
|
||||
type CohereMeta struct {
|
||||
|
||||
@@ -198,6 +198,12 @@ func streamResponseDify2OpenAI(difyResponse DifyChunkChatCompletionResponse) *dt
|
||||
choice.Delta.SetReasoningContent(text + "\n")
|
||||
}
|
||||
} else if difyResponse.Event == "message" || difyResponse.Event == "agent_message" {
|
||||
if difyResponse.Answer == "<details style=\"color:gray;background-color: #f8f8f8;padding: 8px;border-radius: 4px;\" open> <summary> Thinking... </summary>\n" {
|
||||
difyResponse.Answer = "<think>"
|
||||
} else if difyResponse.Answer == "</details>" {
|
||||
difyResponse.Answer = "</think>"
|
||||
}
|
||||
|
||||
choice.Delta.SetContentString(difyResponse.Answer)
|
||||
}
|
||||
response.Choices = append(response.Choices, choice)
|
||||
|
||||
@@ -16,6 +16,8 @@ var ModelList = []string{
|
||||
"gemini-2.0-pro-exp",
|
||||
// thinking exp
|
||||
"gemini-2.0-flash-thinking-exp",
|
||||
"gemini-2.5-pro-exp-03-25",
|
||||
"gemini-2.5-pro-preview-03-25",
|
||||
// imagen models
|
||||
"imagen-3.0-generate-002",
|
||||
// embedding models
|
||||
|
||||
@@ -69,7 +69,7 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela
|
||||
|
||||
func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) {
|
||||
if info.RelayMode == constant.RelayModeRerank {
|
||||
err, usage = common_handler.RerankHandler(c, resp)
|
||||
err, usage = common_handler.RerankHandler(c, info, resp)
|
||||
} else if info.RelayMode == constant.RelayModeEmbeddings {
|
||||
err, usage = openai.OpenaiHandler(c, resp, info)
|
||||
}
|
||||
|
||||
@@ -262,7 +262,7 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
|
||||
case constant.RelayModeImagesGenerations:
|
||||
err, usage = OpenaiTTSHandler(c, resp, info)
|
||||
case constant.RelayModeRerank:
|
||||
err, usage = common_handler.RerankHandler(c, resp)
|
||||
err, usage = common_handler.RerankHandler(c, info, resp)
|
||||
default:
|
||||
if info.IsStream {
|
||||
err, usage = OaiStreamHandler(c, resp, info)
|
||||
|
||||
@@ -12,6 +12,6 @@ type SFMeta struct {
|
||||
}
|
||||
|
||||
type SFRerankResponse struct {
|
||||
Results []dto.RerankResponseDocument `json:"results"`
|
||||
Meta SFMeta `json:"meta"`
|
||||
Results []dto.RerankResponseResult `json:"results"`
|
||||
Meta SFMeta `json:"meta"`
|
||||
}
|
||||
|
||||
@@ -39,8 +39,15 @@ type Adaptor struct {
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) {
|
||||
return request, nil
|
||||
if v, ok := claudeModelMap[info.UpstreamModelName]; ok {
|
||||
c.Set("request_model", v)
|
||||
} else {
|
||||
c.Set("request_model", request.Model)
|
||||
}
|
||||
vertexClaudeReq := copyRequest(request, anthropicVersion)
|
||||
return vertexClaudeReq, nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
|
||||
//TODO implement me
|
||||
return nil, errors.New("not implemented")
|
||||
|
||||
11
relay/channel/xinference/dto.go
Normal file
11
relay/channel/xinference/dto.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package xinference
|
||||
|
||||
type XinRerankResponseDocument struct {
|
||||
Document string `json:"document,omitempty"`
|
||||
Index int `json:"index"`
|
||||
RelevanceScore float64 `json:"relevance_score"`
|
||||
}
|
||||
|
||||
type XinRerankResponse struct {
|
||||
Results []XinRerankResponseDocument `json:"results"`
|
||||
}
|
||||
@@ -33,6 +33,11 @@ const (
|
||||
RelayFormatClaude = "claude"
|
||||
)
|
||||
|
||||
type RerankerInfo struct {
|
||||
Documents []any
|
||||
ReturnDocuments bool
|
||||
}
|
||||
|
||||
type RelayInfo struct {
|
||||
ChannelType int
|
||||
ChannelId int
|
||||
@@ -71,6 +76,7 @@ type RelayInfo struct {
|
||||
AudioUsage bool
|
||||
ReasoningEffort string
|
||||
ChannelSetting map[string]interface{}
|
||||
ParamOverride map[string]interface{}
|
||||
UserSetting map[string]interface{}
|
||||
UserEmail string
|
||||
UserQuota int
|
||||
@@ -78,6 +84,7 @@ type RelayInfo struct {
|
||||
SendResponseCount int
|
||||
ThinkingContentInfo
|
||||
ClaudeConvertInfo
|
||||
*RerankerInfo
|
||||
}
|
||||
|
||||
// 定义支持流式选项的通道类型
|
||||
@@ -111,10 +118,21 @@ func GenRelayInfoClaude(c *gin.Context) *RelayInfo {
|
||||
return info
|
||||
}
|
||||
|
||||
func GenRelayInfoRerank(c *gin.Context, req *dto.RerankRequest) *RelayInfo {
|
||||
info := GenRelayInfo(c)
|
||||
info.RelayMode = relayconstant.RelayModeRerank
|
||||
info.RerankerInfo = &RerankerInfo{
|
||||
Documents: req.Documents,
|
||||
ReturnDocuments: req.GetReturnDocuments(),
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
func GenRelayInfo(c *gin.Context) *RelayInfo {
|
||||
channelType := c.GetInt("channel_type")
|
||||
channelId := c.GetInt("channel_id")
|
||||
channelSetting := c.GetStringMap("channel_setting")
|
||||
paramOverride := c.GetStringMap("param_override")
|
||||
|
||||
tokenId := c.GetInt("token_id")
|
||||
tokenKey := c.GetString("token_key")
|
||||
@@ -152,6 +170,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
|
||||
ApiKey: strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "),
|
||||
Organization: c.GetString("channel_organization"),
|
||||
ChannelSetting: channelSetting,
|
||||
ParamOverride: paramOverride,
|
||||
RelayFormat: RelayFormatOpenAI,
|
||||
ThinkingContentInfo: ThinkingContentInfo{
|
||||
IsFirstThinkingContent: true,
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
package common_handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/gin-gonic/gin"
|
||||
"io"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/dto"
|
||||
"one-api/relay/channel/xinference"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/service"
|
||||
)
|
||||
|
||||
func RerankHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
|
||||
func RerankHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
|
||||
@@ -18,18 +20,49 @@ func RerankHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithSta
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
||||
}
|
||||
if common.DebugEnabled {
|
||||
println("reranker response body: ", string(responseBody))
|
||||
}
|
||||
var jinaResp dto.RerankResponse
|
||||
err = json.Unmarshal(responseBody, &jinaResp)
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
|
||||
if info.ChannelType == common.ChannelTypeXinference {
|
||||
var xinRerankResponse xinference.XinRerankResponse
|
||||
err = common.DecodeJson(responseBody, &xinRerankResponse)
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
|
||||
}
|
||||
jinaRespResults := make([]dto.RerankResponseResult, len(xinRerankResponse.Results))
|
||||
for i, result := range xinRerankResponse.Results {
|
||||
respResult := dto.RerankResponseResult{
|
||||
Index: result.Index,
|
||||
RelevanceScore: result.RelevanceScore,
|
||||
}
|
||||
if info.ReturnDocuments {
|
||||
var document any
|
||||
if result.Document == "" {
|
||||
document = info.Documents[result.Index]
|
||||
} else {
|
||||
document = result.Document
|
||||
}
|
||||
respResult.Document = document
|
||||
}
|
||||
jinaRespResults[i] = respResult
|
||||
}
|
||||
jinaResp = dto.RerankResponse{
|
||||
Results: jinaRespResults,
|
||||
Usage: dto.Usage{
|
||||
PromptTokens: info.PromptTokens,
|
||||
TotalTokens: info.PromptTokens,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
err = common.DecodeJson(responseBody, &jinaResp)
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
|
||||
}
|
||||
jinaResp.Usage.PromptTokens = jinaResp.Usage.TotalTokens
|
||||
}
|
||||
|
||||
jsonResponse, err := json.Marshal(jinaResp)
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapper(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)
|
||||
c.JSON(http.StatusOK, jinaResp)
|
||||
return nil, &jinaResp.Usage
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"one-api/common"
|
||||
constant2 "one-api/constant"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/setting"
|
||||
"one-api/setting/operation_setting"
|
||||
@@ -20,6 +21,10 @@ type PriceData struct {
|
||||
ShouldPreConsumedQuota int
|
||||
}
|
||||
|
||||
func (p PriceData) ToSetting() string {
|
||||
return fmt.Sprintf("ModelPrice: %f, ModelRatio: %f, CompletionRatio: %f, CacheRatio: %f, GroupRatio: %f, UsePrice: %t, CacheCreationRatio: %f, ShouldPreConsumedQuota: %d", p.ModelPrice, p.ModelRatio, p.CompletionRatio, p.CacheRatio, p.GroupRatio, p.UsePrice, p.CacheCreationRatio, p.ShouldPreConsumedQuota)
|
||||
}
|
||||
|
||||
func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens int, maxTokens int) (PriceData, error) {
|
||||
modelPrice, usePrice := operation_setting.GetModelPrice(info.OriginModelName, false)
|
||||
groupRatio := setting.GetGroupRatio(info.Group)
|
||||
@@ -36,10 +41,19 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
|
||||
var success bool
|
||||
modelRatio, success = operation_setting.GetModelRatio(info.OriginModelName)
|
||||
if !success {
|
||||
if info.UserId == 1 {
|
||||
return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置,请设置或开始自用模式;Model %s ratio or price not set, please set or start self-use mode", info.OriginModelName, info.OriginModelName)
|
||||
} else {
|
||||
return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置, 请联系管理员设置;Model %s ratio or price not set, please contact administrator to set", info.OriginModelName, info.OriginModelName)
|
||||
acceptUnsetRatio := false
|
||||
if accept, ok := info.UserSetting[constant2.UserAcceptUnsetRatioModel]; ok {
|
||||
b, ok := accept.(bool)
|
||||
if ok {
|
||||
acceptUnsetRatio = b
|
||||
}
|
||||
}
|
||||
if !acceptUnsetRatio {
|
||||
if info.UserId == 1 {
|
||||
return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置,请设置或开始自用模式;Model %s ratio or price not set, please set or start self-use mode", info.OriginModelName, info.OriginModelName)
|
||||
} else {
|
||||
return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置, 请联系管理员设置;Model %s ratio or price not set, please contact administrator to set", info.OriginModelName, info.OriginModelName)
|
||||
}
|
||||
}
|
||||
}
|
||||
completionRatio = operation_setting.GetCompletionRatio(info.OriginModelName)
|
||||
@@ -50,7 +64,8 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
|
||||
} else {
|
||||
preConsumedQuota = int(modelPrice * common.QuotaPerUnit * groupRatio)
|
||||
}
|
||||
return PriceData{
|
||||
|
||||
priceData := PriceData{
|
||||
ModelPrice: modelPrice,
|
||||
ModelRatio: modelRatio,
|
||||
CompletionRatio: completionRatio,
|
||||
@@ -59,5 +74,11 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
|
||||
CacheRatio: cacheRatio,
|
||||
CacheCreationRatio: cacheCreationRatio,
|
||||
ShouldPreConsumedQuota: preConsumedQuota,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if common.DebugEnabled {
|
||||
println(fmt.Sprintf("model_price_helper result: %s", priceData.ToSetting()))
|
||||
}
|
||||
|
||||
return priceData, nil
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
|
||||
c.Set("prompt_tokens", promptTokens)
|
||||
}
|
||||
|
||||
priceData, err := helper.ModelPriceHelper(c, relayInfo, promptTokens, int(textRequest.MaxTokens))
|
||||
priceData, err := helper.ModelPriceHelper(c, relayInfo, promptTokens, int(math.Max(float64(textRequest.MaxTokens), float64(textRequest.MaxCompletionTokens))))
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapperLocal(err, "model_price_error", http.StatusInternalServerError)
|
||||
}
|
||||
@@ -168,6 +168,23 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapperLocal(err, "json_marshal_failed", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// apply param override
|
||||
if len(relayInfo.ParamOverride) > 0 {
|
||||
reqMap := make(map[string]interface{})
|
||||
err = json.Unmarshal(jsonData, &reqMap)
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapperLocal(err, "param_override_unmarshal_failed", http.StatusInternalServerError)
|
||||
}
|
||||
for key, value := range relayInfo.ParamOverride {
|
||||
reqMap[key] = value
|
||||
}
|
||||
jsonData, err = json.Marshal(reqMap)
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapperLocal(err, "param_override_marshal_failed", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
if common.DebugEnabled {
|
||||
println("requestBody: ", string(jsonData))
|
||||
}
|
||||
@@ -372,17 +389,18 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
|
||||
common.LogError(ctx, fmt.Sprintf("total tokens is 0, cannot consume quota, userId %d, channelId %d, "+
|
||||
"tokenId %d, model %s, pre-consumed quota %d", relayInfo.UserId, relayInfo.ChannelId, relayInfo.TokenId, modelName, preConsumedQuota))
|
||||
} else {
|
||||
quotaDelta := quota - preConsumedQuota
|
||||
if quotaDelta != 0 {
|
||||
err := service.PostConsumeQuota(relayInfo, quotaDelta, preConsumedQuota, true)
|
||||
if err != nil {
|
||||
common.LogError(ctx, "error consuming token remain quota: "+err.Error())
|
||||
}
|
||||
}
|
||||
model.UpdateUserUsedQuotaAndRequestCount(relayInfo.UserId, quota)
|
||||
model.UpdateChannelUsedQuota(relayInfo.ChannelId, quota)
|
||||
}
|
||||
|
||||
quotaDelta := quota - preConsumedQuota
|
||||
if quotaDelta != 0 {
|
||||
err := service.PostConsumeQuota(relayInfo, quotaDelta, preConsumedQuota, true)
|
||||
if err != nil {
|
||||
common.LogError(ctx, "error consuming token remain quota: "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
logModel := modelName
|
||||
if strings.HasPrefix(logModel, "gpt-4-gizmo") {
|
||||
logModel = "gpt-4-gizmo-*"
|
||||
|
||||
@@ -25,7 +25,6 @@ func getRerankPromptToken(rerankRequest dto.RerankRequest) int {
|
||||
}
|
||||
|
||||
func RerankHelper(c *gin.Context, relayMode int) (openaiErr *dto.OpenAIErrorWithStatusCode) {
|
||||
relayInfo := relaycommon.GenRelayInfo(c)
|
||||
|
||||
var rerankRequest *dto.RerankRequest
|
||||
err := common.UnmarshalBodyReusable(c, &rerankRequest)
|
||||
@@ -33,6 +32,9 @@ func RerankHelper(c *gin.Context, relayMode int) (openaiErr *dto.OpenAIErrorWith
|
||||
common.LogError(c, fmt.Sprintf("getAndValidateTextRequest failed: %s", err.Error()))
|
||||
return service.OpenAIErrorWrapperLocal(err, "invalid_text_request", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
relayInfo := relaycommon.GenRelayInfoRerank(c, rerankRequest)
|
||||
|
||||
if rerankRequest.Query == "" {
|
||||
return service.OpenAIErrorWrapperLocal(fmt.Errorf("query is empty"), "invalid_query", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ func SetApiRouter(router *gin.Engine) {
|
||||
apiRouter.Use(gzip.Gzip(gzip.DefaultCompression))
|
||||
apiRouter.Use(middleware.GlobalAPIRateLimit())
|
||||
{
|
||||
apiRouter.GET("/setup", controller.GetSetup)
|
||||
apiRouter.POST("/setup", controller.PostSetup)
|
||||
apiRouter.GET("/status", controller.GetStatus)
|
||||
apiRouter.GET("/models", middleware.UserAuth(), controller.DashboardListModels)
|
||||
apiRouter.GET("/status/test", middleware.AdminAuth(), controller.TestStatus)
|
||||
|
||||
@@ -243,20 +243,18 @@ func PostClaudeConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
|
||||
common.LogError(ctx, fmt.Sprintf("total tokens is 0, cannot consume quota, userId %d, channelId %d, "+
|
||||
"tokenId %d, model %s, pre-consumed quota %d", relayInfo.UserId, relayInfo.ChannelId, relayInfo.TokenId, modelName, preConsumedQuota))
|
||||
} else {
|
||||
//if sensitiveResp != nil {
|
||||
// logContent += fmt.Sprintf(",敏感词:%s", strings.Join(sensitiveResp.SensitiveWords, ", "))
|
||||
//}
|
||||
quotaDelta := quota - preConsumedQuota
|
||||
if quotaDelta != 0 {
|
||||
err := PostConsumeQuota(relayInfo, quotaDelta, preConsumedQuota, true)
|
||||
if err != nil {
|
||||
common.LogError(ctx, "error consuming token remain quota: "+err.Error())
|
||||
}
|
||||
}
|
||||
model.UpdateUserUsedQuotaAndRequestCount(relayInfo.UserId, quota)
|
||||
model.UpdateChannelUsedQuota(relayInfo.ChannelId, quota)
|
||||
}
|
||||
|
||||
quotaDelta := quota - preConsumedQuota
|
||||
if quotaDelta != 0 {
|
||||
err := PostConsumeQuota(relayInfo, quotaDelta, preConsumedQuota, true)
|
||||
if err != nil {
|
||||
common.LogError(ctx, "error consuming token remain quota: "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
other := GenerateClaudeOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio,
|
||||
cacheTokens, cacheRatio, cacheCreationTokens, cacheCreationRatio, modelPrice)
|
||||
model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, promptTokens, completionTokens, modelName,
|
||||
@@ -318,17 +316,18 @@ func PostAudioConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
|
||||
common.LogError(ctx, fmt.Sprintf("total tokens is 0, cannot consume quota, userId %d, channelId %d, "+
|
||||
"tokenId %d, model %s, pre-consumed quota %d", relayInfo.UserId, relayInfo.ChannelId, relayInfo.TokenId, relayInfo.OriginModelName, preConsumedQuota))
|
||||
} else {
|
||||
quotaDelta := quota - preConsumedQuota
|
||||
if quotaDelta != 0 {
|
||||
err := PostConsumeQuota(relayInfo, quotaDelta, preConsumedQuota, true)
|
||||
if err != nil {
|
||||
common.LogError(ctx, "error consuming token remain quota: "+err.Error())
|
||||
}
|
||||
}
|
||||
model.UpdateUserUsedQuotaAndRequestCount(relayInfo.UserId, quota)
|
||||
model.UpdateChannelUsedQuota(relayInfo.ChannelId, quota)
|
||||
}
|
||||
|
||||
quotaDelta := quota - preConsumedQuota
|
||||
if quotaDelta != 0 {
|
||||
err := PostConsumeQuota(relayInfo, quotaDelta, preConsumedQuota, true)
|
||||
if err != nil {
|
||||
common.LogError(ctx, "error consuming token remain quota: "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
logModel := relayInfo.OriginModelName
|
||||
if extraContent != "" {
|
||||
logContent += ", " + extraContent
|
||||
|
||||
@@ -14,6 +14,8 @@ var defaultCacheRatio = map[string]float64{
|
||||
"o1-preview": 0.5,
|
||||
"o1-mini-2024-09-12": 0.5,
|
||||
"o1-mini": 0.5,
|
||||
"o3-mini": 0.5,
|
||||
"o3-mini-2025-01-31": 0.5,
|
||||
"gpt-4o-2024-11-20": 0.5,
|
||||
"gpt-4o-2024-08-06": 0.5,
|
||||
"gpt-4o": 0.5,
|
||||
@@ -21,6 +23,8 @@ var defaultCacheRatio = map[string]float64{
|
||||
"gpt-4o-mini": 0.5,
|
||||
"gpt-4o-realtime-preview": 0.5,
|
||||
"gpt-4o-mini-realtime-preview": 0.5,
|
||||
"gpt-4.5-preview": 0.5,
|
||||
"gpt-4.5-preview-2025-02-27": 0.5,
|
||||
"deepseek-chat": 0.25,
|
||||
"deepseek-reasoner": 0.25,
|
||||
"deepseek-coder": 0.25,
|
||||
|
||||
@@ -131,17 +131,12 @@ var defaultModelRatio = map[string]float64{
|
||||
"bge-large-en": 0.002 * RMB,
|
||||
"tao-8k": 0.002 * RMB,
|
||||
"PaLM-2": 1,
|
||||
"gemini-pro": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens
|
||||
"gemini-pro-vision": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens
|
||||
"gemini-1.0-pro-vision-001": 1,
|
||||
"gemini-1.0-pro-001": 1,
|
||||
"gemini-1.5-pro-latest": 1.75, // $3.5 / 1M tokens
|
||||
"gemini-1.5-pro-exp-0827": 1.75, // $3.5 / 1M tokens
|
||||
"gemini-1.5-flash-latest": 1,
|
||||
"gemini-1.5-flash-exp-0827": 1,
|
||||
"gemini-1.0-pro-latest": 1,
|
||||
"gemini-1.0-pro-vision-latest": 1,
|
||||
"gemini-ultra": 1,
|
||||
"gemini-1.5-pro-latest": 1.25, // $3.5 / 1M tokens
|
||||
"gemini-1.5-flash-latest": 0.075,
|
||||
"gemini-2.0-flash": 0.05,
|
||||
"gemini-2.5-pro-exp-03-25": 1.25,
|
||||
"gemini-2.5-pro-preview-03-25": 1.25,
|
||||
"text-embedding-004": 0.001,
|
||||
"chatglm_turbo": 0.3572, // ¥0.005 / 1k tokens
|
||||
"chatglm_pro": 0.7143, // ¥0.01 / 1k tokens
|
||||
"chatglm_std": 0.3572, // ¥0.005 / 1k tokens
|
||||
@@ -207,26 +202,27 @@ var defaultModelRatio = map[string]float64{
|
||||
}
|
||||
|
||||
var defaultModelPrice = map[string]float64{
|
||||
"suno_music": 0.1,
|
||||
"suno_lyrics": 0.01,
|
||||
"dall-e-3": 0.04,
|
||||
"gpt-4-gizmo-*": 0.1,
|
||||
"mj_imagine": 0.1,
|
||||
"mj_variation": 0.1,
|
||||
"mj_reroll": 0.1,
|
||||
"mj_blend": 0.1,
|
||||
"mj_modal": 0.1,
|
||||
"mj_zoom": 0.1,
|
||||
"mj_shorten": 0.1,
|
||||
"mj_high_variation": 0.1,
|
||||
"mj_low_variation": 0.1,
|
||||
"mj_pan": 0.1,
|
||||
"mj_inpaint": 0,
|
||||
"mj_custom_zoom": 0,
|
||||
"mj_describe": 0.05,
|
||||
"mj_upscale": 0.05,
|
||||
"swap_face": 0.05,
|
||||
"mj_upload": 0.05,
|
||||
"suno_music": 0.1,
|
||||
"suno_lyrics": 0.01,
|
||||
"dall-e-3": 0.04,
|
||||
"imagen-3.0-generate-002": 0.03,
|
||||
"gpt-4-gizmo-*": 0.1,
|
||||
"mj_imagine": 0.1,
|
||||
"mj_variation": 0.1,
|
||||
"mj_reroll": 0.1,
|
||||
"mj_blend": 0.1,
|
||||
"mj_modal": 0.1,
|
||||
"mj_zoom": 0.1,
|
||||
"mj_shorten": 0.1,
|
||||
"mj_high_variation": 0.1,
|
||||
"mj_low_variation": 0.1,
|
||||
"mj_pan": 0.1,
|
||||
"mj_inpaint": 0,
|
||||
"mj_custom_zoom": 0,
|
||||
"mj_describe": 0.05,
|
||||
"mj_upscale": 0.05,
|
||||
"swap_face": 0.05,
|
||||
"mj_upload": 0.05,
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -375,6 +371,17 @@ func GetCompletionRatio(name string) float64 {
|
||||
return ratio
|
||||
}
|
||||
}
|
||||
hardCodedRatio, contain := getHardcodedCompletionModelRatio(name)
|
||||
if contain {
|
||||
return hardCodedRatio
|
||||
}
|
||||
if ratio, ok := CompletionRatio[name]; ok {
|
||||
return ratio
|
||||
}
|
||||
return hardCodedRatio
|
||||
}
|
||||
|
||||
func getHardcodedCompletionModelRatio(name string) (float64, bool) {
|
||||
lowercaseName := strings.ToLower(name)
|
||||
if strings.HasPrefix(name, "gpt-4-gizmo") {
|
||||
name = "gpt-4-gizmo-*"
|
||||
@@ -385,87 +392,93 @@ func GetCompletionRatio(name string) float64 {
|
||||
if strings.HasPrefix(name, "gpt-4") && !strings.HasSuffix(name, "-all") && !strings.HasSuffix(name, "-gizmo-*") {
|
||||
if strings.HasPrefix(name, "gpt-4o") {
|
||||
if name == "gpt-4o-2024-05-13" {
|
||||
return 3
|
||||
return 3, true
|
||||
}
|
||||
return 4
|
||||
return 4, true
|
||||
}
|
||||
if strings.HasPrefix(name, "gpt-4.5") {
|
||||
return 2
|
||||
// gpt-4.5-preview匹配
|
||||
if strings.HasPrefix(name, "gpt-4.5-preview") {
|
||||
return 2, true
|
||||
}
|
||||
if strings.HasPrefix(name, "gpt-4-turbo") || strings.HasSuffix(name, "preview") {
|
||||
return 3
|
||||
if strings.HasPrefix(name, "gpt-4-turbo") || strings.HasSuffix(name, "gpt-4-1106") || strings.HasSuffix(name, "gpt-4-1105") {
|
||||
return 3, true
|
||||
}
|
||||
return 2
|
||||
// 没有特殊标记的 gpt-4 模型默认倍率为 2
|
||||
return 2, false
|
||||
}
|
||||
if strings.HasPrefix(name, "o1") || strings.HasPrefix(name, "o3") {
|
||||
return 4
|
||||
return 4, true
|
||||
}
|
||||
if name == "chatgpt-4o-latest" {
|
||||
return 3
|
||||
return 3, true
|
||||
}
|
||||
if strings.Contains(name, "claude-instant-1") {
|
||||
return 3
|
||||
return 3, true
|
||||
} else if strings.Contains(name, "claude-2") {
|
||||
return 3
|
||||
return 3, true
|
||||
} else if strings.Contains(name, "claude-3") {
|
||||
return 5
|
||||
return 5, true
|
||||
}
|
||||
if strings.HasPrefix(name, "gpt-3.5") {
|
||||
if name == "gpt-3.5-turbo" || strings.HasSuffix(name, "0125") {
|
||||
// https://openai.com/blog/new-embedding-models-and-api-updates
|
||||
// Updated GPT-3.5 Turbo model and lower pricing
|
||||
return 3
|
||||
return 3, true
|
||||
}
|
||||
if strings.HasSuffix(name, "1106") {
|
||||
return 2
|
||||
return 2, true
|
||||
}
|
||||
return 4.0 / 3.0
|
||||
return 4.0 / 3.0, true
|
||||
}
|
||||
if strings.HasPrefix(name, "mistral-") {
|
||||
return 3
|
||||
return 3, true
|
||||
}
|
||||
if strings.HasPrefix(name, "gemini-") {
|
||||
return 4
|
||||
if strings.HasPrefix(name, "gemini-1.5") {
|
||||
return 4, true
|
||||
} else if strings.HasPrefix(name, "gemini-2.0") {
|
||||
return 4, true
|
||||
} else if strings.HasPrefix(name, "gemini-2.5-pro-preview") {
|
||||
return 6, true
|
||||
}
|
||||
return 4, false
|
||||
}
|
||||
if strings.HasPrefix(name, "command") {
|
||||
switch name {
|
||||
case "command-r":
|
||||
return 3
|
||||
return 3, true
|
||||
case "command-r-plus":
|
||||
return 5
|
||||
return 5, true
|
||||
case "command-r-08-2024":
|
||||
return 4
|
||||
return 4, true
|
||||
case "command-r-plus-08-2024":
|
||||
return 4
|
||||
return 4, true
|
||||
default:
|
||||
return 4
|
||||
return 4, false
|
||||
}
|
||||
}
|
||||
// hint 只给官方上4倍率,由于开源模型供应商自行定价,不对其进行补全倍率进行强制对齐
|
||||
if lowercaseName == "deepseek-chat" || lowercaseName == "deepseek-reasoner" {
|
||||
return 4
|
||||
return 4, true
|
||||
}
|
||||
if strings.HasPrefix(name, "ERNIE-Speed-") {
|
||||
return 2
|
||||
return 2, true
|
||||
} else if strings.HasPrefix(name, "ERNIE-Lite-") {
|
||||
return 2
|
||||
return 2, true
|
||||
} else if strings.HasPrefix(name, "ERNIE-Character") {
|
||||
return 2
|
||||
return 2, true
|
||||
} else if strings.HasPrefix(name, "ERNIE-Functions") {
|
||||
return 2
|
||||
return 2, true
|
||||
}
|
||||
switch name {
|
||||
case "llama2-70b-4096":
|
||||
return 0.8 / 0.64
|
||||
return 0.8 / 0.64, true
|
||||
case "llama3-8b-8192":
|
||||
return 2
|
||||
return 2, true
|
||||
case "llama3-70b-8192":
|
||||
return 0.79 / 0.59
|
||||
return 0.79 / 0.59, true
|
||||
}
|
||||
if ratio, ok := CompletionRatio[name]; ok {
|
||||
return ratio
|
||||
}
|
||||
return 1
|
||||
return 1, false
|
||||
}
|
||||
|
||||
func GetAudioRatio(name string) float64 {
|
||||
|
||||
5319
web/pnpm-lock.yaml
generated
5319
web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,7 @@ import Task from "./pages/Task/index.js";
|
||||
import Playground from './pages/Playground/Playground.js';
|
||||
import OAuth2Callback from "./components/OAuth2Callback.js";
|
||||
import PersonalSetting from './components/PersonalSetting.js';
|
||||
import Setup from './pages/Setup/index.js';
|
||||
|
||||
const Home = lazy(() => import('./pages/Home'));
|
||||
const Detail = lazy(() => import('./pages/Detail'));
|
||||
@@ -44,6 +45,14 @@ function App() {
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/setup'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
|
||||
<Setup />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/channel'
|
||||
element={
|
||||
|
||||
@@ -6,17 +6,27 @@ const LinuxDoIcon = (props) => {
|
||||
return (
|
||||
<svg
|
||||
className='icon'
|
||||
viewBox='0 0 24 24'
|
||||
viewBox='0 0 16 16'
|
||||
version='1.1'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='1em'
|
||||
height='1em'
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d='M19.7,17.6c-0.1-0.2-0.2-0.4-0.2-0.6c0-0.4-0.2-0.7-0.5-1c-0.1-0.1-0.3-0.2-0.4-0.2c0.6-1.8-0.3-3.6-1.3-4.9c0,0,0,0,0,0c-0.8-1.2-2-2.1-1.9-3.7c0-1.9,0.2-5.4-3.3-5.1C8.5,2.3,9.5,6,9.4,7.3c0,1.1-0.5,2.2-1.3,3.1c-0.2,0.2-0.4,0.5-0.5,0.7c-1,1.2-1.5,2.8-1.5,4.3c-0.2,0.2-0.4,0.4-0.5,0.6c-0.1,0.1-0.2,0.2-0.2,0.3c-0.1,0.1-0.3,0.2-0.5,0.3c-0.4,0.1-0.7,0.3-0.9,0.7c-0.1,0.3-0.2,0.7-0.1,1.1c0.1,0.2,0.1,0.4,0,0.7c-0.2,0.4-0.2,0.9,0,1.4c0.3,0.4,0.8,0.5,1.5,0.6c0.5,0,1.1,0.2,1.6,0.4l0,0c0.5,0.3,1.1,0.5,1.7,0.5c0.3,0,0.7-0.1,1-0.2c0.3-0.2,0.5-0.4,0.6-0.7c0.4,0,1-0.2,1.7-0.2c0.6,0,1.2,0.2,2,0.1c0,0.1,0,0.2,0.1,0.3c0.2,0.5,0.7,0.9,1.3,1c0.1,0,0.1,0,0.2,0c0.8-0.1,1.6-0.5,2.1-1.1l0,0c0.4-0.4,0.9-0.7,1.4-0.9c0.6-0.3,1-0.5,1.1-1C20.3,18.6,20.1,18.2,19.7,17.6z M12.8,4.8c0.6,0.1,1.1,0.6,1,1.2c0,0.3-0.1,0.6-0.3,0.9c0,0,0,0-0.1,0c-0.2-0.1-0.3-0.1-0.4-0.2c0.1-0.1,0.1-0.3,0.2-0.5c0-0.4-0.2-0.7-0.4-0.7c-0.3,0-0.5,0.3-0.5,0.7c0,0,0,0.1,0,0.1c-0.1-0.1-0.3-0.1-0.4-0.2c0,0,0-0.1,0-0.1C11.8,5.5,12.2,4.9,12.8,4.8z M12.5,6.8c0.1,0.1,0.3,0.2,0.4,0.2c0.1,0,0.3,0.1,0.4,0.2c0.2,0.1,0.4,0.2,0.4,0.5c0,0.3-0.3,0.6-0.9,0.8c-0.2,0.1-0.3,0.1-0.4,0.2c-0.3,0.2-0.6,0.3-1,0.3c-0.3,0-0.6-0.2-0.8-0.4c-0.1-0.1-0.2-0.2-0.4-0.3C10.1,8.2,9.9,8,9.8,7.7c0-0.1,0.1-0.2,0.2-0.3c0.3-0.2,0.4-0.3,0.5-0.4l0.1-0.1c0.2-0.3,0.6-0.5,1-0.5C11.9,6.5,12.2,6.6,12.5,6.8z M10.4,5c0.4,0,0.7,0.4,0.8,1.1c0,0.1,0,0.1,0,0.2c-0.1,0-0.3,0.1-0.4,0.2c0,0,0-0.1,0-0.2c0-0.3-0.2-0.6-0.4-0.5c-0.2,0-0.3,0.3-0.3,0.6c0,0.2,0.1,0.3,0.2,0.4l0,0c0,0-0.1,0.1-0.2,0.1C9.9,6.7,9.7,6.4,9.7,6.1C9.7,5.5,10,5,10.4,5z M9.4,21.1c-0.7,0.3-1.6,0.2-2.2-0.2c-0.6-0.3-1.1-0.4-1.8-0.4c-0.5-0.1-1-0.1-1.1-0.3c-0.1-0.2-0.1-0.5,0.1-1c0.1-0.3,0.1-0.6,0-0.9c-0.1-0.3-0.1-0.5,0-0.8C4.5,17.2,4.7,17.1,5,17c0.3-0.1,0.5-0.2,0.7-0.4c0.1-0.1,0.2-0.2,0.3-0.4c0.3-0.4,0.5-0.6,0.8-0.6c0.6,0.1,1.1,1,1.5,1.9c0.2,0.3,0.4,0.7,0.7,1c0.4,0.5,0.9,1.2,0.9,1.6C9.9,20.6,9.7,20.9,9.4,21.1z M14.3,18.9c0,0.1,0,0.1-0.1,0.2c-1.2,0.9-2.8,1-4.1,0.3c-0.2-0.3-0.4-0.6-0.6-0.9c0.9-0.1,0.7-1.3-1.2-2.5c-2-1.3-0.6-3.7,0.1-4.8c0.1-0.1,0.1,0-0.3,0.8c-0.3,0.6-0.9,2.1-0.1,3.2c0-0.8,0.2-1.6,0.5-2.4c0.7-1.3,1.2-2.8,1.5-4.3c0.1,0.1,0.1,0.1,0.2,0.1c0.1,0.1,0.2,0.2,0.3,0.2c0.2,0.3,0.6,0.4,0.9,0.4c0,0,0.1,0,0.1,0c0.4,0,0.8-0.1,1.1-0.4c0.1-0.1,0.2-0.2,0.4-0.2c0.3-0.1,0.6-0.3,0.9-0.6c0.4,1.3,0.8,2.5,1.4,3.6c0.4,0.8,0.7,1.6,0.9,2.5c0.3,0,0.7,0.1,1,0.3c0.8,0.4,1.1,0.7,1,1.2c-0.1,0-0.1,0-0.2,0c0-0.3-0.2-0.6-0.9-0.9c-0.7-0.3-1.3-0.3-1.5,0.4c-0.1,0-0.2,0.1-0.3,0.1c-0.8,0.4-0.8,1.5-0.9,2.6C14.5,18.2,14.4,18.5,14.3,18.9z M18.9,19.5c-0.6,0.2-1.1,0.6-1.5,1.1c-0.4,0.6-1.1,1-1.9,0.9c-0.4,0-0.8-0.3-0.9-0.7c-0.1-0.6-0.1-1.2,0.2-1.8c0.1-0.4,0.2-0.7,0.3-1.1c0.1-1.2,0.1-1.9,0.6-2.2h0c0,0.5,0.3,0.8,0.7,1c0.5,0,1-0.1,1.4-0.5c0.1,0,0.1,0,0.2,0c0.3,0,0.5,0,0.7,0.2c0.2,0.2,0.3,0.5,0.3,0.7c0,0.3,0.2,0.6,0.3,0.9c0.5,0.5,0.5,0.8,0.5,0.9C19.7,19.1,19.3,19.3,18.9,19.5z M9.9,7.5c-0.1,0-0.1,0-0.1,0.1c0,0,0,0.1,0.1,0.1c0,0,0,0,0,0c0.1,0,0.1,0.1,0.1,0.1c0.3,0.4,0.8,0.6,1.4,0.7c0.5-0.1,1-0.2,1.5-0.6c0.2-0.1,0.4-0.2,0.6-0.3c0.1,0,0.1-0.1,0.1-0.1c0-0.1,0-0.1-0.1-0.1l0,0c-0.2,0.1-0.5,0.2-0.7,0.3c-0.4,0.3-0.9,0.5-1.4,0.5c-0.5,0-0.9-0.3-1.2-0.6C10.1,7.6,10,7.5,9.9,7.5z'
|
||||
fill='currentColor'
|
||||
/>
|
||||
<g id='linuxdo_icon' data-name='linuxdo_icon'>
|
||||
<path
|
||||
d='m7.44,0s.09,0,.13,0c.09,0,.19,0,.28,0,.14,0,.29,0,.43,0,.09,0,.18,0,.27,0q.12,0,.25,0t.26.08c.15.03.29.06.44.08,1.97.38,3.78,1.47,4.95,3.11.04.06.09.12.13.18.67.96,1.15,2.11,1.3,3.28q0,.19.09.26c0,.15,0,.29,0,.44,0,.04,0,.09,0,.13,0,.09,0,.19,0,.28,0,.14,0,.29,0,.43,0,.09,0,.18,0,.27,0,.08,0,.17,0,.25q0,.19-.08.26c-.03.15-.06.29-.08.44-.38,1.97-1.47,3.78-3.11,4.95-.06.04-.12.09-.18.13-.96.67-2.11,1.15-3.28,1.3q-.19,0-.26.09c-.15,0-.29,0-.44,0-.04,0-.09,0-.13,0-.09,0-.19,0-.28,0-.14,0-.29,0-.43,0-.09,0-.18,0-.27,0-.08,0-.17,0-.25,0q-.19,0-.26-.08c-.15-.03-.29-.06-.44-.08-1.97-.38-3.78-1.47-4.95-3.11q-.07-.09-.13-.18c-.67-.96-1.15-2.11-1.3-3.28q0-.19-.09-.26c0-.15,0-.29,0-.44,0-.04,0-.09,0-.13,0-.09,0-.19,0-.28,0-.14,0-.29,0-.43,0-.09,0-.18,0-.27,0-.08,0-.17,0-.25q0-.19.08-.26c.03-.15.06-.29.08-.44.38-1.97,1.47-3.78,3.11-4.95.06-.04.12-.09.18-.13C4.42.73,5.57.26,6.74.1,7,.07,7.15,0,7.44,0Z'
|
||||
fill='#EFEFEF'
|
||||
/>
|
||||
<path
|
||||
d='m1.27,11.33h13.45c-.94,1.89-2.51,3.21-4.51,3.88-1.99.59-3.96.37-5.8-.57-1.25-.7-2.67-1.9-3.14-3.3Z'
|
||||
fill='#FEB005'
|
||||
/>
|
||||
<path
|
||||
d='m12.54,1.99c.87.7,1.82,1.59,2.18,2.68H1.27c.87-1.74,2.33-3.13,4.2-3.78,2.44-.79,5-.47,7.07,1.1Z'
|
||||
fill='#1D1D1F'
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -24,4 +34,4 @@ const LinuxDoIcon = (props) => {
|
||||
return <Icon svg={<CustomIcon />} />;
|
||||
};
|
||||
|
||||
export default LinuxDoIcon;
|
||||
export default LinuxDoIcon;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -60,6 +60,9 @@ export const StyleProvider = ({ children }) => {
|
||||
if (pathname === '' || pathname === '/' || pathname.includes('/home') || pathname.includes('/chat')) {
|
||||
dispatch({ type: 'SET_SIDER', payload: false });
|
||||
dispatch({ type: 'SET_INNER_PADDING', payload: false });
|
||||
} else if (pathname === '/setup') {
|
||||
dispatch({ type: 'SET_SIDER', payload: false });
|
||||
dispatch({ type: 'SET_INNER_PADDING', payload: false });
|
||||
} else {
|
||||
// Only show sidebar on non-mobile devices by default
|
||||
dispatch({ type: 'SET_SIDER', payload: !isMobile() });
|
||||
|
||||
@@ -1275,6 +1275,7 @@
|
||||
"代理站地址": "Base URL",
|
||||
"对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "For official channels, the new-api has a built-in address. Unless it is a third-party proxy site or a special Azure access address, there is no need to fill it in",
|
||||
"渠道额外设置": "Channel extra settings",
|
||||
"参数覆盖": "Parameters override",
|
||||
"模型请求速率限制": "Model request rate limit",
|
||||
"启用用户模型请求速率限制(可能会影响高并发性能)": "Enable user model request rate limit (may affect high concurrency performance)",
|
||||
"限制周期": "Limit period",
|
||||
@@ -1345,5 +1346,26 @@
|
||||
"提示缓存倍率": "Prompt cache ratio",
|
||||
"缓存:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "Cache: ${{price}} * {{ratio}} = ${{total}} / 1M tokens (cache ratio: {{cacheRatio}})",
|
||||
"提示 {{nonCacheInput}} tokens + 缓存 {{cacheInput}} tokens * {{cacheRatio}} / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}": "Prompt {{nonCacheInput}} tokens + cache {{cacheInput}} tokens * {{cacheRatio}} / 1M tokens * ${{price}} + completion {{completion}} tokens / 1M tokens * ${{compPrice}} * group {{ratio}} = ${{total}}",
|
||||
"缓存 Tokens": "Cache Tokens"
|
||||
"缓存 Tokens": "Cache Tokens",
|
||||
"系统初始化": "System initialization",
|
||||
"管理员账号已经初始化过,请继续设置系统参数": "The admin account has already been initialized, please continue to set the system parameters",
|
||||
"管理员账号": "Admin account",
|
||||
"请输入管理员用户名": "Please enter the admin username",
|
||||
"请输入管理员密码": "Please enter the admin password",
|
||||
"请确认管理员密码": "Please confirm the admin password",
|
||||
"请选择使用模式": "Please select the usage mode",
|
||||
"数据库警告": "Database warning",
|
||||
"您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!": "You are using the SQLite database. If you are running in a container environment, please ensure that the database file persistence mapping is correctly set, otherwise all data will be lost after container restart!",
|
||||
"建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。": "It is recommended to use MySQL or PostgreSQL databases in production environments, or ensure that the SQLite database file is mapped to the persistent storage of the host machine.",
|
||||
"使用模式": "Usage mode",
|
||||
"对外运营模式": "Default mode",
|
||||
"密码长度至少为8个字符": "Password must be at least 8 characters long",
|
||||
"表单引用错误,请刷新页面重试": "Form reference error, please refresh the page and try again",
|
||||
"默认模式,适用于为多个用户提供服务的场景。": "Default mode, suitable for scenarios where multiple users are provided.",
|
||||
"此模式下,系统将计算每次调用的用量,您需要对每个模型都设置价格,如果没有设置价格,用户将无法使用该模型。": "In this mode, the system will calculate the usage of each call, you need to set the price for each model, if the price is not set, the user will not be able to use the model.",
|
||||
"适用于个人使用的场景。": "Suitable for personal use.",
|
||||
"不需要设置模型价格,系统将弱化用量计算,您可专注于使用模型。": "No need to set the model price, the system will weaken the usage calculation, you can focus on using the model.",
|
||||
"适用于展示系统功能的场景。": "Suitable for scenarios where the system functions are displayed.",
|
||||
"可在初始化后修改": "Can be modified after initialization",
|
||||
"初始化系统": "Initialize system"
|
||||
}
|
||||
|
||||
@@ -983,6 +983,23 @@ const EditChannel = (props) => {
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</>
|
||||
<>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>
|
||||
{t('参数覆盖')}:
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<TextArea
|
||||
placeholder={t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数。为一个 JSON 字符串,例如:') + '\n{\n "temperature": 0\n}'}
|
||||
name="setting"
|
||||
onChange={(value) => {
|
||||
handleInputChange('param_override', value);
|
||||
}}
|
||||
autosize
|
||||
value={inputs.param_override}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</>
|
||||
{inputs.type === 1 && (
|
||||
<>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
|
||||
@@ -66,6 +66,10 @@ const Home = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (statusState.status?.setup === false) {
|
||||
window.location.href = '/setup';
|
||||
return;
|
||||
}
|
||||
displayNotice().then();
|
||||
displayHomePageContent().then();
|
||||
});
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// ModelSettingsVisualEditor.js
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Table, Button, Input, Modal, Form, Space } from '@douyinfe/semi-ui';
|
||||
import { IconDelete, IconPlus, IconSearch, IconSave } from '@douyinfe/semi-icons';
|
||||
import React, { useContext, useEffect, useState, useRef } from 'react';
|
||||
import { Table, Button, Input, Modal, Form, Space, RadioGroup, Radio, Tabs, TabPane } from '@douyinfe/semi-ui';
|
||||
import { IconDelete, IconPlus, IconSearch, IconSave, IconEdit } from '@douyinfe/semi-icons';
|
||||
import { showError, showSuccess } from '../../../helpers';
|
||||
import { API } from '../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StatusContext } from '../../../context/Status/index.js';
|
||||
import { getQuotaPerUnit } from '../../../helpers/render.js';
|
||||
|
||||
export default function ModelSettingsVisualEditor(props) {
|
||||
const { t } = useTranslation();
|
||||
@@ -14,7 +16,11 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [pricingMode, setPricingMode] = useState('per-token'); // 'per-token' or 'per-request'
|
||||
const [pricingSubMode, setPricingSubMode] = useState('ratio'); // 'ratio' or 'token-price'
|
||||
const formRef = useRef(null);
|
||||
const pageSize = 10;
|
||||
const quotaPerUnit = getQuotaPerUnit()
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
@@ -171,11 +177,19 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
title: t('操作'),
|
||||
key: 'action',
|
||||
render: (_, record) => (
|
||||
<Button
|
||||
icon={<IconDelete />}
|
||||
type="danger"
|
||||
onClick={() => deleteModel(record.name)}
|
||||
/>
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<IconEdit />}
|
||||
onClick={() => editModel(record)}
|
||||
>
|
||||
</Button>
|
||||
<Button
|
||||
icon={<IconDelete />}
|
||||
type="danger"
|
||||
onClick={() => deleteModel(record.name)}
|
||||
/>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
];
|
||||
@@ -197,28 +211,171 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
const deleteModel = (name) => {
|
||||
setModels(prev => prev.filter(model => model.name !== name));
|
||||
};
|
||||
const addModel = (values) => {
|
||||
// 检查模型名称是否存在, 如果存在则拒绝添加
|
||||
if (models.some(model => model.name === values.name)) {
|
||||
showError('模型名称已存在');
|
||||
return;
|
||||
|
||||
const calculateRatioFromTokenPrice = (tokenPrice) => {
|
||||
return tokenPrice / 2;
|
||||
};
|
||||
|
||||
const calculateCompletionRatioFromPrices = (modelTokenPrice, completionTokenPrice) => {
|
||||
if (!modelTokenPrice || modelTokenPrice === '0') {
|
||||
showError('模型价格不能为0');
|
||||
return '';
|
||||
}
|
||||
setModels(prev => [{
|
||||
name: values.name,
|
||||
price: values.price || '',
|
||||
ratio: values.ratio || '',
|
||||
completionRatio: values.completionRatio || ''
|
||||
}, ...prev]);
|
||||
setVisible(false);
|
||||
showSuccess('添加成功');
|
||||
return completionTokenPrice / modelTokenPrice;
|
||||
};
|
||||
|
||||
const handleTokenPriceChange = (value) => {
|
||||
|
||||
// Use a temporary variable to hold the new state
|
||||
let newState = {
|
||||
...(currentModel || {}),
|
||||
tokenPrice: value,
|
||||
ratio: 0
|
||||
};
|
||||
|
||||
if (!isNaN(value) && value !== '') {
|
||||
const tokenPrice = parseFloat(value);
|
||||
const ratio = calculateRatioFromTokenPrice(tokenPrice);
|
||||
newState.ratio = ratio;
|
||||
}
|
||||
|
||||
// Set the state with the complete updated object
|
||||
setCurrentModel(newState);
|
||||
};
|
||||
|
||||
const handleCompletionTokenPriceChange = (value) => {
|
||||
|
||||
// Use a temporary variable to hold the new state
|
||||
let newState = {
|
||||
...(currentModel || {}),
|
||||
completionTokenPrice: value,
|
||||
completionRatio: 0
|
||||
};
|
||||
|
||||
if (!isNaN(value) && value !== '' && currentModel?.tokenPrice) {
|
||||
const completionTokenPrice = parseFloat(value);
|
||||
const modelTokenPrice = parseFloat(currentModel.tokenPrice);
|
||||
|
||||
if (modelTokenPrice > 0) {
|
||||
const completionRatio = calculateCompletionRatioFromPrices(modelTokenPrice, completionTokenPrice);
|
||||
newState.completionRatio = completionRatio;
|
||||
}
|
||||
}
|
||||
|
||||
// Set the state with the complete updated object
|
||||
setCurrentModel(newState);
|
||||
};
|
||||
|
||||
const addOrUpdateModel = (values) => {
|
||||
// Check if we're editing an existing model or adding a new one
|
||||
const existingModelIndex = models.findIndex(model => model.name === values.name);
|
||||
|
||||
if (existingModelIndex >= 0) {
|
||||
// Update existing model
|
||||
setModels(prev => prev.map((model, index) =>
|
||||
index === existingModelIndex ? {
|
||||
name: values.name,
|
||||
price: values.price || '',
|
||||
ratio: values.ratio || '',
|
||||
completionRatio: values.completionRatio || ''
|
||||
} : model
|
||||
));
|
||||
setVisible(false);
|
||||
showSuccess(t('更新成功'));
|
||||
} else {
|
||||
// Add new model
|
||||
// Check if model name already exists
|
||||
if (models.some(model => model.name === values.name)) {
|
||||
showError(t('模型名称已存在'));
|
||||
return;
|
||||
}
|
||||
|
||||
setModels(prev => [{
|
||||
name: values.name,
|
||||
price: values.price || '',
|
||||
ratio: values.ratio || '',
|
||||
completionRatio: values.completionRatio || ''
|
||||
}, ...prev]);
|
||||
setVisible(false);
|
||||
showSuccess(t('添加成功'));
|
||||
}
|
||||
};
|
||||
|
||||
const calculateTokenPriceFromRatio = (ratio) => {
|
||||
return ratio * 2;
|
||||
};
|
||||
|
||||
const resetModalState = () => {
|
||||
setCurrentModel(null);
|
||||
setPricingMode('per-token');
|
||||
setPricingSubMode('ratio');
|
||||
};
|
||||
|
||||
const editModel = (record) => {
|
||||
|
||||
// Determine which pricing mode to use based on the model's current configuration
|
||||
let initialPricingMode = 'per-token';
|
||||
let initialPricingSubMode = 'ratio';
|
||||
|
||||
if (record.price !== '') {
|
||||
initialPricingMode = 'per-request';
|
||||
} else {
|
||||
initialPricingMode = 'per-token';
|
||||
// We default to ratio mode, but could set to token-price if needed
|
||||
}
|
||||
|
||||
// Set the pricing modes for the form
|
||||
setPricingMode(initialPricingMode);
|
||||
setPricingSubMode(initialPricingSubMode);
|
||||
|
||||
// Create a copy of the model data to avoid modifying the original
|
||||
const modelCopy = { ...record };
|
||||
|
||||
// If the model has ratio data and we want to populate token price fields
|
||||
if (record.ratio) {
|
||||
modelCopy.tokenPrice = calculateTokenPriceFromRatio(parseFloat(record.ratio)).toString();
|
||||
|
||||
if (record.completionRatio) {
|
||||
modelCopy.completionTokenPrice = (parseFloat(modelCopy.tokenPrice) * parseFloat(record.completionRatio)).toString();
|
||||
}
|
||||
}
|
||||
|
||||
// Set the current model
|
||||
setCurrentModel(modelCopy);
|
||||
|
||||
// Open the modal
|
||||
setVisible(true);
|
||||
|
||||
// Use setTimeout to ensure the form is rendered before setting values
|
||||
setTimeout(() => {
|
||||
if (formRef.current) {
|
||||
// Update the form fields based on pricing mode
|
||||
const formValues = {
|
||||
name: modelCopy.name,
|
||||
};
|
||||
|
||||
if (initialPricingMode === 'per-request') {
|
||||
formValues.priceInput = modelCopy.price;
|
||||
} else if (initialPricingMode === 'per-token') {
|
||||
formValues.ratioInput = modelCopy.ratio;
|
||||
formValues.completionRatioInput = modelCopy.completionRatio;
|
||||
formValues.modelTokenPrice = modelCopy.tokenPrice;
|
||||
formValues.completionTokenPrice = modelCopy.completionTokenPrice;
|
||||
}
|
||||
|
||||
formRef.current.setValues(formValues);
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Space vertical align="start" style={{ width: '100%' }}>
|
||||
<Space>
|
||||
<Button icon={<IconPlus />} onClick={() => setVisible(true)}>
|
||||
<Button icon={<IconPlus />} onClick={() => {
|
||||
resetModalState();
|
||||
setVisible(true);
|
||||
}}>
|
||||
{t('添加模型')}
|
||||
</Button>
|
||||
<Button type="primary" icon={<IconSave />} onClick={SubmitData}>
|
||||
@@ -256,56 +413,205 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
</Space>
|
||||
|
||||
<Modal
|
||||
title={t('添加模型')}
|
||||
title={currentModel && currentModel.name && models.some(model => model.name === currentModel.name) ? t('编辑模型') : t('添加模型')}
|
||||
visible={visible}
|
||||
onCancel={() => setVisible(false)}
|
||||
onCancel={() => {
|
||||
resetModalState();
|
||||
setVisible(false);
|
||||
}}
|
||||
onOk={() => {
|
||||
currentModel && addModel(currentModel);
|
||||
if (currentModel) {
|
||||
// If we're in token price mode, make sure ratio values are properly set
|
||||
const valuesToSave = { ...currentModel };
|
||||
|
||||
if (pricingMode === 'per-token' && pricingSubMode === 'token-price' && currentModel.tokenPrice) {
|
||||
// Calculate and set ratio from token price
|
||||
const tokenPrice = parseFloat(currentModel.tokenPrice);
|
||||
valuesToSave.ratio = (tokenPrice / 2).toString();
|
||||
|
||||
// Calculate and set completion ratio if both token prices are available
|
||||
if (currentModel.completionTokenPrice && currentModel.tokenPrice) {
|
||||
const completionPrice = parseFloat(currentModel.completionTokenPrice);
|
||||
const modelPrice = parseFloat(currentModel.tokenPrice);
|
||||
if (modelPrice > 0) {
|
||||
valuesToSave.completionRatio = (completionPrice / modelPrice).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear price if we're in per-token mode
|
||||
if (pricingMode === 'per-token') {
|
||||
valuesToSave.price = '';
|
||||
} else {
|
||||
// Clear ratios if we're in per-request mode
|
||||
valuesToSave.ratio = '';
|
||||
valuesToSave.completionRatio = '';
|
||||
}
|
||||
|
||||
addOrUpdateModel(valuesToSave);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Form>
|
||||
<Form getFormApi={api => formRef.current = api}>
|
||||
<Form.Input
|
||||
field="name"
|
||||
label={t('模型名称')}
|
||||
placeholder="strawberry"
|
||||
required
|
||||
disabled={currentModel && currentModel.name && models.some(model => model.name === currentModel.name)}
|
||||
onChange={value => setCurrentModel(prev => ({ ...prev, name: value }))}
|
||||
/>
|
||||
<Form.Switch
|
||||
field="priceMode"
|
||||
label={<>{t('定价模式')}:{currentModel?.priceMode ? t("固定价格") : t("倍率模式")}</>}
|
||||
onChange={checked => {
|
||||
setCurrentModel(prev => ({
|
||||
...prev,
|
||||
price: '',
|
||||
ratio: '',
|
||||
completionRatio: '',
|
||||
priceMode: checked
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
{currentModel?.priceMode ? (
|
||||
|
||||
<Form.Section text={t('定价模式')}>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<RadioGroup type="button" value={pricingMode} onChange={(e) => {
|
||||
const newMode = e.target.value;
|
||||
const oldMode = pricingMode;
|
||||
setPricingMode(newMode);
|
||||
|
||||
// Instead of resetting all values, convert between modes
|
||||
if (currentModel) {
|
||||
const updatedModel = { ...currentModel };
|
||||
|
||||
// Update formRef with converted values
|
||||
if (formRef.current) {
|
||||
const formValues = {
|
||||
name: updatedModel.name
|
||||
};
|
||||
|
||||
if (newMode === 'per-request') {
|
||||
formValues.priceInput = updatedModel.price || '';
|
||||
} else if (newMode === 'per-token') {
|
||||
formValues.ratioInput = updatedModel.ratio || '';
|
||||
formValues.completionRatioInput = updatedModel.completionRatio || '';
|
||||
formValues.modelTokenPrice = updatedModel.tokenPrice || '';
|
||||
formValues.completionTokenPrice = updatedModel.completionTokenPrice || '';
|
||||
}
|
||||
|
||||
formRef.current.setValues(formValues);
|
||||
}
|
||||
|
||||
// Update the model state
|
||||
setCurrentModel(updatedModel);
|
||||
}
|
||||
}}>
|
||||
<Radio value="per-token">{t('按量计费')}</Radio>
|
||||
<Radio value="per-request">{t('按次计费')}</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</Form.Section>
|
||||
|
||||
{pricingMode === 'per-token' && (
|
||||
<>
|
||||
<Form.Section text={t('价格设置方式')}>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<RadioGroup type="button" value={pricingSubMode} onChange={(e) => {
|
||||
const newSubMode = e.target.value;
|
||||
const oldSubMode = pricingSubMode;
|
||||
setPricingSubMode(newSubMode);
|
||||
|
||||
// Handle conversion between submodes
|
||||
if (currentModel) {
|
||||
const updatedModel = { ...currentModel };
|
||||
|
||||
// Convert between ratio and token price
|
||||
if (oldSubMode === 'ratio' && newSubMode === 'token-price') {
|
||||
if (updatedModel.ratio) {
|
||||
updatedModel.tokenPrice = calculateTokenPriceFromRatio(parseFloat(updatedModel.ratio)).toString();
|
||||
|
||||
if (updatedModel.completionRatio) {
|
||||
updatedModel.completionTokenPrice = (parseFloat(updatedModel.tokenPrice) * parseFloat(updatedModel.completionRatio)).toString();
|
||||
}
|
||||
}
|
||||
} else if (oldSubMode === 'token-price' && newSubMode === 'ratio') {
|
||||
// Ratio values should already be calculated by the handlers
|
||||
}
|
||||
|
||||
// Update the form values
|
||||
if (formRef.current) {
|
||||
const formValues = {};
|
||||
|
||||
if (newSubMode === 'ratio') {
|
||||
formValues.ratioInput = updatedModel.ratio || '';
|
||||
formValues.completionRatioInput = updatedModel.completionRatio || '';
|
||||
} else if (newSubMode === 'token-price') {
|
||||
formValues.modelTokenPrice = updatedModel.tokenPrice || '';
|
||||
formValues.completionTokenPrice = updatedModel.completionTokenPrice || '';
|
||||
}
|
||||
|
||||
formRef.current.setValues(formValues);
|
||||
}
|
||||
|
||||
setCurrentModel(updatedModel);
|
||||
}
|
||||
}}>
|
||||
<Radio value="ratio">{t('按倍率设置')}</Radio>
|
||||
<Radio value="token-price">{t('按价格设置')}</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</Form.Section>
|
||||
|
||||
{pricingSubMode === 'ratio' && (
|
||||
<>
|
||||
<Form.Input
|
||||
field="ratioInput"
|
||||
label={t('模型倍率')}
|
||||
placeholder={t('输入模型倍率')}
|
||||
onChange={value => setCurrentModel(prev => ({
|
||||
...prev || {},
|
||||
ratio: value
|
||||
}))}
|
||||
initValue={currentModel?.ratio || ''}
|
||||
/>
|
||||
<Form.Input
|
||||
field="completionRatioInput"
|
||||
label={t('补全倍率')}
|
||||
placeholder={t('输入补全倍率')}
|
||||
onChange={value => setCurrentModel(prev => ({
|
||||
...prev || {},
|
||||
completionRatio: value
|
||||
}))}
|
||||
initValue={currentModel?.completionRatio || ''}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{pricingSubMode === 'token-price' && (
|
||||
<>
|
||||
<Form.Input
|
||||
field="modelTokenPrice"
|
||||
label={t('输入价格')}
|
||||
onChange={(value) => {
|
||||
handleTokenPriceChange(value);
|
||||
}}
|
||||
initValue={currentModel?.tokenPrice || ''}
|
||||
suffix={t('$/1M tokens')}
|
||||
/>
|
||||
<Form.Input
|
||||
field="completionTokenPrice"
|
||||
label={t('输出价格')}
|
||||
onChange={(value) => {
|
||||
handleCompletionTokenPriceChange(value);
|
||||
}}
|
||||
initValue={currentModel?.completionTokenPrice || ''}
|
||||
suffix={t('$/1M tokens')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{pricingMode === 'per-request' && (
|
||||
<Form.Input
|
||||
field="price"
|
||||
field="priceInput"
|
||||
label={t('固定价格(每次)')}
|
||||
placeholder={t('输入每次价格')}
|
||||
onChange={value => setCurrentModel(prev => ({ ...prev, price: value }))}
|
||||
onChange={value => setCurrentModel(prev => ({
|
||||
...prev || {},
|
||||
price: value
|
||||
}))}
|
||||
initValue={currentModel?.price || ''}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Form.Input
|
||||
field="ratio"
|
||||
label={t('模型倍率')}
|
||||
placeholder={t('输入模型倍率')}
|
||||
onChange={value => setCurrentModel(prev => ({ ...prev, ratio: value }))}
|
||||
/>
|
||||
<Form.Input
|
||||
field="completionRatio"
|
||||
label={t('补全倍率')}
|
||||
placeholder={t('输入补全价格')}
|
||||
onChange={value => setCurrentModel(prev => ({ ...prev, completionRatio: value }))}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
@@ -27,9 +27,10 @@ export default function GeneralSettings(props) {
|
||||
const refForm = useRef();
|
||||
const [inputsRow, setInputsRow] = useState(inputs);
|
||||
|
||||
function onChange(value, e) {
|
||||
const name = e.target.id;
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
function handleFieldChange(fieldName) {
|
||||
return (value) => {
|
||||
setInputs((inputs) => ({ ...inputs, [fieldName]: value }));
|
||||
};
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
@@ -98,7 +99,7 @@ export default function GeneralSettings(props) {
|
||||
label={t('充值链接')}
|
||||
initValue={''}
|
||||
placeholder={t('例如发卡网站的购买链接')}
|
||||
onChange={onChange}
|
||||
onChange={handleFieldChange('TopUpLink')}
|
||||
showClear
|
||||
/>
|
||||
</Col>
|
||||
@@ -108,7 +109,7 @@ export default function GeneralSettings(props) {
|
||||
label={t('文档地址')}
|
||||
initValue={''}
|
||||
placeholder={t('例如 https://docs.newapi.pro')}
|
||||
onChange={onChange}
|
||||
onChange={handleFieldChange('general_setting.docs_link')}
|
||||
showClear
|
||||
/>
|
||||
</Col>
|
||||
@@ -118,7 +119,7 @@ export default function GeneralSettings(props) {
|
||||
label={t('单位美元额度')}
|
||||
initValue={''}
|
||||
placeholder={t('一单位货币能兑换的额度')}
|
||||
onChange={onChange}
|
||||
onChange={handleFieldChange('QuotaPerUnit')}
|
||||
showClear
|
||||
onClick={() => setShowQuotaWarning(true)}
|
||||
/>
|
||||
@@ -129,7 +130,7 @@ export default function GeneralSettings(props) {
|
||||
label={t('失败重试次数')}
|
||||
initValue={''}
|
||||
placeholder={t('失败重试次数')}
|
||||
onChange={onChange}
|
||||
onChange={handleFieldChange('RetryTimes')}
|
||||
showClear
|
||||
/>
|
||||
</Col>
|
||||
@@ -142,12 +143,7 @@ export default function GeneralSettings(props) {
|
||||
size='default'
|
||||
checkedText='|'
|
||||
uncheckedText='〇'
|
||||
onChange={(value) => {
|
||||
setInputs({
|
||||
...inputs,
|
||||
DisplayInCurrencyEnabled: value,
|
||||
});
|
||||
}}
|
||||
onChange={handleFieldChange('DisplayInCurrencyEnabled')}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
||||
@@ -157,12 +153,7 @@ export default function GeneralSettings(props) {
|
||||
size='default'
|
||||
checkedText='|'
|
||||
uncheckedText='〇'
|
||||
onChange={(value) =>
|
||||
setInputs({
|
||||
...inputs,
|
||||
DisplayTokenStatEnabled: value,
|
||||
})
|
||||
}
|
||||
onChange={handleFieldChange('DisplayTokenStatEnabled')}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
||||
@@ -172,12 +163,7 @@ export default function GeneralSettings(props) {
|
||||
size='default'
|
||||
checkedText='|'
|
||||
uncheckedText='〇'
|
||||
onChange={(value) =>
|
||||
setInputs({
|
||||
...inputs,
|
||||
DefaultCollapseSidebar: value,
|
||||
})
|
||||
}
|
||||
onChange={handleFieldChange('DefaultCollapseSidebar')}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -189,12 +175,7 @@ export default function GeneralSettings(props) {
|
||||
size='default'
|
||||
checkedText='|'
|
||||
uncheckedText='〇'
|
||||
onChange={(value) =>
|
||||
setInputs({
|
||||
...inputs,
|
||||
DemoSiteEnabled: value
|
||||
})
|
||||
}
|
||||
onChange={handleFieldChange('DemoSiteEnabled')}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
||||
@@ -205,12 +186,7 @@ export default function GeneralSettings(props) {
|
||||
size='default'
|
||||
checkedText='|'
|
||||
uncheckedText='〇'
|
||||
onChange={(value) =>
|
||||
setInputs({
|
||||
...inputs,
|
||||
SelfUseModeEnabled: value
|
||||
})
|
||||
}
|
||||
onChange={handleFieldChange('SelfUseModeEnabled')}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
252
web/src/pages/Setup/index.js
Normal file
252
web/src/pages/Setup/index.js
Normal file
@@ -0,0 +1,252 @@
|
||||
import React, { useContext, useEffect, useState, useRef } from 'react';
|
||||
import { Card, Col, Row, Form, Button, Typography, Space, RadioGroup, Radio, Modal, Banner } from '@douyinfe/semi-ui';
|
||||
import { API, showError, showNotice, timestamp2string } from '../../helpers';
|
||||
import { StatusContext } from '../../context/Status';
|
||||
import { marked } from 'marked';
|
||||
import { StyleContext } from '../../context/Style/index.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IconHelpCircle, IconInfoCircle, IconAlertTriangle } from '@douyinfe/semi-icons';
|
||||
|
||||
const Setup = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [statusState] = useContext(StatusContext);
|
||||
const [styleState, styleDispatch] = useContext(StyleContext);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selfUseModeInfoVisible, setUsageModeInfoVisible] = useState(false);
|
||||
const [setupStatus, setSetupStatus] = useState({
|
||||
status: false,
|
||||
root_init: false,
|
||||
database_type: ''
|
||||
});
|
||||
const { Text, Title } = Typography;
|
||||
const formRef = useRef(null);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
usageMode: 'external'
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchSetupStatus();
|
||||
}, []);
|
||||
|
||||
const fetchSetupStatus = async () => {
|
||||
try {
|
||||
const res = await API.get('/api/setup');
|
||||
const { success, data } = res.data;
|
||||
if (success) {
|
||||
setSetupStatus(data);
|
||||
|
||||
// If setup is already completed, redirect to home
|
||||
if (data.status) {
|
||||
window.location.href = '/';
|
||||
}
|
||||
} else {
|
||||
showError(t('获取初始化状态失败'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch setup status:', error);
|
||||
showError(t('获取初始化状态失败'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleUsageModeChange = (val) => {
|
||||
setFormData({...formData, usageMode: val});
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
if (!formRef.current) {
|
||||
console.error("Form reference is null");
|
||||
showError(t('表单引用错误,请刷新页面重试'));
|
||||
return;
|
||||
}
|
||||
|
||||
const values = formRef.current.getValues();
|
||||
console.log("Form values:", values);
|
||||
|
||||
// For root_init=false, validate admin username and password
|
||||
if (!setupStatus.root_init) {
|
||||
if (!values.username || !values.username.trim()) {
|
||||
showError(t('请输入管理员用户名'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!values.password || values.password.length < 8) {
|
||||
showError(t('密码长度至少为8个字符'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (values.password !== values.confirmPassword) {
|
||||
showError(t('两次输入的密码不一致'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare submission data
|
||||
const formValues = {...values};
|
||||
formValues.SelfUseModeEnabled = values.usageMode === 'self';
|
||||
formValues.DemoSiteEnabled = values.usageMode === 'demo';
|
||||
|
||||
// Remove usageMode as it's not needed by the backend
|
||||
delete formValues.usageMode;
|
||||
|
||||
console.log("Submitting data to backend:", formValues);
|
||||
setLoading(true);
|
||||
|
||||
// Submit to backend
|
||||
API.post('/api/setup', formValues)
|
||||
.then(res => {
|
||||
const { success, message } = res.data;
|
||||
console.log("API response:", res.data);
|
||||
|
||||
if (success) {
|
||||
showNotice(t('系统初始化成功,正在跳转...'));
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
showError(message || t('初始化失败,请重试'));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('API error:', error);
|
||||
showError(t('系统初始化失败,请重试'));
|
||||
setLoading(false);
|
||||
})
|
||||
.finally(() => {
|
||||
// setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
|
||||
<Card>
|
||||
<Title heading={2} style={{ marginBottom: '24px' }}>{t('系统初始化')}</Title>
|
||||
|
||||
{setupStatus.database_type === 'sqlite' && (
|
||||
<Banner
|
||||
type="warning"
|
||||
icon={<IconAlertTriangle size="large" />}
|
||||
closeIcon={null}
|
||||
title={t('数据库警告')}
|
||||
description={
|
||||
<div>
|
||||
<p>{t('您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!')}</p>
|
||||
<p>{t('建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。')}</p>
|
||||
</div>
|
||||
}
|
||||
style={{ marginBottom: '24px' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Form
|
||||
getFormApi={(formApi) => { formRef.current = formApi; console.log("Form API set:", formApi); }}
|
||||
initValues={formData}
|
||||
>
|
||||
{setupStatus.root_init ? (
|
||||
<Banner
|
||||
type="info"
|
||||
icon={<IconInfoCircle />}
|
||||
closeIcon={null}
|
||||
description={t('管理员账号已经初始化过,请继续设置系统参数')}
|
||||
style={{ marginBottom: '24px' }}
|
||||
/>
|
||||
) : (
|
||||
<Form.Section text={t('管理员账号')}>
|
||||
<Form.Input
|
||||
field="username"
|
||||
label={t('用户名')}
|
||||
placeholder={t('请输入管理员用户名')}
|
||||
showClear
|
||||
onChange={(value) => setFormData({...formData, username: value})}
|
||||
/>
|
||||
<Form.Input
|
||||
field="password"
|
||||
label={t('密码')}
|
||||
placeholder={t('请输入管理员密码')}
|
||||
type="password"
|
||||
showClear
|
||||
onChange={(value) => setFormData({...formData, password: value})}
|
||||
/>
|
||||
<Form.Input
|
||||
field="confirmPassword"
|
||||
label={t('确认密码')}
|
||||
placeholder={t('请确认管理员密码')}
|
||||
type="password"
|
||||
showClear
|
||||
onChange={(value) => setFormData({...formData, confirmPassword: value})}
|
||||
/>
|
||||
</Form.Section>
|
||||
)}
|
||||
|
||||
<Form.Section text={
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
{t('系统设置')}
|
||||
</div>
|
||||
}>
|
||||
<Form.RadioGroup
|
||||
field="usageMode"
|
||||
label={
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
{t('使用模式')}
|
||||
<IconHelpCircle
|
||||
style={{ marginLeft: '4px', color: 'var(--semi-color-primary)', verticalAlign: 'middle', cursor: 'pointer' }}
|
||||
onClick={(e) => {
|
||||
// e.preventDefault();
|
||||
// e.stopPropagation();
|
||||
setUsageModeInfoVisible(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
extraText={t('可在初始化后修改')}
|
||||
initValue="external"
|
||||
onChange={handleUsageModeChange}
|
||||
>
|
||||
<Form.Radio value="external">{t('对外运营模式')}</Form.Radio>
|
||||
<Form.Radio value="self">{t('自用模式')}</Form.Radio>
|
||||
<Form.Radio value="demo">{t('演示站点模式')}</Form.Radio>
|
||||
</Form.RadioGroup>
|
||||
</Form.Section>
|
||||
</Form>
|
||||
|
||||
<div style={{ marginTop: '24px', textAlign: 'right' }}>
|
||||
<Button type="primary" onClick={onSubmit} loading={loading}>
|
||||
{t('初始化系统')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
title={t('使用模式说明')}
|
||||
visible={selfUseModeInfoVisible}
|
||||
onOk={() => setUsageModeInfoVisible(false)}
|
||||
onCancel={() => setUsageModeInfoVisible(false)}
|
||||
closeOnEsc={true}
|
||||
okText={t('确定')}
|
||||
cancelText={null}
|
||||
>
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<Title heading={6}>{t('对外运营模式')}</Title>
|
||||
<p>{t('默认模式,适用于为多个用户提供服务的场景。')}</p>
|
||||
<p>{t('此模式下,系统将计算每次调用的用量,您需要对每个模型都设置价格,如果没有设置价格,用户将无法使用该模型。')}</p>
|
||||
</div>
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<Title heading={6}>{t('自用模式')}</Title>
|
||||
<p>{t('适用于个人使用的场景。')}</p>
|
||||
<p>{t('不需要设置模型价格,系统将弱化用量计算,您可专注于使用模型。')}</p>
|
||||
</div>
|
||||
<div style={{ padding: '8px 0' }}>
|
||||
<Title heading={6}>{t('演示站点模式')}</Title>
|
||||
<p>{t('适用于展示系统功能的场景。')}</p>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Setup;
|
||||
Reference in New Issue
Block a user