From abc53cb208aac5546be811d6953c8ff6cad3035c Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 15 Jul 2023 11:49:58 +0800 Subject: [PATCH 01/10] feat: disable channel when account_deactivated received (close #271) --- controller/relay.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/relay.go b/controller/relay.go index 42aa0c0f..c8bd929c 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -124,7 +124,7 @@ func Relay(c *gin.Context) { channelId := c.GetInt("channel_id") common.SysError(fmt.Sprintf("relay error (channel #%d): %s", channelId, err.Message)) // https://platform.openai.com/docs/guides/error-codes/api-errors - if common.AutomaticDisableChannelEnabled && (err.Type == "insufficient_quota" || err.Code == "invalid_api_key") { + if common.AutomaticDisableChannelEnabled && (err.Type == "insufficient_quota" || err.Code == "invalid_api_key" || err.Code == "account_deactivated") { channelId := c.GetInt("channel_id") channelName := c.GetString("channel_name") disableChannel(channelId, channelName, err.Message) From 81c5901123b74553cfb004388d51779936c1afdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=A9=E7=89=9B=E7=89=9B?= Date: Sat, 15 Jul 2023 12:03:23 +0800 Subject: [PATCH 02/10] feat: add support for /v1/engines/text-embedding-ada-002/embeddings (#224, close #222) --- controller/relay-text.go | 6 +++++- controller/relay.go | 5 ++++- middleware/distributor.go | 8 +++++++- router/relay-router.go | 4 +++- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/controller/relay-text.go b/controller/relay-text.go index eab71a95..a26355e3 100644 --- a/controller/relay-text.go +++ b/controller/relay-text.go @@ -6,12 +6,13 @@ import ( "encoding/json" "errors" "fmt" - "github.com/gin-gonic/gin" "io" "net/http" "one-api/common" "one-api/model" "strings" + + "github.com/gin-gonic/gin" ) func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { @@ -30,6 +31,9 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { if relayMode == RelayModeModerations && textRequest.Model == "" { textRequest.Model = "text-moderation-latest" } + if relayMode == RelayModeEmbeddings && textRequest.Model == "" { + textRequest.Model = c.Param("model") + } // request validation if textRequest.Model == "" { return errorWrapper(errors.New("model is required"), "required_field_missing", http.StatusBadRequest) diff --git a/controller/relay.go b/controller/relay.go index c8bd929c..2f562799 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -2,10 +2,11 @@ package controller import ( "fmt" - "github.com/gin-gonic/gin" "net/http" "one-api/common" "strings" + + "github.com/gin-gonic/gin" ) type Message struct { @@ -100,6 +101,8 @@ func Relay(c *gin.Context) { relayMode = RelayModeCompletions } else if strings.HasPrefix(c.Request.URL.Path, "/v1/embeddings") { relayMode = RelayModeEmbeddings + } else if strings.HasSuffix(c.Request.URL.Path, "embeddings") { + relayMode = RelayModeEmbeddings } else if strings.HasPrefix(c.Request.URL.Path, "/v1/moderations") { relayMode = RelayModeModerations } else if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations") { diff --git a/middleware/distributor.go b/middleware/distributor.go index 314677c7..cb419d6d 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -2,12 +2,13 @@ package middleware import ( "fmt" - "github.com/gin-gonic/gin" "net/http" "one-api/common" "one-api/model" "strconv" "strings" + + "github.com/gin-gonic/gin" ) type ModelRequest struct { @@ -73,6 +74,11 @@ func Distribute() func(c *gin.Context) { modelRequest.Model = "text-moderation-stable" } } + if strings.HasSuffix(c.Request.URL.Path, "embeddings") { + if modelRequest.Model == "" { + modelRequest.Model = c.Param("model") + } + } channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, modelRequest.Model) if err != nil { message := "无可用渠道" diff --git a/router/relay-router.go b/router/relay-router.go index cbdfef11..cef5c7cc 100644 --- a/router/relay-router.go +++ b/router/relay-router.go @@ -1,9 +1,10 @@ package router import ( - "github.com/gin-gonic/gin" "one-api/controller" "one-api/middleware" + + "github.com/gin-gonic/gin" ) func SetRelayRouter(router *gin.Engine) { @@ -24,6 +25,7 @@ func SetRelayRouter(router *gin.Engine) { relayV1Router.POST("/images/edits", controller.RelayNotImplemented) relayV1Router.POST("/images/variations", controller.RelayNotImplemented) relayV1Router.POST("/embeddings", controller.Relay) + relayV1Router.POST("/engines/:model/embeddings", controller.Relay) relayV1Router.POST("/audio/transcriptions", controller.RelayNotImplemented) relayV1Router.POST("/audio/translations", controller.RelayNotImplemented) relayV1Router.GET("/files", controller.RelayNotImplemented) From b520b54625f30095aa15da3d1ad7bca2287ad3d3 Mon Sep 17 00:00:00 2001 From: ckt <65409152+ckt1031@users.noreply.github.com> Date: Sat, 15 Jul 2023 12:30:06 +0800 Subject: [PATCH 03/10] feat: initial support of Dall-E (#148, #266) * feat: initial support of Dall-E * fix: fix N not timed --------- Co-authored-by: JustSong Co-authored-by: JustSong <39998050+songquanpeng@users.noreply.github.com> --- README.md | 11 +-- common/model-ratio.go | 1 + controller/model.go | 10 +++ controller/relay-image.go | 161 ++++++++++++++++++++++++++++++++++++-- controller/relay.go | 14 ++++ middleware/distributor.go | 5 ++ router/relay-router.go | 2 +- 7 files changed, 191 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 5cb2a072..ed75e2b3 100644 --- a/README.md +++ b/README.md @@ -81,16 +81,17 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 12. 支持以美元为单位显示额度。 13. 支持发布公告,设置充值链接,设置新用户初始额度。 14. 支持模型映射,重定向用户的请求模型。 -15. 支持丰富的**自定义**设置, +15. 支持绘图接口。 +16. 支持丰富的**自定义**设置, 1. 支持自定义系统名称,logo 以及页脚。 2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。 -16. 支持通过系统访问令牌访问管理 API。 -17. 支持 Cloudflare Turnstile 用户校验。 -18. 支持用户管理,支持**多种用户登录注册方式**: +17. 支持通过系统访问令牌访问管理 API。 +18. 支持 Cloudflare Turnstile 用户校验。 +19. 支持用户管理,支持**多种用户登录注册方式**: + 邮箱登录注册以及通过邮箱进行密码重置。 + [GitHub 开放授权](https://github.com/settings/applications/new)。 + 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。 -19. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。 +20. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。 ## 部署 ### 基于 Docker 进行部署 diff --git a/common/model-ratio.go b/common/model-ratio.go index ece41ebd..d01c3ffb 100644 --- a/common/model-ratio.go +++ b/common/model-ratio.go @@ -35,6 +35,7 @@ var ModelRatio = map[string]float64{ "text-search-ada-doc-001": 10, "text-moderation-stable": 0.1, "text-moderation-latest": 0.1, + "dall-e": 8, } func ModelRatio2JSONString() string { diff --git a/controller/model.go b/controller/model.go index 83d0a774..2be935d6 100644 --- a/controller/model.go +++ b/controller/model.go @@ -2,6 +2,7 @@ package controller import ( "fmt" + "github.com/gin-gonic/gin" ) @@ -53,6 +54,15 @@ func init() { }) // https://platform.openai.com/docs/models/model-endpoint-compatibility openAIModels = []OpenAIModels{ + { + Id: "dall-e", + Object: "model", + Created: 1677649963, + OwnedBy: "openai", + Permission: permission, + Root: "dall-e", + Parent: nil, + }, { Id: "gpt-3.5-turbo", Object: "model", diff --git a/controller/relay-image.go b/controller/relay-image.go index c5311272..7a37be80 100644 --- a/controller/relay-image.go +++ b/controller/relay-image.go @@ -1,34 +1,181 @@ package controller import ( - "github.com/gin-gonic/gin" + "bytes" + "encoding/json" + "errors" + "fmt" "io" "net/http" + "one-api/common" + "one-api/model" + + "github.com/gin-gonic/gin" ) func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { - // TODO: this part is not finished - req, err := http.NewRequest(c.Request.Method, c.Request.RequestURI, c.Request.Body) + imageModel := "dall-e" + + tokenId := c.GetInt("token_id") + channelType := c.GetInt("channel") + userId := c.GetInt("id") + consumeQuota := c.GetBool("consume_quota") + group := c.GetString("group") + + var imageRequest ImageRequest + if consumeQuota { + err := common.UnmarshalBodyReusable(c, &imageRequest) + if err != nil { + return errorWrapper(err, "bind_request_body_failed", http.StatusBadRequest) + } + } + + // Prompt validation + if imageRequest.Prompt == "" { + return errorWrapper(errors.New("prompt is required"), "required_field_missing", http.StatusBadRequest) + } + + // Not "256x256", "512x512", or "1024x1024" + if imageRequest.Size != "" && imageRequest.Size != "256x256" && imageRequest.Size != "512x512" && imageRequest.Size != "1024x1024" { + return errorWrapper(errors.New("size must be one of 256x256, 512x512, or 1024x1024"), "invalid_field_value", http.StatusBadRequest) + } + + // N should between 1 and 10 + if imageRequest.N != 0 && (imageRequest.N < 1 || imageRequest.N > 10) { + return errorWrapper(errors.New("n must be between 1 and 10"), "invalid_field_value", http.StatusBadRequest) + } + + // map model name + modelMapping := c.GetString("model_mapping") + isModelMapped := false + if modelMapping != "" { + modelMap := make(map[string]string) + err := json.Unmarshal([]byte(modelMapping), &modelMap) + if err != nil { + return errorWrapper(err, "unmarshal_model_mapping_failed", http.StatusInternalServerError) + } + if modelMap[imageModel] != "" { + imageModel = modelMap[imageModel] + isModelMapped = true + } + } + + baseURL := common.ChannelBaseURLs[channelType] + requestURL := c.Request.URL.String() + + if c.GetString("base_url") != "" { + baseURL = c.GetString("base_url") + } + + fullRequestURL := fmt.Sprintf("%s%s", baseURL, requestURL) + + var requestBody io.Reader + if isModelMapped { + jsonStr, err := json.Marshal(imageRequest) + if err != nil { + return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError) + } + requestBody = bytes.NewBuffer(jsonStr) + } else { + requestBody = c.Request.Body + } + + modelRatio := common.GetModelRatio(imageModel) + groupRatio := common.GetGroupRatio(group) + ratio := modelRatio * groupRatio + userQuota, err := model.CacheGetUserQuota(userId) + + sizeRatio := 1.0 + // Size + if imageRequest.Size == "256x256" { + sizeRatio = 1 + } else if imageRequest.Size == "512x512" { + sizeRatio = 1.125 + } else if imageRequest.Size == "1024x1024" { + sizeRatio = 1.25 + } + quota := int(ratio*sizeRatio*1000) * imageRequest.N + + if consumeQuota && userQuota-quota < 0 { + return errorWrapper(err, "insufficient_user_quota", http.StatusForbidden) + } + + req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody) + if err != nil { + return errorWrapper(err, "new_request_failed", http.StatusInternalServerError) + } + 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")) + client := &http.Client{} resp, err := client.Do(req) if err != nil { - return errorWrapper(err, "do_request_failed", http.StatusOK) + return errorWrapper(err, "do_request_failed", http.StatusInternalServerError) } + err = req.Body.Close() if err != nil { - return errorWrapper(err, "close_request_body_failed", http.StatusOK) + return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError) } + err = c.Request.Body.Close() + if err != nil { + return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError) + } + var textResponse ImageResponse + + defer func() { + if consumeQuota { + err := model.PostConsumeTokenQuota(tokenId, quota) + if err != nil { + common.SysError("error consuming token remain quota: " + err.Error()) + } + err = model.CacheUpdateUserQuota(userId) + if err != nil { + common.SysError("error update user quota cache: " + err.Error()) + } + if quota != 0 { + tokenName := c.GetString("token_name") + logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio) + model.RecordConsumeLog(userId, 0, 0, imageModel, tokenName, quota, logContent) + model.UpdateUserUsedQuotaAndRequestCount(userId, quota) + channelId := c.GetInt("channel_id") + model.UpdateChannelUsedQuota(channelId, quota) + } + } + }() + + if consumeQuota { + responseBody, err := io.ReadAll(resp.Body) + + if err != nil { + return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) + } + err = resp.Body.Close() + if err != nil { + return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError) + } + err = json.Unmarshal(responseBody, &textResponse) + if err != nil { + return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError) + } + + resp.Body = io.NopCloser(bytes.NewBuffer(responseBody)) + } + for k, v := range resp.Header { c.Writer.Header().Set(k, v[0]) } c.Writer.WriteHeader(resp.StatusCode) + _, err = io.Copy(c.Writer, resp.Body) if err != nil { - return errorWrapper(err, "copy_response_body_failed", http.StatusOK) + return errorWrapper(err, "copy_response_body_failed", http.StatusInternalServerError) } err = resp.Body.Close() if err != nil { - return errorWrapper(err, "close_response_body_failed", http.StatusOK) + return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError) } return nil } diff --git a/controller/relay.go b/controller/relay.go index 2f562799..013e56d5 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -38,6 +38,7 @@ type GeneralOpenAIRequest struct { N int `json:"n,omitempty"` Input any `json:"input,omitempty"` Instruction string `json:"instruction,omitempty"` + Size string `json:"size,omitempty"` } type ChatRequest struct { @@ -54,6 +55,12 @@ type TextRequest struct { //Stream bool `json:"stream"` } +type ImageRequest struct { + Prompt string `json:"prompt"` + N int `json:"n"` + Size string `json:"size"` +} + type Usage struct { PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"` @@ -77,6 +84,13 @@ type TextResponse struct { Error OpenAIError `json:"error"` } +type ImageResponse struct { + Created int `json:"created"` + Data []struct { + Url string `json:"url"` + } +} + type ChatCompletionsStreamResponse struct { Choices []struct { Delta struct { diff --git a/middleware/distributor.go b/middleware/distributor.go index cb419d6d..a1baf52e 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -79,6 +79,11 @@ func Distribute() func(c *gin.Context) { modelRequest.Model = c.Param("model") } } + if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations") { + if modelRequest.Model == "" { + modelRequest.Model = "dall-e" + } + } channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, modelRequest.Model) if err != nil { message := "无可用渠道" diff --git a/router/relay-router.go b/router/relay-router.go index cef5c7cc..0c8e9415 100644 --- a/router/relay-router.go +++ b/router/relay-router.go @@ -21,7 +21,7 @@ func SetRelayRouter(router *gin.Engine) { relayV1Router.POST("/completions", controller.Relay) relayV1Router.POST("/chat/completions", controller.Relay) relayV1Router.POST("/edits", controller.Relay) - relayV1Router.POST("/images/generations", controller.RelayNotImplemented) + relayV1Router.POST("/images/generations", controller.Relay) relayV1Router.POST("/images/edits", controller.RelayNotImplemented) relayV1Router.POST("/images/variations", controller.RelayNotImplemented) relayV1Router.POST("/embeddings", controller.Relay) From d592e2c8b81c52ab870147c11e6005cc8f4976d6 Mon Sep 17 00:00:00 2001 From: ckt <65409152+ckt1031@users.noreply.github.com> Date: Sat, 15 Jul 2023 12:41:21 +0800 Subject: [PATCH 04/10] feat: add turnstile for login form (#263) --- router/api-router.go | 7 ++++--- web/src/components/LoginForm.js | 28 ++++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/router/api-router.go b/router/api-router.go index 3bbac17e..e89ba4e7 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -1,10 +1,11 @@ package router import ( - "github.com/gin-contrib/gzip" - "github.com/gin-gonic/gin" "one-api/controller" "one-api/middleware" + + "github.com/gin-contrib/gzip" + "github.com/gin-gonic/gin" ) func SetApiRouter(router *gin.Engine) { @@ -27,7 +28,7 @@ func SetApiRouter(router *gin.Engine) { userRoute := apiRouter.Group("/user") { userRoute.POST("/register", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Register) - userRoute.POST("/login", middleware.CriticalRateLimit(), controller.Login) + userRoute.POST("/login", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Login) userRoute.GET("/logout", controller.Logout) selfRoute := userRoute.Group("/") diff --git a/web/src/components/LoginForm.js b/web/src/components/LoginForm.js index 52e3c840..d3954cf8 100644 --- a/web/src/components/LoginForm.js +++ b/web/src/components/LoginForm.js @@ -12,7 +12,8 @@ import { } from 'semantic-ui-react'; import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import { UserContext } from '../context/User'; -import { API, getLogo, showError, showSuccess } from '../helpers'; +import { API, getLogo, showError, showSuccess, showInfo } from '../helpers'; +import Turnstile from 'react-turnstile'; const LoginForm = () => { const [inputs, setInputs] = useState({ @@ -24,6 +25,9 @@ const LoginForm = () => { const [submitted, setSubmitted] = useState(false); const { username, password } = inputs; const [userState, userDispatch] = useContext(UserContext); + const [turnstileEnabled, setTurnstileEnabled] = useState(false); + const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); + const [turnstileToken, setTurnstileToken] = useState(''); let navigate = useNavigate(); const [status, setStatus] = useState({}); @@ -37,6 +41,11 @@ const LoginForm = () => { if (status) { status = JSON.parse(status); setStatus(status); + + if (status.turnstile_check) { + setTurnstileEnabled(true); + setTurnstileSiteKey(status.turnstile_site_key); + } } }, []); @@ -76,7 +85,12 @@ const LoginForm = () => { async function handleSubmit(e) { setSubmitted(true); if (username && password) { - const res = await API.post('/api/user/login', { + if (turnstileEnabled && turnstileToken === '') { + showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); + return; + } + + const res = await API.post(`/api/user/login?turnstile=${turnstileToken}`, { username, password, }); @@ -119,6 +133,16 @@ const LoginForm = () => { value={password} onChange={handleChange} /> + {turnstileEnabled ? ( + { + setTurnstileToken(token); + }} + /> + ) : ( + <> + )} From ccf7709e23ddf3edace65f0ec00458fa3ce58944 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 15 Jul 2023 13:51:46 +0800 Subject: [PATCH 05/10] feat: support custom model now (close #276) --- web/src/pages/Channel/EditChannel.js | 39 ++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index a420f25f..0d9b169f 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { Button, Form, Header, Message, Segment } from 'semantic-ui-react'; +import { Button, Form, Header, Input, Message, Segment } from 'semantic-ui-react'; import { useParams } from 'react-router-dom'; import { API, showError, showInfo, showSuccess, verifyJSON } from '../../helpers'; import { CHANNEL_OPTIONS } from '../../constants'; @@ -31,6 +31,7 @@ const EditChannel = () => { const [groupOptions, setGroupOptions] = useState([]); const [basicModels, setBasicModels] = useState([]); const [fullModels, setFullModels] = useState([]); + const [customModel, setCustomModel] = useState(''); const handleInputChange = (e, { name, value }) => { setInputs((inputs) => ({ ...inputs, [name]: value })); }; @@ -43,6 +44,19 @@ const EditChannel = () => { data.models = []; } else { data.models = data.models.split(','); + setTimeout(() => { + let localModelOptions = [...modelOptions]; + data.models.forEach((model) => { + if (!localModelOptions.find((option) => option.key === model)) { + localModelOptions.push({ + key: model, + text: model, + value: model + }); + } + }); + setModelOptions(localModelOptions); + }, 1000); } if (data.group === '') { data.groups = []; @@ -263,6 +277,27 @@ const EditChannel = () => { + { + let localModels = [...inputs.models]; + localModels.push(customModel); + let localModelOptions = [...modelOptions]; + localModelOptions.push({ + key: customModel, + text: customModel, + value: customModel, + }); + setModelOptions(localModelOptions); + handleInputChange(null, { name: 'models', value: localModels }); + }}>填入 + } + placeholder='输入自定义模型名称' + value={customModel} + onChange={(e, { value }) => { + setCustomModel(value); + }} + /> { /> ) } - + From f61d3267218bd7d6406559c1c24c0b2e1c59ab18 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 15 Jul 2023 16:06:01 +0800 Subject: [PATCH 06/10] revert: do not enable turnstile check on login --- router/api-router.go | 2 +- web/src/components/LoginForm.js | 26 +------------------------- 2 files changed, 2 insertions(+), 26 deletions(-) diff --git a/router/api-router.go b/router/api-router.go index e89ba4e7..cc330d7e 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -28,7 +28,7 @@ func SetApiRouter(router *gin.Engine) { userRoute := apiRouter.Group("/user") { userRoute.POST("/register", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Register) - userRoute.POST("/login", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Login) + userRoute.POST("/login", middleware.CriticalRateLimit(), controller.Login) userRoute.GET("/logout", controller.Logout) selfRoute := userRoute.Group("/") diff --git a/web/src/components/LoginForm.js b/web/src/components/LoginForm.js index d3954cf8..bcc2df65 100644 --- a/web/src/components/LoginForm.js +++ b/web/src/components/LoginForm.js @@ -13,7 +13,6 @@ import { import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import { UserContext } from '../context/User'; import { API, getLogo, showError, showSuccess, showInfo } from '../helpers'; -import Turnstile from 'react-turnstile'; const LoginForm = () => { const [inputs, setInputs] = useState({ @@ -25,9 +24,6 @@ const LoginForm = () => { const [submitted, setSubmitted] = useState(false); const { username, password } = inputs; const [userState, userDispatch] = useContext(UserContext); - const [turnstileEnabled, setTurnstileEnabled] = useState(false); - const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); - const [turnstileToken, setTurnstileToken] = useState(''); let navigate = useNavigate(); const [status, setStatus] = useState({}); @@ -41,11 +37,6 @@ const LoginForm = () => { if (status) { status = JSON.parse(status); setStatus(status); - - if (status.turnstile_check) { - setTurnstileEnabled(true); - setTurnstileSiteKey(status.turnstile_site_key); - } } }, []); @@ -85,12 +76,7 @@ const LoginForm = () => { async function handleSubmit(e) { setSubmitted(true); if (username && password) { - if (turnstileEnabled && turnstileToken === '') { - showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); - return; - } - - const res = await API.post(`/api/user/login?turnstile=${turnstileToken}`, { + const res = await API.post(`/api/user/login`, { username, password, }); @@ -133,16 +119,6 @@ const LoginForm = () => { value={password} onChange={handleChange} /> - {turnstileEnabled ? ( - { - setTurnstileToken(token); - }} - /> - ) : ( - <> - )} From 0e088f7c3e53943d7ff85a4e71717b75cebda5e3 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 15 Jul 2023 17:07:05 +0800 Subject: [PATCH 07/10] feat: support ChatGLM2 (close #274) --- README.md | 3 ++- controller/model.go | 18 ++++++++++++++++++ controller/relay-text.go | 9 +++++---- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ed75e2b3..bb8c05e7 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,8 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 + 邮箱登录注册以及通过邮箱进行密码重置。 + [GitHub 开放授权](https://github.com/settings/applications/new)。 + 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。 -20. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。 +20. 支持 [ChatGLM](https://github.com/THUDM/ChatGLM2-6B)。 +21. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。 ## 部署 ### 基于 Docker 进行部署 diff --git a/controller/model.go b/controller/model.go index 2be935d6..5d7becb7 100644 --- a/controller/model.go +++ b/controller/model.go @@ -252,6 +252,24 @@ func init() { Root: "code-davinci-edit-001", Parent: nil, }, + { + Id: "ChatGLM", + Object: "model", + Created: 1677649963, + OwnedBy: "thudm", + Permission: permission, + Root: "ChatGLM", + Parent: nil, + }, + { + Id: "ChatGLM2", + Object: "model", + Created: 1677649963, + OwnedBy: "thudm", + Permission: permission, + Root: "ChatGLM2", + Parent: nil, + }, } openAIModelsMap = make(map[string]OpenAIModels) for _, model := range openAIModels { diff --git a/controller/relay-text.go b/controller/relay-text.go index a26355e3..e9461edd 100644 --- a/controller/relay-text.go +++ b/controller/relay-text.go @@ -227,8 +227,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { return 0, nil, nil } - if i := strings.Index(string(data), "\n\n"); i >= 0 { - return i + 2, data[0:i], nil + if i := strings.Index(string(data), "\n"); i >= 0 { + return i + 1, data[0:i], nil } if atEOF { @@ -242,8 +242,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { go func() { for scanner.Scan() { data := scanner.Text() - if len(data) < 6 { // must be something wrong! - common.SysError("invalid stream response: " + data) + if len(data) < 6 { // ignore blank line or wrong format continue } dataChan <- data @@ -286,6 +285,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { if strings.HasPrefix(data, "data: [DONE]") { data = data[:12] } + // some implementations may add \r at the end of data + data = strings.TrimSuffix(data, "\r") c.Render(-1, common.CustomEvent{Data: data}) return true case <-stopChan: From 35cfebee121a38b78f3dafc9793f85b068a75f5a Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 15 Jul 2023 19:06:51 +0800 Subject: [PATCH 08/10] feat: retry on failed (close #112) --- common/constants.go | 1 + controller/relay.go | 20 +++++++++++++++----- model/option.go | 3 +++ web/src/components/OperationSetting.js | 17 ++++++++++++++++- 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/common/constants.go b/common/constants.go index a97fda0e..4402b9d9 100644 --- a/common/constants.go +++ b/common/constants.go @@ -68,6 +68,7 @@ var AutomaticDisableChannelEnabled = false var QuotaRemindThreshold = 1000 var PreConsumedQuota = 500 var ApproximateTokenEnabled = false +var RetryTimes = 0 var RootUserEmail = "" diff --git a/controller/relay.go b/controller/relay.go index 013e56d5..45054945 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "one-api/common" + "strconv" "strings" "github.com/gin-gonic/gin" @@ -132,12 +133,21 @@ func Relay(c *gin.Context) { err = relayTextHelper(c, relayMode) } if err != nil { - if err.StatusCode == http.StatusTooManyRequests { - err.OpenAIError.Message = "当前分组负载已饱和,请稍后再试,或升级账户以提升服务质量。" + retryTimesStr := c.Query("retry") + retryTimes, _ := strconv.Atoi(retryTimesStr) + if retryTimesStr == "" { + retryTimes = common.RetryTimes + } + if retryTimes > 0 { + c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s?retry=%d", c.Request.URL.Path, retryTimes-1)) + } else { + if err.StatusCode == http.StatusTooManyRequests { + err.OpenAIError.Message = "当前分组负载已饱和,请稍后再试,或升级账户以提升服务质量。" + } + c.JSON(err.StatusCode, gin.H{ + "error": err.OpenAIError, + }) } - c.JSON(err.StatusCode, gin.H{ - "error": err.OpenAIError, - }) channelId := c.GetInt("channel_id") common.SysError(fmt.Sprintf("relay error (channel #%d): %s", channelId, err.Message)) // https://platform.openai.com/docs/guides/error-codes/api-errors diff --git a/model/option.go b/model/option.go index 35aeec4c..e7bc6806 100644 --- a/model/option.go +++ b/model/option.go @@ -68,6 +68,7 @@ func InitOptionMap() { common.OptionMap["TopUpLink"] = common.TopUpLink common.OptionMap["ChatLink"] = common.ChatLink common.OptionMap["QuotaPerUnit"] = strconv.FormatFloat(common.QuotaPerUnit, 'f', -1, 64) + common.OptionMap["RetryTimes"] = strconv.Itoa(common.RetryTimes) common.OptionMapRWMutex.Unlock() loadOptionsFromDatabase() } @@ -196,6 +197,8 @@ func updateOptionMap(key string, value string) (err error) { common.QuotaRemindThreshold, _ = strconv.Atoi(value) case "PreConsumedQuota": common.PreConsumedQuota, _ = strconv.Atoi(value) + case "RetryTimes": + common.RetryTimes, _ = strconv.Atoi(value) case "ModelRatio": err = common.UpdateModelRatioByJSONString(value) case "GroupRatio": diff --git a/web/src/components/OperationSetting.js b/web/src/components/OperationSetting.js index 69100c85..2adc7fa4 100644 --- a/web/src/components/OperationSetting.js +++ b/web/src/components/OperationSetting.js @@ -20,6 +20,7 @@ const OperationSetting = () => { DisplayInCurrencyEnabled: '', DisplayTokenStatEnabled: '', ApproximateTokenEnabled: '', + RetryTimes: 0, }); const [originInputs, setOriginInputs] = useState({}); let [loading, setLoading] = useState(false); @@ -122,6 +123,9 @@ const OperationSetting = () => { if (originInputs['QuotaPerUnit'] !== inputs.QuotaPerUnit) { await updateOption('QuotaPerUnit', inputs.QuotaPerUnit); } + if (originInputs['RetryTimes'] !== inputs.RetryTimes) { + await updateOption('RetryTimes', inputs.RetryTimes); + } break; } }; @@ -133,7 +137,7 @@ const OperationSetting = () => {
通用设置
- + { step='0.01' placeholder='一单位货币能兑换的额度' /> + Date: Sat, 15 Jul 2023 19:07:38 +0800 Subject: [PATCH 09/10] docs: update README --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index bb8c05e7..8f92bb50 100644 --- a/README.md +++ b/README.md @@ -81,18 +81,19 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 12. 支持以美元为单位显示额度。 13. 支持发布公告,设置充值链接,设置新用户初始额度。 14. 支持模型映射,重定向用户的请求模型。 -15. 支持绘图接口。 -16. 支持丰富的**自定义**设置, +15. 支持失败自动重试。 +16. 支持绘图接口。 +17. 支持丰富的**自定义**设置, 1. 支持自定义系统名称,logo 以及页脚。 2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。 -17. 支持通过系统访问令牌访问管理 API。 -18. 支持 Cloudflare Turnstile 用户校验。 -19. 支持用户管理,支持**多种用户登录注册方式**: +18. 支持通过系统访问令牌访问管理 API。 +19. 支持 Cloudflare Turnstile 用户校验。 +20. 支持用户管理,支持**多种用户登录注册方式**: + 邮箱登录注册以及通过邮箱进行密码重置。 + [GitHub 开放授权](https://github.com/settings/applications/new)。 + 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。 -20. 支持 [ChatGLM](https://github.com/THUDM/ChatGLM2-6B)。 -21. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。 +21. 支持 [ChatGLM](https://github.com/THUDM/ChatGLM2-6B)。 +22. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。 ## 部署 ### 基于 Docker 进行部署 From 4139a7036fd8258875103aa681676dbf96a4c73d Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 15 Jul 2023 23:01:54 +0800 Subject: [PATCH 10/10] chore: make subscription api compatible with official api --- controller/billing.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/controller/billing.go b/controller/billing.go index a45253ab..2ef2d99c 100644 --- a/controller/billing.go +++ b/controller/billing.go @@ -7,16 +7,19 @@ import ( ) func GetSubscription(c *gin.Context) { - var quota int + var remainQuota int + var usedQuota int var err error var token *model.Token if common.DisplayTokenStatEnabled { tokenId := c.GetInt("token_id") token, err = model.GetTokenById(tokenId) - quota = token.RemainQuota + remainQuota = token.RemainQuota + usedQuota = token.UsedQuota } else { userId := c.GetInt("id") - quota, err = model.GetUserQuota(userId) + remainQuota, err = model.GetUserQuota(userId) + usedQuota, err = model.GetUserUsedQuota(userId) } if err != nil { openAIError := OpenAIError{ @@ -28,6 +31,7 @@ func GetSubscription(c *gin.Context) { }) return } + quota := remainQuota + usedQuota amount := float64(quota) if common.DisplayInCurrencyEnabled { amount /= common.QuotaPerUnit