mirror of
				https://github.com/songquanpeng/one-api.git
				synced 2025-10-31 13:53:41 +08:00 
			
		
		
		
	Compare commits
	
		
			13 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | e6af636fa0 | ||
|  | 6e1ef75009 | ||
|  | d9db16e999 | ||
|  | 241ade2fae | ||
|  | 80065de8a3 | ||
|  | 16f53b5afb | ||
|  | 3071300c0c | ||
|  | 8b056bf408 | ||
|  | e5640857b1 | ||
|  | 331177d97e | ||
|  | 4fed003f1a | ||
|  | a1ea1bf696 | ||
|  | 7c66fc6c21 | 
							
								
								
									
										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 Max](https://openaimax.com) | ||||
|    + [x] [OhMyGPT](https://www.ohmygpt.com) | ||||
|    + [x] 自定义渠道 | ||||
|    + [x] 自定义渠道:例如使用自行搭建的 OpenAI 代理 | ||||
| 2. 支持通过负载均衡的方式访问多个渠道。 | ||||
| 3. 支持单个访问渠道设置多个 API Key,利用起来你的多个 API Key。 | ||||
| 4. 支持 HTTP SSE,可以通过流式传输实现打字机效果。 | ||||
|   | ||||
| @@ -49,11 +49,6 @@ var TurnstileSecretKey = "" | ||||
|  | ||||
| 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 ( | ||||
| 	RoleGuestUser  = 0 | ||||
| 	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 | ||||
| } | ||||
| @@ -94,10 +94,6 @@ func relayHelper(c *gin.Context) error { | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	err = c.Request.Body.Close() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	req.Header.Set("Authorization", c.Request.Header.Get("Authorization")) | ||||
| 	req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type")) | ||||
| 	req.Header.Set("Accept", c.Request.Header.Get("Accept")) | ||||
| @@ -111,7 +107,10 @@ func relayHelper(c *gin.Context) error { | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	err = c.Request.Body.Close() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	var textResponse TextResponse | ||||
| 	isStream := resp.Header.Get("Content-Type") == "text/event-stream" | ||||
| 	var streamResponseText string | ||||
| @@ -119,26 +118,24 @@ func relayHelper(c *gin.Context) error { | ||||
| 	defer func() { | ||||
| 		if consumeQuota { | ||||
| 			quota := 0 | ||||
| 			usingGPT4 := strings.HasPrefix(textRequest.Model, "gpt-4") | ||||
| 			completionRatio := 1 | ||||
| 			if usingGPT4 { | ||||
| 				completionRatio = 2 | ||||
| 			} | ||||
| 			if isStream { | ||||
| 				var text string | ||||
| 				var promptText string | ||||
| 				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) | ||||
| 				quota = countToken(text) + 3 | ||||
| 				completionText := fmt.Sprintf("%s: %s\n", "assistant", streamResponseText) | ||||
| 				quota = countToken(promptText) + countToken(completionText)*completionRatio + 3 | ||||
| 			} else { | ||||
| 				quota = textResponse.Usage.TotalTokens | ||||
| 			} | ||||
| 			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 | ||||
| 				quota = textResponse.Usage.PromptTokens + textResponse.Usage.CompletionTokens*completionRatio | ||||
| 			} | ||||
| 			ratio := common.GetModelRatio(textRequest.Model) | ||||
| 			quota = int(float64(quota) * ratio) | ||||
| 			err := model.ConsumeTokenQuota(tokenId, quota) | ||||
| 			err := model.DecreaseTokenQuota(tokenId, quota) | ||||
| 			if err != nil { | ||||
| 				common.SysError("Error consuming token remain quota: " + err.Error()) | ||||
| 			} | ||||
|   | ||||
| @@ -75,6 +75,30 @@ func GetToken(c *gin.Context) { | ||||
| 	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) { | ||||
| 	isAdmin := c.GetInt("role") >= common.RoleAdminUser | ||||
| 	token := model.Token{} | ||||
| @@ -114,10 +138,8 @@ func AddToken(c *gin.Context) { | ||||
| 			}) | ||||
| 			return | ||||
| 		} | ||||
| 		if quota > 0 { | ||||
| 		cleanToken.RemainQuota = quota | ||||
| 	} | ||||
| 	} | ||||
| 	err = cleanToken.Insert() | ||||
| 	if err != nil { | ||||
| 		c.JSON(http.StatusOK, gin.H{ | ||||
|   | ||||
							
								
								
									
										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["TurnstileSecretKey"] = "" | ||||
| 	common.OptionMap["QuotaForNewUser"] = strconv.Itoa(common.QuotaForNewUser) | ||||
| 	common.OptionMap["RatioGPT3dot5"] = strconv.FormatFloat(common.RatioGPT3dot5, 'f', -1, 64) | ||||
| 	common.OptionMap["RatioGPT4"] = strconv.FormatFloat(common.RatioGPT4, 'f', -1, 64) | ||||
| 	common.OptionMap["RatioGPT4_32k"] = strconv.FormatFloat(common.RatioGPT4_32k, 'f', -1, 64) | ||||
| 	common.OptionMap["ModelRatio"] = common.ModelRatio2JSONString() | ||||
| 	common.OptionMap["TopUpLink"] = common.TopUpLink | ||||
| 	common.OptionMapRWMutex.Unlock() | ||||
| 	options, _ := AllOption() | ||||
| @@ -75,7 +73,7 @@ func UpdateOption(key string, value string) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func updateOptionMap(key string, value string) { | ||||
| func updateOptionMap(key string, value string) (err error) { | ||||
| 	common.OptionMapRWMutex.Lock() | ||||
| 	defer common.OptionMapRWMutex.Unlock() | ||||
| 	common.OptionMap[key] = value | ||||
| @@ -138,13 +136,10 @@ func updateOptionMap(key string, value string) { | ||||
| 		common.TurnstileSecretKey = value | ||||
| 	case "QuotaForNewUser": | ||||
| 		common.QuotaForNewUser, _ = strconv.Atoi(value) | ||||
| 	case "RatioGPT3dot5": | ||||
| 		common.RatioGPT3dot5, _ = strconv.ParseFloat(value, 64) | ||||
| 	case "RatioGPT4": | ||||
| 		common.RatioGPT4, _ = strconv.ParseFloat(value, 64) | ||||
| 	case "RatioGPT4_32k": | ||||
| 		common.RatioGPT4_32k, _ = strconv.ParseFloat(value, 64) | ||||
| 	case "ModelRatio": | ||||
| 		err = common.UpdateModelRatioByJSONString(value) | ||||
| 	case "TopUpLink": | ||||
| 		common.TopUpLink = value | ||||
| 	} | ||||
| 	return err | ||||
| } | ||||
|   | ||||
| @@ -55,7 +55,7 @@ func Redeem(key string, tokenId int) (quota int, err error) { | ||||
| 	if redemption.Status != common.RedemptionCodeStatusEnabled { | ||||
| 		return 0, errors.New("该兑换码已被使用") | ||||
| 	} | ||||
| 	err = TopUpTokenQuota(tokenId, redemption.Quota) | ||||
| 	err = IncreaseTokenQuota(tokenId, redemption.Quota) | ||||
| 	if err != nil { | ||||
| 		return 0, err | ||||
| 	} | ||||
|   | ||||
| @@ -116,15 +116,26 @@ func DeleteTokenById(id int, userId int) (err error) { | ||||
| 	if err != nil { | ||||
| 		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() | ||||
| } | ||||
|  | ||||
| func ConsumeTokenQuota(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) { | ||||
| 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 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 | ||||
| } | ||||
|  | ||||
| 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) { | ||||
| 	err = DB.Model(&User{}).Where("id = ?", id).Update("quota", gorm.Expr("quota - ?", quota)).Error | ||||
| 	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) { | ||||
| 	SetApiRouter(router) | ||||
| 	SetDashboardRouter(router) | ||||
| 	SetRelayRouter(router) | ||||
| 	setWebRouter(router, buildFS, indexPage) | ||||
| } | ||||
|   | ||||
| @@ -19,7 +19,7 @@ func SetRelayRouter(router *gin.Engine) { | ||||
| 		relayV1Router.POST("/images/generations", controller.RelayNotImplemented) | ||||
| 		relayV1Router.POST("/images/edits", 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/translations", controller.RelayNotImplemented) | ||||
| 		relayV1Router.GET("/files", controller.RelayNotImplemented) | ||||
| @@ -35,9 +35,4 @@ func SetRelayRouter(router *gin.Engine) { | ||||
| 		relayV1Router.DELETE("/models/:model", 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 { Divider, Form, Grid, Header, Message } from 'semantic-ui-react'; | ||||
| import { API, removeTrailingSlash, showError } from '../helpers'; | ||||
| import { API, removeTrailingSlash, showError, verifyJSON } from '../helpers'; | ||||
|  | ||||
| const SystemSetting = () => { | ||||
|   let [inputs, setInputs] = useState({ | ||||
| @@ -25,9 +25,7 @@ const SystemSetting = () => { | ||||
|     TurnstileSecretKey: '', | ||||
|     RegisterEnabled: '', | ||||
|     QuotaForNewUser: 0, | ||||
|     RatioGPT3dot5: 2, | ||||
|     RatioGPT4: 30, | ||||
|     RatioGPT4_32k: 60, | ||||
|     ModelRatio: '', | ||||
|     TopUpLink: '' | ||||
|   }); | ||||
|   let originInputs = {}; | ||||
| @@ -93,7 +91,7 @@ const SystemSetting = () => { | ||||
|       name === 'TurnstileSiteKey' || | ||||
|       name === 'TurnstileSecretKey' || | ||||
|       name === 'QuotaForNewUser' || | ||||
|       name.startsWith('Ratio') || | ||||
|       name === 'ModelRatio' || | ||||
|       name === 'TopUpLink' | ||||
|     ) { | ||||
|       setInputs((inputs) => ({ ...inputs, [name]: value })); | ||||
| @@ -111,19 +109,17 @@ const SystemSetting = () => { | ||||
|     if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) { | ||||
|       await updateOption('QuotaForNewUser', inputs.QuotaForNewUser); | ||||
|     } | ||||
|     if (originInputs['RatioGPT3dot5'] !== inputs.RatioGPT3dot5) { | ||||
|       await updateOption('RatioGPT3dot5', inputs.RatioGPT3dot5); | ||||
|     if (originInputs['ModelRatio'] !== inputs.ModelRatio) { | ||||
|       if (!verifyJSON(inputs.ModelRatio)) { | ||||
|         showError('模型倍率不是合法的 JSON 字符串'); | ||||
|         return; | ||||
|       } | ||||
|     if (originInputs['RatioGPT4'] !== inputs.RatioGPT4) { | ||||
|       await updateOption('RatioGPT4', inputs.RatioGPT4); | ||||
|     } | ||||
|     if (originInputs['RatioGPT4_32k'] !== inputs.RatioGPT4_32k) { | ||||
|       await updateOption('RatioGPT4_32k', inputs.RatioGPT4_32k); | ||||
|       await updateOption('ModelRatio', inputs.ModelRatio); | ||||
|     } | ||||
|     if (originInputs['TopUpLink'] !== inputs.TopUpLink) { | ||||
|       await updateOption('TopUpLink', inputs.TopUpLink); | ||||
|     } | ||||
|   } | ||||
|   }; | ||||
|  | ||||
|   const submitSMTP = async () => { | ||||
|     if (originInputs['SMTPServer'] !== inputs.SMTPServer) { | ||||
| @@ -278,39 +274,15 @@ const SystemSetting = () => { | ||||
|               placeholder='例如发卡网站的购买链接' | ||||
|             /> | ||||
|           </Form.Group> | ||||
|           <Form.Group widths={3}> | ||||
|             <Form.Input | ||||
|               label='GPT-3.5 系列模型倍率' | ||||
|               name='RatioGPT3dot5' | ||||
|           <Form.Group widths='equal'> | ||||
|             <Form.TextArea | ||||
|               label='模型倍率' | ||||
|               name='ModelRatio' | ||||
|               onChange={handleInputChange} | ||||
|               style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }} | ||||
|               autoComplete='off' | ||||
|               value={inputs.RatioGPT3dot5} | ||||
|               type='number' | ||||
|               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' | ||||
|               value={inputs.ModelRatio} | ||||
|               placeholder='为一个 JSON 文本,键为模型名称,值为倍率' | ||||
|             /> | ||||
|           </Form.Group> | ||||
|           <Form.Button onClick={submitOperationConfig}>保存运营设置</Form.Button> | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| 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 { API, showError, showSuccess } from '../helpers'; | ||||
|  | ||||
| @@ -237,15 +237,25 @@ const UsersTable = () => { | ||||
|                       > | ||||
|                         降级 | ||||
|                       </Button> | ||||
|                       <Popup | ||||
|                         trigger={ | ||||
|                           <Button size='small' negative> | ||||
|                             删除 | ||||
|                           </Button> | ||||
|                         } | ||||
|                         on='click' | ||||
|                         flowing | ||||
|                         hoverable | ||||
|                       > | ||||
|                         <Button | ||||
|                         size={'small'} | ||||
|                           negative | ||||
|                           onClick={() => { | ||||
|                             manageUser(user.username, 'delete', idx); | ||||
|                           }} | ||||
|                         > | ||||
|                         删除 | ||||
|                           删除用户 {user.username} | ||||
|                         </Button> | ||||
|                       </Popup> | ||||
|                       <Button | ||||
|                         size={'small'} | ||||
|                         onClick={() => { | ||||
|   | ||||
| @@ -153,3 +153,12 @@ export function downloadTextAsFile(text, filename) { | ||||
|   a.download = filename; | ||||
|   a.click(); | ||||
| } | ||||
|  | ||||
| export const verifyJSON = (str) => { | ||||
|   try { | ||||
|     JSON.parse(str); | ||||
|   } catch (e) { | ||||
|     return false; | ||||
|   } | ||||
|   return true; | ||||
| }; | ||||
| @@ -52,7 +52,7 @@ const AddChannel = () => { | ||||
|                 <Form.Input | ||||
|                   label='Base URL' | ||||
|                   name='base_url' | ||||
|                   placeholder={'请输入自定义渠道的 Base URL'} | ||||
|                   placeholder={'请输入自定义渠道的 Base URL,例如:https://openai.justsong.cn'} | ||||
|                   onChange={handleInputChange} | ||||
|                   value={inputs.base_url} | ||||
|                   autoComplete='off' | ||||
|   | ||||
| @@ -66,7 +66,7 @@ const EditChannel = () => { | ||||
|                 <Form.Input | ||||
|                   label='Base URL' | ||||
|                   name='base_url' | ||||
|                   placeholder={'请输入新的自定义渠道的 Base URL'} | ||||
|                   placeholder={'请输入新的自定义渠道的 Base URL,例如:https://openai.justsong.cn'} | ||||
|                   onChange={handleInputChange} | ||||
|                   value={inputs.base_url} | ||||
|                   autoComplete='off' | ||||
|   | ||||
		Reference in New Issue
	
	Block a user