mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-11-05 08:13:43 +08:00
Compare commits
12 Commits
v0.2.1-alp
...
v0.2.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6af636fa0 | ||
|
|
6e1ef75009 | ||
|
|
d9db16e999 | ||
|
|
241ade2fae | ||
|
|
80065de8a3 | ||
|
|
16f53b5afb | ||
|
|
3071300c0c | ||
|
|
8b056bf408 | ||
|
|
e5640857b1 | ||
|
|
331177d97e | ||
|
|
4fed003f1a | ||
|
|
a1ea1bf696 |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
custom: ['https://iamazing.cn/page/reward']
|
||||||
@@ -49,7 +49,7 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
|
|||||||
+ [x] [OpenAI-SB](https://openai-sb.com)
|
+ [x] [OpenAI-SB](https://openai-sb.com)
|
||||||
+ [x] [OpenAI Max](https://openaimax.com)
|
+ [x] [OpenAI Max](https://openaimax.com)
|
||||||
+ [x] [OhMyGPT](https://www.ohmygpt.com)
|
+ [x] [OhMyGPT](https://www.ohmygpt.com)
|
||||||
+ [x] 自定义渠道
|
+ [x] 自定义渠道:例如使用自行搭建的 OpenAI 代理
|
||||||
2. 支持通过负载均衡的方式访问多个渠道。
|
2. 支持通过负载均衡的方式访问多个渠道。
|
||||||
3. 支持单个访问渠道设置多个 API Key,利用起来你的多个 API Key。
|
3. 支持单个访问渠道设置多个 API Key,利用起来你的多个 API Key。
|
||||||
4. 支持 HTTP SSE,可以通过流式传输实现打字机效果。
|
4. 支持 HTTP SSE,可以通过流式传输实现打字机效果。
|
||||||
|
|||||||
@@ -49,11 +49,6 @@ var TurnstileSecretKey = ""
|
|||||||
|
|
||||||
var QuotaForNewUser = 100
|
var QuotaForNewUser = 100
|
||||||
|
|
||||||
// https://platform.openai.com/docs/models/model-endpoint-compatibility
|
|
||||||
var RatioGPT3dot5 float64 = 2
|
|
||||||
var RatioGPT4 float64 = 30
|
|
||||||
var RatioGPT4_32k float64 = 60
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
RoleGuestUser = 0
|
RoleGuestUser = 0
|
||||||
RoleCommonUser = 1
|
RoleCommonUser = 1
|
||||||
|
|||||||
52
common/model-ratio.go
Normal file
52
common/model-ratio.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
// https://platform.openai.com/docs/models/model-endpoint-compatibility
|
||||||
|
// https://openai.com/pricing
|
||||||
|
// TODO: when a new api is enabled, check the pricing here
|
||||||
|
var ModelRatio = map[string]float64{
|
||||||
|
"gpt-4": 15,
|
||||||
|
"gpt-4-0314": 15,
|
||||||
|
"gpt-4-32k": 30,
|
||||||
|
"gpt-4-32k-0314": 30,
|
||||||
|
"gpt-3.5-turbo": 1,
|
||||||
|
"gpt-3.5-turbo-0301": 1,
|
||||||
|
"text-ada-001": 0.2,
|
||||||
|
"text-babbage-001": 0.25,
|
||||||
|
"text-curie-001": 1,
|
||||||
|
"text-davinci-002": 10,
|
||||||
|
"text-davinci-003": 10,
|
||||||
|
"text-davinci-edit-001": 10,
|
||||||
|
"code-davinci-edit-001": 10,
|
||||||
|
"whisper-1": 10,
|
||||||
|
"davinci": 10,
|
||||||
|
"curie": 10,
|
||||||
|
"babbage": 10,
|
||||||
|
"ada": 10,
|
||||||
|
"text-embedding-ada-002": 0.25,
|
||||||
|
"text-search-ada-doc-001": 10,
|
||||||
|
"text-moderation-stable": 10,
|
||||||
|
"text-moderation-latest": 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
func ModelRatio2JSONString() string {
|
||||||
|
jsonBytes, err := json.Marshal(ModelRatio)
|
||||||
|
if err != nil {
|
||||||
|
SysError("Error marshalling model ratio: " + err.Error())
|
||||||
|
}
|
||||||
|
return string(jsonBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateModelRatioByJSONString(jsonStr string) error {
|
||||||
|
return json.Unmarshal([]byte(jsonStr), &ModelRatio)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetModelRatio(name string) float64 {
|
||||||
|
ratio, ok := ModelRatio[name]
|
||||||
|
if !ok {
|
||||||
|
SysError("Model ratio not found: " + name)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return ratio
|
||||||
|
}
|
||||||
@@ -118,26 +118,24 @@ func relayHelper(c *gin.Context) error {
|
|||||||
defer func() {
|
defer func() {
|
||||||
if consumeQuota {
|
if consumeQuota {
|
||||||
quota := 0
|
quota := 0
|
||||||
|
usingGPT4 := strings.HasPrefix(textRequest.Model, "gpt-4")
|
||||||
|
completionRatio := 1
|
||||||
|
if usingGPT4 {
|
||||||
|
completionRatio = 2
|
||||||
|
}
|
||||||
if isStream {
|
if isStream {
|
||||||
var text string
|
var promptText string
|
||||||
for _, message := range textRequest.Messages {
|
for _, message := range textRequest.Messages {
|
||||||
text += fmt.Sprintf("%s: %s\n", message.Role, message.Content)
|
promptText += fmt.Sprintf("%s: %s\n", message.Role, message.Content)
|
||||||
}
|
}
|
||||||
text += fmt.Sprintf("%s: %s\n", "assistant", streamResponseText)
|
completionText := fmt.Sprintf("%s: %s\n", "assistant", streamResponseText)
|
||||||
quota = countToken(text) + 3
|
quota = countToken(promptText) + countToken(completionText)*completionRatio + 3
|
||||||
} else {
|
} else {
|
||||||
quota = textResponse.Usage.TotalTokens
|
quota = textResponse.Usage.PromptTokens + textResponse.Usage.CompletionTokens*completionRatio
|
||||||
}
|
|
||||||
ratio := common.RatioGPT3dot5
|
|
||||||
if strings.HasPrefix(textRequest.Model, "gpt-4-32k") {
|
|
||||||
ratio = common.RatioGPT4_32k
|
|
||||||
} else if strings.HasPrefix(textRequest.Model, "gpt-4") {
|
|
||||||
ratio = common.RatioGPT4
|
|
||||||
} else {
|
|
||||||
ratio = common.RatioGPT3dot5
|
|
||||||
}
|
}
|
||||||
|
ratio := common.GetModelRatio(textRequest.Model)
|
||||||
quota = int(float64(quota) * ratio)
|
quota = int(float64(quota) * ratio)
|
||||||
err := model.ConsumeTokenQuota(tokenId, quota)
|
err := model.DecreaseTokenQuota(tokenId, quota)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.SysError("Error consuming token remain quota: " + err.Error())
|
common.SysError("Error consuming token remain quota: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,30 @@ func GetToken(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetTokenStatus(c *gin.Context) {
|
||||||
|
tokenId := c.GetInt("token_id")
|
||||||
|
userId := c.GetInt("id")
|
||||||
|
token, err := model.GetTokenByIds(tokenId, userId)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expiredAt := token.ExpiredTime
|
||||||
|
if expiredAt == -1 {
|
||||||
|
expiredAt = 0
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"object": "credit_summary",
|
||||||
|
"total_granted": token.RemainQuota,
|
||||||
|
"total_used": 0, // not supported currently
|
||||||
|
"total_available": token.RemainQuota,
|
||||||
|
"expires_at": expiredAt * 1000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func AddToken(c *gin.Context) {
|
func AddToken(c *gin.Context) {
|
||||||
isAdmin := c.GetInt("role") >= common.RoleAdminUser
|
isAdmin := c.GetInt("role") >= common.RoleAdminUser
|
||||||
token := model.Token{}
|
token := model.Token{}
|
||||||
@@ -114,9 +138,7 @@ func AddToken(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if quota > 0 {
|
cleanToken.RemainQuota = quota
|
||||||
cleanToken.RemainQuota = quota
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
err = cleanToken.Insert()
|
err = cleanToken.Insert()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
22
docker-compose.yml
Normal file
22
docker-compose.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
version: '3.4'
|
||||||
|
|
||||||
|
services:
|
||||||
|
one-api:
|
||||||
|
image: ghcr.io/songquanpeng/one-api:latest
|
||||||
|
container_name: one-api
|
||||||
|
restart: always
|
||||||
|
command: --log-dir /app/logs
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
volumes:
|
||||||
|
- /home/ubuntu/data/one-api:/data
|
||||||
|
- /home/ubuntu/data/one-api/logs:/app/logs
|
||||||
|
# environment:
|
||||||
|
# REDIS_CONN_STRING: redis://default:redispw@localhost:49153
|
||||||
|
# SESSION_SECRET: random_string
|
||||||
|
# SQL_DSN: root:123456@tcp(localhost:3306)/one-api
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -s http://localhost:3000/api/status | grep -o '\"success\":\\s*true' | awk '{print $2}' | grep 'true'"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
@@ -47,9 +47,7 @@ func InitOptionMap() {
|
|||||||
common.OptionMap["TurnstileSiteKey"] = ""
|
common.OptionMap["TurnstileSiteKey"] = ""
|
||||||
common.OptionMap["TurnstileSecretKey"] = ""
|
common.OptionMap["TurnstileSecretKey"] = ""
|
||||||
common.OptionMap["QuotaForNewUser"] = strconv.Itoa(common.QuotaForNewUser)
|
common.OptionMap["QuotaForNewUser"] = strconv.Itoa(common.QuotaForNewUser)
|
||||||
common.OptionMap["RatioGPT3dot5"] = strconv.FormatFloat(common.RatioGPT3dot5, 'f', -1, 64)
|
common.OptionMap["ModelRatio"] = common.ModelRatio2JSONString()
|
||||||
common.OptionMap["RatioGPT4"] = strconv.FormatFloat(common.RatioGPT4, 'f', -1, 64)
|
|
||||||
common.OptionMap["RatioGPT4_32k"] = strconv.FormatFloat(common.RatioGPT4_32k, 'f', -1, 64)
|
|
||||||
common.OptionMap["TopUpLink"] = common.TopUpLink
|
common.OptionMap["TopUpLink"] = common.TopUpLink
|
||||||
common.OptionMapRWMutex.Unlock()
|
common.OptionMapRWMutex.Unlock()
|
||||||
options, _ := AllOption()
|
options, _ := AllOption()
|
||||||
@@ -75,7 +73,7 @@ func UpdateOption(key string, value string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateOptionMap(key string, value string) {
|
func updateOptionMap(key string, value string) (err error) {
|
||||||
common.OptionMapRWMutex.Lock()
|
common.OptionMapRWMutex.Lock()
|
||||||
defer common.OptionMapRWMutex.Unlock()
|
defer common.OptionMapRWMutex.Unlock()
|
||||||
common.OptionMap[key] = value
|
common.OptionMap[key] = value
|
||||||
@@ -138,13 +136,10 @@ func updateOptionMap(key string, value string) {
|
|||||||
common.TurnstileSecretKey = value
|
common.TurnstileSecretKey = value
|
||||||
case "QuotaForNewUser":
|
case "QuotaForNewUser":
|
||||||
common.QuotaForNewUser, _ = strconv.Atoi(value)
|
common.QuotaForNewUser, _ = strconv.Atoi(value)
|
||||||
case "RatioGPT3dot5":
|
case "ModelRatio":
|
||||||
common.RatioGPT3dot5, _ = strconv.ParseFloat(value, 64)
|
err = common.UpdateModelRatioByJSONString(value)
|
||||||
case "RatioGPT4":
|
|
||||||
common.RatioGPT4, _ = strconv.ParseFloat(value, 64)
|
|
||||||
case "RatioGPT4_32k":
|
|
||||||
common.RatioGPT4_32k, _ = strconv.ParseFloat(value, 64)
|
|
||||||
case "TopUpLink":
|
case "TopUpLink":
|
||||||
common.TopUpLink = value
|
common.TopUpLink = value
|
||||||
}
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ func Redeem(key string, tokenId int) (quota int, err error) {
|
|||||||
if redemption.Status != common.RedemptionCodeStatusEnabled {
|
if redemption.Status != common.RedemptionCodeStatusEnabled {
|
||||||
return 0, errors.New("该兑换码已被使用")
|
return 0, errors.New("该兑换码已被使用")
|
||||||
}
|
}
|
||||||
err = TopUpTokenQuota(tokenId, redemption.Quota)
|
err = IncreaseTokenQuota(tokenId, redemption.Quota)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,15 +116,26 @@ func DeleteTokenById(id int, userId int) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
quota := token.RemainQuota
|
||||||
|
if quota != 0 {
|
||||||
|
if quota > 0 {
|
||||||
|
err = IncreaseUserQuota(userId, quota)
|
||||||
|
} else {
|
||||||
|
err = DecreaseUserQuota(userId, -quota)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return token.Delete()
|
return token.Delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConsumeTokenQuota(id int, quota int) (err error) {
|
func IncreaseTokenQuota(id int, quota int) (err error) {
|
||||||
err = DB.Model(&Token{}).Where("id = ?", id).Update("remain_quota", gorm.Expr("remain_quota - ?", quota)).Error
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func TopUpTokenQuota(id int, quota int) (err error) {
|
|
||||||
err = DB.Model(&Token{}).Where("id = ?", id).Update("remain_quota", gorm.Expr("remain_quota + ?", quota)).Error
|
err = DB.Model(&Token{}).Where("id = ?", id).Update("remain_quota", gorm.Expr("remain_quota + ?", quota)).Error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DecreaseTokenQuota(id int, quota int) (err error) {
|
||||||
|
err = DB.Model(&Token{}).Where("id = ?", id).Update("remain_quota", gorm.Expr("remain_quota - ?", quota)).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
@@ -225,6 +225,11 @@ func GetUserQuota(id int) (quota int, err error) {
|
|||||||
return quota, err
|
return quota, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IncreaseUserQuota(id int, quota int) (err error) {
|
||||||
|
err = DB.Model(&User{}).Where("id = ?", id).Update("quota", gorm.Expr("quota + ?", quota)).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func DecreaseUserQuota(id int, quota int) (err error) {
|
func DecreaseUserQuota(id int, quota int) (err error) {
|
||||||
err = DB.Model(&User{}).Where("id = ?", id).Update("quota", gorm.Expr("quota - ?", quota)).Error
|
err = DB.Model(&User{}).Where("id = ?", id).Update("quota", gorm.Expr("quota - ?", quota)).Error
|
||||||
return err
|
return err
|
||||||
|
|||||||
13
one-api.service
Normal file
13
one-api.service
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=One API Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=yourusername # 守护进程用户名
|
||||||
|
WorkingDirectory=/path/to/One-API # One API运行路径
|
||||||
|
ExecStart=/path/to/One-API/one-api --port 3000 --log-dir /path/to/One-API/logs # 端口
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
18
router/dashboard.go
Normal file
18
router/dashboard.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-contrib/gzip"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"one-api/controller"
|
||||||
|
"one-api/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetDashboardRouter(router *gin.Engine) {
|
||||||
|
apiRouter := router.Group("/dashboard")
|
||||||
|
apiRouter.Use(gzip.Gzip(gzip.DefaultCompression))
|
||||||
|
apiRouter.Use(middleware.GlobalAPIRateLimit())
|
||||||
|
apiRouter.Use(middleware.TokenAuth())
|
||||||
|
{
|
||||||
|
apiRouter.GET("/billing/credit_grants", controller.GetTokenStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
func SetRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
|
func SetRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
|
||||||
SetApiRouter(router)
|
SetApiRouter(router)
|
||||||
|
SetDashboardRouter(router)
|
||||||
SetRelayRouter(router)
|
SetRelayRouter(router)
|
||||||
setWebRouter(router, buildFS, indexPage)
|
setWebRouter(router, buildFS, indexPage)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func SetRelayRouter(router *gin.Engine) {
|
|||||||
relayV1Router.POST("/images/generations", controller.RelayNotImplemented)
|
relayV1Router.POST("/images/generations", controller.RelayNotImplemented)
|
||||||
relayV1Router.POST("/images/edits", controller.RelayNotImplemented)
|
relayV1Router.POST("/images/edits", controller.RelayNotImplemented)
|
||||||
relayV1Router.POST("/images/variations", controller.RelayNotImplemented)
|
relayV1Router.POST("/images/variations", controller.RelayNotImplemented)
|
||||||
relayV1Router.POST("/embeddings", controller.RelayNotImplemented)
|
relayV1Router.POST("/embeddings", controller.Relay)
|
||||||
relayV1Router.POST("/audio/transcriptions", controller.RelayNotImplemented)
|
relayV1Router.POST("/audio/transcriptions", controller.RelayNotImplemented)
|
||||||
relayV1Router.POST("/audio/translations", controller.RelayNotImplemented)
|
relayV1Router.POST("/audio/translations", controller.RelayNotImplemented)
|
||||||
relayV1Router.GET("/files", controller.RelayNotImplemented)
|
relayV1Router.GET("/files", controller.RelayNotImplemented)
|
||||||
@@ -35,9 +35,4 @@ func SetRelayRouter(router *gin.Engine) {
|
|||||||
relayV1Router.DELETE("/models/:model", controller.RelayNotImplemented)
|
relayV1Router.DELETE("/models/:model", controller.RelayNotImplemented)
|
||||||
relayV1Router.POST("/moderations", controller.RelayNotImplemented)
|
relayV1Router.POST("/moderations", controller.RelayNotImplemented)
|
||||||
}
|
}
|
||||||
relayDashboardRouter := router.Group("/dashboard") // TODO: return system's own token info
|
|
||||||
relayDashboardRouter.Use(middleware.TokenAuth(), middleware.Distribute())
|
|
||||||
{
|
|
||||||
relayDashboardRouter.Any("/*path", controller.Relay)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Divider, Form, Grid, Header, Message } from 'semantic-ui-react';
|
import { Divider, Form, Grid, Header, Message } from 'semantic-ui-react';
|
||||||
import { API, removeTrailingSlash, showError } from '../helpers';
|
import { API, removeTrailingSlash, showError, verifyJSON } from '../helpers';
|
||||||
|
|
||||||
const SystemSetting = () => {
|
const SystemSetting = () => {
|
||||||
let [inputs, setInputs] = useState({
|
let [inputs, setInputs] = useState({
|
||||||
@@ -25,9 +25,7 @@ const SystemSetting = () => {
|
|||||||
TurnstileSecretKey: '',
|
TurnstileSecretKey: '',
|
||||||
RegisterEnabled: '',
|
RegisterEnabled: '',
|
||||||
QuotaForNewUser: 0,
|
QuotaForNewUser: 0,
|
||||||
RatioGPT3dot5: 2,
|
ModelRatio: '',
|
||||||
RatioGPT4: 30,
|
|
||||||
RatioGPT4_32k: 60,
|
|
||||||
TopUpLink: ''
|
TopUpLink: ''
|
||||||
});
|
});
|
||||||
let originInputs = {};
|
let originInputs = {};
|
||||||
@@ -93,7 +91,7 @@ const SystemSetting = () => {
|
|||||||
name === 'TurnstileSiteKey' ||
|
name === 'TurnstileSiteKey' ||
|
||||||
name === 'TurnstileSecretKey' ||
|
name === 'TurnstileSecretKey' ||
|
||||||
name === 'QuotaForNewUser' ||
|
name === 'QuotaForNewUser' ||
|
||||||
name.startsWith('Ratio') ||
|
name === 'ModelRatio' ||
|
||||||
name === 'TopUpLink'
|
name === 'TopUpLink'
|
||||||
) {
|
) {
|
||||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||||
@@ -111,19 +109,17 @@ const SystemSetting = () => {
|
|||||||
if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) {
|
if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) {
|
||||||
await updateOption('QuotaForNewUser', inputs.QuotaForNewUser);
|
await updateOption('QuotaForNewUser', inputs.QuotaForNewUser);
|
||||||
}
|
}
|
||||||
if (originInputs['RatioGPT3dot5'] !== inputs.RatioGPT3dot5) {
|
if (originInputs['ModelRatio'] !== inputs.ModelRatio) {
|
||||||
await updateOption('RatioGPT3dot5', inputs.RatioGPT3dot5);
|
if (!verifyJSON(inputs.ModelRatio)) {
|
||||||
}
|
showError('模型倍率不是合法的 JSON 字符串');
|
||||||
if (originInputs['RatioGPT4'] !== inputs.RatioGPT4) {
|
return;
|
||||||
await updateOption('RatioGPT4', inputs.RatioGPT4);
|
}
|
||||||
}
|
await updateOption('ModelRatio', inputs.ModelRatio);
|
||||||
if (originInputs['RatioGPT4_32k'] !== inputs.RatioGPT4_32k) {
|
|
||||||
await updateOption('RatioGPT4_32k', inputs.RatioGPT4_32k);
|
|
||||||
}
|
}
|
||||||
if (originInputs['TopUpLink'] !== inputs.TopUpLink) {
|
if (originInputs['TopUpLink'] !== inputs.TopUpLink) {
|
||||||
await updateOption('TopUpLink', inputs.TopUpLink);
|
await updateOption('TopUpLink', inputs.TopUpLink);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const submitSMTP = async () => {
|
const submitSMTP = async () => {
|
||||||
if (originInputs['SMTPServer'] !== inputs.SMTPServer) {
|
if (originInputs['SMTPServer'] !== inputs.SMTPServer) {
|
||||||
@@ -278,39 +274,15 @@ const SystemSetting = () => {
|
|||||||
placeholder='例如发卡网站的购买链接'
|
placeholder='例如发卡网站的购买链接'
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Group widths={3}>
|
<Form.Group widths='equal'>
|
||||||
<Form.Input
|
<Form.TextArea
|
||||||
label='GPT-3.5 系列模型倍率'
|
label='模型倍率'
|
||||||
name='RatioGPT3dot5'
|
name='ModelRatio'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
|
style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
value={inputs.RatioGPT3dot5}
|
value={inputs.ModelRatio}
|
||||||
type='number'
|
placeholder='为一个 JSON 文本,键为模型名称,值为倍率'
|
||||||
step='0.01'
|
|
||||||
min='0'
|
|
||||||
placeholder='例如:2'
|
|
||||||
/>
|
|
||||||
<Form.Input
|
|
||||||
label='GPT-4 系列模型倍率'
|
|
||||||
name='RatioGPT4'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
autoComplete='off'
|
|
||||||
value={inputs.RatioGPT4}
|
|
||||||
type='number'
|
|
||||||
step='0.01'
|
|
||||||
min='0'
|
|
||||||
placeholder='例如:30'
|
|
||||||
/>
|
|
||||||
<Form.Input
|
|
||||||
label='GPT-4 32k 系列模型倍率'
|
|
||||||
name='RatioGPT4_32k'
|
|
||||||
onChange={handleInputChange}
|
|
||||||
autoComplete='off'
|
|
||||||
value={inputs.RatioGPT4_32k}
|
|
||||||
type='number'
|
|
||||||
step='0.01'
|
|
||||||
min='0'
|
|
||||||
placeholder='例如:60'
|
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Button onClick={submitOperationConfig}>保存运营设置</Form.Button>
|
<Form.Button onClick={submitOperationConfig}>保存运营设置</Form.Button>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button, Form, Label, Pagination, Table } from 'semantic-ui-react';
|
import { Button, Form, Label, Pagination, Popup, Table } from 'semantic-ui-react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { API, showError, showSuccess } from '../helpers';
|
import { API, showError, showSuccess } from '../helpers';
|
||||||
|
|
||||||
@@ -237,15 +237,25 @@ const UsersTable = () => {
|
|||||||
>
|
>
|
||||||
降级
|
降级
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Popup
|
||||||
size={'small'}
|
trigger={
|
||||||
negative
|
<Button size='small' negative>
|
||||||
onClick={() => {
|
删除
|
||||||
manageUser(user.username, 'delete', idx);
|
</Button>
|
||||||
}}
|
}
|
||||||
|
on='click'
|
||||||
|
flowing
|
||||||
|
hoverable
|
||||||
>
|
>
|
||||||
删除
|
<Button
|
||||||
</Button>
|
negative
|
||||||
|
onClick={() => {
|
||||||
|
manageUser(user.username, 'delete', idx);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
删除用户 {user.username}
|
||||||
|
</Button>
|
||||||
|
</Popup>
|
||||||
<Button
|
<Button
|
||||||
size={'small'}
|
size={'small'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -152,4 +152,13 @@ export function downloadTextAsFile(text, filename) {
|
|||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = filename;
|
a.download = filename;
|
||||||
a.click();
|
a.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const verifyJSON = (str) => {
|
||||||
|
try {
|
||||||
|
JSON.parse(str);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
@@ -52,7 +52,7 @@ const AddChannel = () => {
|
|||||||
<Form.Input
|
<Form.Input
|
||||||
label='Base URL'
|
label='Base URL'
|
||||||
name='base_url'
|
name='base_url'
|
||||||
placeholder={'请输入自定义渠道的 Base URL'}
|
placeholder={'请输入自定义渠道的 Base URL,例如:https://openai.justsong.cn'}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={inputs.base_url}
|
value={inputs.base_url}
|
||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ const EditChannel = () => {
|
|||||||
<Form.Input
|
<Form.Input
|
||||||
label='Base URL'
|
label='Base URL'
|
||||||
name='base_url'
|
name='base_url'
|
||||||
placeholder={'请输入新的自定义渠道的 Base URL'}
|
placeholder={'请输入新的自定义渠道的 Base URL,例如:https://openai.justsong.cn'}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
value={inputs.base_url}
|
value={inputs.base_url}
|
||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
|
|||||||
Reference in New Issue
Block a user