From 0a39521a3d77e6ddcf2f4bf62a652499f3c68184 Mon Sep 17 00:00:00 2001 From: JustSong Date: Thu, 27 Jul 2023 23:16:11 +0800 Subject: [PATCH 01/73] fix: convert system message to user message (close #328) --- controller/relay-baidu.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/controller/relay-baidu.go b/controller/relay-baidu.go index e82f4904..4267757d 100644 --- a/controller/relay-baidu.go +++ b/controller/relay-baidu.go @@ -57,10 +57,21 @@ type BaiduChatStreamResponse struct { func requestOpenAI2Baidu(request GeneralOpenAIRequest) *BaiduChatRequest { messages := make([]BaiduMessage, 0, len(request.Messages)) for _, message := range request.Messages { - messages = append(messages, BaiduMessage{ - Role: message.Role, - Content: message.Content, - }) + if message.Role == "system" { + messages = append(messages, BaiduMessage{ + Role: "user", + Content: message.Content, + }) + messages = append(messages, BaiduMessage{ + Role: "assistant", + Content: "Okay", + }) + } else { + messages = append(messages, BaiduMessage{ + Role: message.Role, + Content: message.Content, + }) + } } return &BaiduChatRequest{ Messages: messages, From b9f6461dd424cfb2e8f527e1947b8722a42fe513 Mon Sep 17 00:00:00 2001 From: JustSong Date: Thu, 27 Jul 2023 23:26:56 +0800 Subject: [PATCH 02/73] fix: convert system message to user message for claude --- controller/relay-claude.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/controller/relay-claude.go b/controller/relay-claude.go index 22f41cef..1d67fa7b 100644 --- a/controller/relay-claude.go +++ b/controller/relay-claude.go @@ -69,11 +69,11 @@ func requestOpenAI2Claude(textRequest GeneralOpenAIRequest) *ClaudeRequest { prompt += fmt.Sprintf("\n\nHuman: %s", message.Content) } else if message.Role == "assistant" { prompt += fmt.Sprintf("\n\nAssistant: %s", message.Content) - } else { - // ignore other roles + } else if message.Role == "system" { + prompt += fmt.Sprintf("\n\nSystem: %s", message.Content) } - prompt += "\n\nAssistant:" } + prompt += "\n\nAssistant:" claudeRequest.Prompt = prompt return &claudeRequest } From d1b6f492b6f13c4b1f3d63046b721f3677e876db Mon Sep 17 00:00:00 2001 From: JustSong Date: Thu, 27 Jul 2023 23:32:00 +0800 Subject: [PATCH 03/73] fix: convert system message to user message for zhipu --- controller/relay-zhipu.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/controller/relay-zhipu.go b/controller/relay-zhipu.go index 349f4742..33d141c7 100644 --- a/controller/relay-zhipu.go +++ b/controller/relay-zhipu.go @@ -111,10 +111,21 @@ func getZhipuToken(apikey string) string { func requestOpenAI2Zhipu(request GeneralOpenAIRequest) *ZhipuRequest { messages := make([]ZhipuMessage, 0, len(request.Messages)) for _, message := range request.Messages { - messages = append(messages, ZhipuMessage{ - Role: message.Role, - Content: message.Content, - }) + if message.Role == "system" { + messages = append(messages, ZhipuMessage{ + Role: "system", + Content: message.Content, + }) + messages = append(messages, ZhipuMessage{ + Role: "user", + Content: "Okay", + }) + } else { + messages = append(messages, ZhipuMessage{ + Role: message.Role, + Content: message.Content, + }) + } } return &ZhipuRequest{ Prompt: messages, From e92da7928b4029c00666ae54b5d83af70860f6ce Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 28 Jul 2023 23:45:08 +0800 Subject: [PATCH 04/73] feat: support ali's llm (close #326) --- README.md | 1 + common/constants.go | 36 ++-- common/model-ratio.go | 2 + controller/model.go | 18 ++ controller/relay-ali.go | 240 +++++++++++++++++++++++++ controller/relay-text.go | 40 ++++- web/src/constants/channel.constants.js | 3 +- 7 files changed, 321 insertions(+), 19 deletions(-) create mode 100644 controller/relay-ali.go diff --git a/README.md b/README.md index 93fb8247..e5579c3d 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 + [x] [Anthropic Claude 系列模型](https://anthropic.com) + [x] [Google PaLM2 系列模型](https://developers.generativeai.google) + [x] [百度文心一言系列模型](https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html) + + [x] [阿里通义千问系列模型](https://help.aliyun.com/document_detail/2400395.html) + [x] [智谱 ChatGLM 系列模型](https://bigmodel.cn) 2. 支持配置镜像以及众多第三方代理服务: + [x] [API Distribute](https://api.gptjk.top/register?aff=QGxj) diff --git a/common/constants.go b/common/constants.go index 81f98163..5dbfa71c 100644 --- a/common/constants.go +++ b/common/constants.go @@ -156,24 +156,26 @@ const ( ChannelTypeAnthropic = 14 ChannelTypeBaidu = 15 ChannelTypeZhipu = 16 + ChannelTypeAli = 17 ) var ChannelBaseURLs = []string{ - "", // 0 - "https://api.openai.com", // 1 - "https://oa.api2d.net", // 2 - "", // 3 - "https://api.closeai-proxy.xyz", // 4 - "https://api.openai-sb.com", // 5 - "https://api.openaimax.com", // 6 - "https://api.ohmygpt.com", // 7 - "", // 8 - "https://api.caipacity.com", // 9 - "https://api.aiproxy.io", // 10 - "", // 11 - "https://api.api2gpt.com", // 12 - "https://api.aigc2d.com", // 13 - "https://api.anthropic.com", // 14 - "https://aip.baidubce.com", // 15 - "https://open.bigmodel.cn", // 16 + "", // 0 + "https://api.openai.com", // 1 + "https://oa.api2d.net", // 2 + "", // 3 + "https://api.closeai-proxy.xyz", // 4 + "https://api.openai-sb.com", // 5 + "https://api.openaimax.com", // 6 + "https://api.ohmygpt.com", // 7 + "", // 8 + "https://api.caipacity.com", // 9 + "https://api.aiproxy.io", // 10 + "", // 11 + "https://api.api2gpt.com", // 12 + "https://api.aigc2d.com", // 13 + "https://api.anthropic.com", // 14 + "https://aip.baidubce.com", // 15 + "https://open.bigmodel.cn", // 16 + "https://dashscope.aliyuncs.com", // 17 } diff --git a/common/model-ratio.go b/common/model-ratio.go index 7f991777..ba6d7245 100644 --- a/common/model-ratio.go +++ b/common/model-ratio.go @@ -46,6 +46,8 @@ var ModelRatio = map[string]float64{ "chatglm_pro": 0.7143, // ¥0.01 / 1k tokens "chatglm_std": 0.3572, // ¥0.005 / 1k tokens "chatglm_lite": 0.1429, // ¥0.002 / 1k tokens + "qwen-v1": 0.8572, // TBD: https://help.aliyun.com/document_detail/2399482.html?spm=a2c4g.2399482.0.0.1ad347feilAgag + "qwen-plus-v1": 0.5715, // Same as above } func ModelRatio2JSONString() string { diff --git a/controller/model.go b/controller/model.go index b469271f..f8096f75 100644 --- a/controller/model.go +++ b/controller/model.go @@ -324,6 +324,24 @@ func init() { Root: "chatglm_lite", Parent: nil, }, + { + Id: "qwen-v1", + Object: "model", + Created: 1677649963, + OwnedBy: "ali", + Permission: permission, + Root: "qwen-v1", + Parent: nil, + }, + { + Id: "qwen-plus-v1", + Object: "model", + Created: 1677649963, + OwnedBy: "ali", + Permission: permission, + Root: "qwen-plus-v1", + Parent: nil, + }, } openAIModelsMap = make(map[string]OpenAIModels) for _, model := range openAIModels { diff --git a/controller/relay-ali.go b/controller/relay-ali.go new file mode 100644 index 00000000..e8437c27 --- /dev/null +++ b/controller/relay-ali.go @@ -0,0 +1,240 @@ +package controller + +import ( + "bufio" + "encoding/json" + "github.com/gin-gonic/gin" + "io" + "net/http" + "one-api/common" + "strings" +) + +// https://help.aliyun.com/document_detail/613695.html?spm=a2c4g.2399480.0.0.1adb778fAdzP9w#341800c0f8w0r + +type AliMessage struct { + User string `json:"user"` + Bot string `json:"bot"` +} + +type AliInput struct { + Prompt string `json:"prompt"` + History []AliMessage `json:"history"` +} + +type AliParameters struct { + TopP float64 `json:"top_p,omitempty"` + TopK int `json:"top_k,omitempty"` + Seed uint64 `json:"seed,omitempty"` + EnableSearch bool `json:"enable_search,omitempty"` +} + +type AliChatRequest struct { + Model string `json:"model"` + Input AliInput `json:"input"` + Parameters AliParameters `json:"parameters,omitempty"` +} + +type AliError struct { + Code string `json:"code"` + Message string `json:"message"` + RequestId string `json:"request_id"` +} + +type AliUsage struct { + InputTokens int `json:"input_tokens"` + OutputTokens int `json:"output_tokens"` +} + +type AliOutput struct { + Text string `json:"text"` + FinishReason string `json:"finish_reason"` +} + +type AliChatResponse struct { + Output AliOutput `json:"output"` + Usage AliUsage `json:"usage"` + AliError +} + +func requestOpenAI2Ali(request GeneralOpenAIRequest) *AliChatRequest { + messages := make([]AliMessage, 0, len(request.Messages)) + prompt := "" + for i := 0; i < len(request.Messages); i++ { + message := request.Messages[i] + if message.Role == "system" { + messages = append(messages, AliMessage{ + User: message.Content, + Bot: "Okay", + }) + continue + } else { + if i == len(request.Messages)-1 { + prompt = message.Content + break + } + messages = append(messages, AliMessage{ + User: message.Content, + Bot: request.Messages[i+1].Content, + }) + i++ + } + } + return &AliChatRequest{ + Model: request.Model, + Input: AliInput{ + Prompt: prompt, + History: messages, + }, + //Parameters: AliParameters{ // ChatGPT's parameters are not compatible with Ali's + // TopP: request.TopP, + // TopK: 50, + // //Seed: 0, + // //EnableSearch: false, + //}, + } +} + +func responseAli2OpenAI(response *AliChatResponse) *OpenAITextResponse { + choice := OpenAITextResponseChoice{ + Index: 0, + Message: Message{ + Role: "assistant", + Content: response.Output.Text, + }, + FinishReason: response.Output.FinishReason, + } + fullTextResponse := OpenAITextResponse{ + Id: response.RequestId, + Object: "chat.completion", + Created: common.GetTimestamp(), + Choices: []OpenAITextResponseChoice{choice}, + Usage: Usage{ + PromptTokens: response.Usage.InputTokens, + CompletionTokens: response.Usage.OutputTokens, + TotalTokens: response.Usage.InputTokens + response.Usage.OutputTokens, + }, + } + return &fullTextResponse +} + +func streamResponseAli2OpenAI(aliResponse *AliChatResponse) *ChatCompletionsStreamResponse { + var choice ChatCompletionsStreamResponseChoice + choice.Delta.Content = aliResponse.Output.Text + choice.FinishReason = aliResponse.Output.FinishReason + response := ChatCompletionsStreamResponse{ + Id: aliResponse.RequestId, + Object: "chat.completion.chunk", + Created: common.GetTimestamp(), + Model: "ernie-bot", + Choices: []ChatCompletionsStreamResponseChoice{choice}, + } + return &response +} + +func aliStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) { + var usage Usage + scanner := bufio.NewScanner(resp.Body) + scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + if i := strings.Index(string(data), "\n"); i >= 0 { + return i + 1, data[0:i], nil + } + if atEOF { + return len(data), data, nil + } + return 0, nil, nil + }) + dataChan := make(chan string) + stopChan := make(chan bool) + go func() { + for scanner.Scan() { + data := scanner.Text() + if len(data) < 5 { // ignore blank line or wrong format + continue + } + if data[:5] != "data:" { + continue + } + data = data[5:] + dataChan <- data + } + stopChan <- true + }() + c.Writer.Header().Set("Content-Type", "text/event-stream") + c.Writer.Header().Set("Cache-Control", "no-cache") + c.Writer.Header().Set("Connection", "keep-alive") + c.Writer.Header().Set("Transfer-Encoding", "chunked") + c.Writer.Header().Set("X-Accel-Buffering", "no") + lastResponseText := "" + c.Stream(func(w io.Writer) bool { + select { + case data := <-dataChan: + var aliResponse AliChatResponse + err := json.Unmarshal([]byte(data), &aliResponse) + if err != nil { + common.SysError("error unmarshalling stream response: " + err.Error()) + return true + } + usage.PromptTokens += aliResponse.Usage.InputTokens + usage.CompletionTokens += aliResponse.Usage.OutputTokens + usage.TotalTokens += aliResponse.Usage.InputTokens + aliResponse.Usage.OutputTokens + response := streamResponseAli2OpenAI(&aliResponse) + response.Choices[0].Delta.Content = strings.TrimPrefix(response.Choices[0].Delta.Content, lastResponseText) + lastResponseText = aliResponse.Output.Text + jsonResponse, err := json.Marshal(response) + if err != nil { + common.SysError("error marshalling stream response: " + err.Error()) + return true + } + c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)}) + return true + case <-stopChan: + c.Render(-1, common.CustomEvent{Data: "data: [DONE]"}) + return false + } + }) + err := resp.Body.Close() + if err != nil { + return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil + } + return nil, &usage +} + +func aliHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) { + var aliResponse AliChatResponse + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil + } + err = resp.Body.Close() + if err != nil { + return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil + } + err = json.Unmarshal(responseBody, &aliResponse) + if err != nil { + return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil + } + if aliResponse.Code != "" { + return &OpenAIErrorWithStatusCode{ + OpenAIError: OpenAIError{ + Message: aliResponse.Message, + Type: aliResponse.Code, + Param: aliResponse.RequestId, + Code: aliResponse.Code, + }, + StatusCode: resp.StatusCode, + }, nil + } + fullTextResponse := responseAli2OpenAI(&aliResponse) + jsonResponse, err := json.Marshal(fullTextResponse) + if err != nil { + return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil + } + c.Writer.Header().Set("Content-Type", "application/json") + c.Writer.WriteHeader(resp.StatusCode) + _, err = c.Writer.Write(jsonResponse) + return nil, &fullTextResponse.Usage +} diff --git a/controller/relay-text.go b/controller/relay-text.go index 52e10f2b..e58c810b 100644 --- a/controller/relay-text.go +++ b/controller/relay-text.go @@ -20,6 +20,7 @@ const ( APITypePaLM APITypeBaidu APITypeZhipu + APITypeAli ) var httpClient *http.Client @@ -94,6 +95,9 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { apiType = APITypePaLM case common.ChannelTypeZhipu: apiType = APITypeZhipu + case common.ChannelTypeAli: + apiType = APITypeAli + } baseURL := common.ChannelBaseURLs[channelType] requestURL := c.Request.URL.String() @@ -153,6 +157,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { method = "sse-invoke" } fullRequestURL = fmt.Sprintf("https://open.bigmodel.cn/api/paas/v3/model-api/%s/%s", textRequest.Model, method) + case APITypeAli: + fullRequestURL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation" } var promptTokens int var completionTokens int @@ -226,6 +232,13 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError) } requestBody = bytes.NewBuffer(jsonStr) + case APITypeAli: + aliRequest := requestOpenAI2Ali(textRequest) + jsonStr, err := json.Marshal(aliRequest) + if err != nil { + return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError) + } + requestBody = bytes.NewBuffer(jsonStr) } req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody) if err != nil { @@ -250,6 +263,11 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { case APITypeZhipu: token := getZhipuToken(apiKey) req.Header.Set("Authorization", token) + case APITypeAli: + req.Header.Set("Authorization", "Bearer "+apiKey) + if textRequest.Stream { + req.Header.Set("X-DashScope-SSE", "enable") + } } req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type")) req.Header.Set("Accept", c.Request.Header.Get("Accept")) @@ -280,7 +298,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { if strings.HasPrefix(textRequest.Model, "gpt-4") { completionRatio = 2 } - if isStream && apiType != APITypeBaidu && apiType != APITypeZhipu { + if isStream && apiType != APITypeBaidu && apiType != APITypeZhipu && apiType != APITypeAli { completionTokens = countTokenText(streamResponseText, textRequest.Model) } else { promptTokens = textResponse.Usage.PromptTokens @@ -415,6 +433,26 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { } return nil } + case APITypeAli: + if isStream { + err, usage := aliStreamHandler(c, resp) + if err != nil { + return err + } + if usage != nil { + textResponse.Usage = *usage + } + return nil + } else { + err, usage := aliHandler(c, resp) + if err != nil { + return err + } + if usage != nil { + textResponse.Usage = *usage + } + return nil + } default: return errorWrapper(errors.New("unknown api type"), "unknown_api_type", http.StatusInternalServerError) } diff --git a/web/src/constants/channel.constants.js b/web/src/constants/channel.constants.js index 54d7716f..16df9894 100644 --- a/web/src/constants/channel.constants.js +++ b/web/src/constants/channel.constants.js @@ -4,6 +4,7 @@ export const CHANNEL_OPTIONS = [ { key: 3, text: 'Azure OpenAI', value: 3, color: 'olive' }, { key: 11, text: 'Google PaLM2', value: 11, color: 'orange' }, { key: 15, text: '百度文心千帆', value: 15, color: 'blue' }, + { key: 17, text: '阿里通义千问', value: 17, color: 'orange' }, { key: 16, text: '智谱 ChatGLM', value: 16, color: 'violet' }, { key: 8, text: '自定义渠道', value: 8, color: 'pink' }, { key: 2, text: '代理:API2D', value: 2, color: 'blue' }, @@ -14,5 +15,5 @@ export const CHANNEL_OPTIONS = [ { key: 6, text: '代理:OpenAI Max', value: 6, color: 'violet' }, { key: 9, text: '代理:AI.LS', value: 9, color: 'yellow' }, { key: 12, text: '代理:API2GPT', value: 12, color: 'blue' }, - { key: 13, text: '代理:AIGC2D', value: 13, color: 'purple' } + { key: 13, text: '代理:AIGC2D', value: 13, color: 'purple' }, ]; \ No newline at end of file From d1335ebc01b6080bcef2d2fbd5bfaacc38dff5c3 Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 28 Jul 2023 23:47:36 +0800 Subject: [PATCH 05/73] docs: update README --- README.en.md | 16 +++++++--------- README.md | 1 - 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/README.en.md b/README.en.md index f635a798..1c5968bc 100644 --- a/README.en.md +++ b/README.en.md @@ -57,15 +57,13 @@ _✨ Access all LLM through the standard OpenAI API format, easy to deploy & use > **Note**: The latest image pulled from Docker may be an `alpha` release. Specify the version manually if you require stability. ## Features -1. Supports multiple API access channels: - + [x] Official OpenAI channel (support proxy configuration) - + [x] **Azure OpenAI API** - + [x] [API Distribute](https://api.gptjk.top/register?aff=QGxj) - + [x] [OpenAI-SB](https://openai-sb.com) - + [x] [API2D](https://api2d.com/r/197971) - + [x] [OhMyGPT](https://aigptx.top?aff=uFpUl2Kf) - + [x] [AI Proxy](https://aiproxy.io/?i=OneAPI) (invitation code: `OneAPI`) - + [x] Custom channel: Various third-party proxy services not included in the list +1. Support for multiple large models: + + [x] [OpenAI ChatGPT Series Models](https://platform.openai.com/docs/guides/gpt/chat-completions-api) (Supports [Azure OpenAI API](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference)) + + [x] [Anthropic Claude Series Models](https://anthropic.com) + + [x] [Google PaLM2 Series Models](https://developers.generativeai.google) + + [x] [Baidu Wenxin Yiyuan Series Models](https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html) + + [x] [Alibaba Tongyi Qianwen Series Models](https://help.aliyun.com/document_detail/2400395.html) + + [x] [Zhipu ChatGLM Series Models](https://bigmodel.cn) 2. Supports access to multiple channels through **load balancing**. 3. Supports **stream mode** that enables typewriter-like effect through stream transmission. 4. Supports **multi-machine deployment**. [See here](#multi-machine-deployment) for more details. diff --git a/README.md b/README.md index e5579c3d..070f90bf 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,6 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 + [x] [阿里通义千问系列模型](https://help.aliyun.com/document_detail/2400395.html) + [x] [智谱 ChatGLM 系列模型](https://bigmodel.cn) 2. 支持配置镜像以及众多第三方代理服务: - + [x] [API Distribute](https://api.gptjk.top/register?aff=QGxj) + [x] [OpenAI-SB](https://openai-sb.com) + [x] [API2D](https://api2d.com/r/197971) + [x] [OhMyGPT](https://aigptx.top?aff=uFpUl2Kf) From 130e6bfd83eef5290410fbbd8df823e07c65c777 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 29 Jul 2023 12:15:07 +0800 Subject: [PATCH 06/73] feat: support baidu's embedding model (close #324) --- common/model-ratio.go | 1 + controller/model.go | 9 +++++ controller/relay-baidu.go | 85 +++++++++++++++++++++++++++++++++++++++ controller/relay-text.go | 25 ++++++++++-- controller/relay.go | 13 ++++++ 5 files changed, 129 insertions(+), 4 deletions(-) diff --git a/common/model-ratio.go b/common/model-ratio.go index ba6d7245..123451f7 100644 --- a/common/model-ratio.go +++ b/common/model-ratio.go @@ -42,6 +42,7 @@ var ModelRatio = map[string]float64{ "claude-2": 30, "ERNIE-Bot": 0.8572, // ¥0.012 / 1k tokens "ERNIE-Bot-turbo": 0.5715, // ¥0.008 / 1k tokens + "Embedding-V1": 0.1429, // ¥0.002 / 1k tokens "PaLM-2": 1, "chatglm_pro": 0.7143, // ¥0.01 / 1k tokens "chatglm_std": 0.3572, // ¥0.005 / 1k tokens diff --git a/controller/model.go b/controller/model.go index f8096f75..123b0a2f 100644 --- a/controller/model.go +++ b/controller/model.go @@ -288,6 +288,15 @@ func init() { Root: "ERNIE-Bot-turbo", Parent: nil, }, + { + Id: "Embedding-V1", + Object: "model", + Created: 1677649963, + OwnedBy: "baidu", + Permission: permission, + Root: "Embedding-V1", + Parent: nil, + }, { Id: "PaLM-2", Object: "model", diff --git a/controller/relay-baidu.go b/controller/relay-baidu.go index 4267757d..7960e8ee 100644 --- a/controller/relay-baidu.go +++ b/controller/relay-baidu.go @@ -54,6 +54,25 @@ type BaiduChatStreamResponse struct { IsEnd bool `json:"is_end"` } +type BaiduEmbeddingRequest struct { + Input []string `json:"input"` +} + +type BaiduEmbeddingData struct { + Object string `json:"object"` + Embedding []float64 `json:"embedding"` + Index int `json:"index"` +} + +type BaiduEmbeddingResponse struct { + Id string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + Data []BaiduEmbeddingData `json:"data"` + Usage Usage `json:"usage"` + BaiduError +} + func requestOpenAI2Baidu(request GeneralOpenAIRequest) *BaiduChatRequest { messages := make([]BaiduMessage, 0, len(request.Messages)) for _, message := range request.Messages { @@ -112,6 +131,36 @@ func streamResponseBaidu2OpenAI(baiduResponse *BaiduChatStreamResponse) *ChatCom return &response } +func embeddingRequestOpenAI2Baidu(request GeneralOpenAIRequest) *BaiduEmbeddingRequest { + baiduEmbeddingRequest := BaiduEmbeddingRequest{ + Input: nil, + } + switch request.Input.(type) { + case string: + baiduEmbeddingRequest.Input = []string{request.Input.(string)} + case []string: + baiduEmbeddingRequest.Input = request.Input.([]string) + } + return &baiduEmbeddingRequest +} + +func embeddingResponseBaidu2OpenAI(response *BaiduEmbeddingResponse) *OpenAIEmbeddingResponse { + openAIEmbeddingResponse := OpenAIEmbeddingResponse{ + Object: "list", + Data: make([]OpenAIEmbeddingResponseItem, 0, len(response.Data)), + Model: "baidu-embedding", + Usage: response.Usage, + } + for _, item := range response.Data { + openAIEmbeddingResponse.Data = append(openAIEmbeddingResponse.Data, OpenAIEmbeddingResponseItem{ + Object: item.Object, + Index: item.Index, + Embedding: item.Embedding, + }) + } + return &openAIEmbeddingResponse +} + func baiduStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) { var usage Usage scanner := bufio.NewScanner(resp.Body) @@ -212,3 +261,39 @@ func baiduHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCo _, err = c.Writer.Write(jsonResponse) return nil, &fullTextResponse.Usage } + +func baiduEmbeddingHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) { + var baiduResponse BaiduEmbeddingResponse + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil + } + err = resp.Body.Close() + if err != nil { + return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil + } + err = json.Unmarshal(responseBody, &baiduResponse) + if err != nil { + return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil + } + if baiduResponse.ErrorMsg != "" { + return &OpenAIErrorWithStatusCode{ + OpenAIError: OpenAIError{ + Message: baiduResponse.ErrorMsg, + Type: "baidu_error", + Param: "", + Code: baiduResponse.ErrorCode, + }, + StatusCode: resp.StatusCode, + }, nil + } + fullTextResponse := embeddingResponseBaidu2OpenAI(&baiduResponse) + jsonResponse, err := json.Marshal(fullTextResponse) + if err != nil { + return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil + } + c.Writer.Header().Set("Content-Type", "application/json") + c.Writer.WriteHeader(resp.StatusCode) + _, err = c.Writer.Write(jsonResponse) + return nil, &fullTextResponse.Usage +} diff --git a/controller/relay-text.go b/controller/relay-text.go index e58c810b..7d3fe1de 100644 --- a/controller/relay-text.go +++ b/controller/relay-text.go @@ -139,6 +139,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { fullRequestURL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/eb-instant" case "BLOOMZ-7B": fullRequestURL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/bloomz_7b1" + case "Embedding-V1": + fullRequestURL = "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/embeddings/embedding-v1" } apiKey := c.Request.Header.Get("Authorization") apiKey = strings.TrimPrefix(apiKey, "Bearer ") @@ -212,12 +214,20 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { } requestBody = bytes.NewBuffer(jsonStr) case APITypeBaidu: - baiduRequest := requestOpenAI2Baidu(textRequest) - jsonStr, err := json.Marshal(baiduRequest) + var jsonData []byte + var err error + switch relayMode { + case RelayModeEmbeddings: + baiduEmbeddingRequest := embeddingRequestOpenAI2Baidu(textRequest) + jsonData, err = json.Marshal(baiduEmbeddingRequest) + default: + baiduRequest := requestOpenAI2Baidu(textRequest) + jsonData, err = json.Marshal(baiduRequest) + } if err != nil { return errorWrapper(err, "marshal_text_request_failed", http.StatusInternalServerError) } - requestBody = bytes.NewBuffer(jsonStr) + requestBody = bytes.NewBuffer(jsonData) case APITypePaLM: palmRequest := requestOpenAI2PaLM(textRequest) jsonStr, err := json.Marshal(palmRequest) @@ -386,7 +396,14 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { } return nil } else { - err, usage := baiduHandler(c, resp) + var err *OpenAIErrorWithStatusCode + var usage *Usage + switch relayMode { + case RelayModeEmbeddings: + err, usage = baiduEmbeddingHandler(c, resp) + default: + err, usage = baiduHandler(c, resp) + } if err != nil { return err } diff --git a/controller/relay.go b/controller/relay.go index 9cfa5c4f..609ae2eb 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -99,6 +99,19 @@ type OpenAITextResponse struct { Usage `json:"usage"` } +type OpenAIEmbeddingResponseItem struct { + Object string `json:"object"` + Index int `json:"index"` + Embedding []float64 `json:"embedding"` +} + +type OpenAIEmbeddingResponse struct { + Object string `json:"object"` + Data []OpenAIEmbeddingResponseItem `json:"data"` + Model string `json:"model"` + Usage `json:"usage"` +} + type ImageResponse struct { Created int `json:"created"` Data []struct { From f31d400b6fee13880a353ed3b1b1e8aa8b6cc124 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 29 Jul 2023 12:24:23 +0800 Subject: [PATCH 07/73] chore: automatically add related models when switch type --- web/src/pages/Channel/EditChannel.js | 33 +++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index 7833c7f3..4d810014 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -35,6 +35,27 @@ const EditChannel = () => { const [customModel, setCustomModel] = useState(''); const handleInputChange = (e, { name, value }) => { setInputs((inputs) => ({ ...inputs, [name]: value })); + if (name === 'type' && inputs.models.length === 0) { + let localModels = []; + switch (value) { + case 14: + localModels = ['claude-instant-1', 'claude-2']; + break; + case 11: + localModels = ['PaLM-2']; + break; + case 15: + localModels = ['ERNIE-Bot', 'ERNIE-Bot-turbo', 'Embedding-V1']; + break; + case 17: + localModels = ['qwen-v1', 'qwen-plus-v1']; + break; + case 16: + localModels = ['chatglm_pro', 'chatglm_std', 'chatglm_lite']; + break; + } + setInputs((inputs) => ({ ...inputs, models: localModels })); + } }; const loadChannel = async () => { @@ -270,8 +291,8 @@ const EditChannel = () => { }}>清除所有模型 { - if (customModel.trim() === "") return; + + From 50dec03ff39f9a51fea4737484968172af7c23e3 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 29 Jul 2023 19:16:42 +0800 Subject: [PATCH 08/73] fix: fix model mapping cannot be deleted --- web/src/pages/Channel/EditChannel.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index 4d810014..bb0567de 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -153,7 +153,10 @@ const EditChannel = () => { localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1); } if (localInputs.type === 3 && localInputs.other === '') { - localInputs.other = '2023-03-15-preview'; + localInputs.other = '2023-06-01-preview'; + } + if (localInputs.model_mapping === '') { + localInputs.model_mapping = '{}'; } let res; localInputs.models = localInputs.models.join(','); @@ -213,7 +216,7 @@ const EditChannel = () => { { Date: Sat, 29 Jul 2023 19:17:26 +0800 Subject: [PATCH 09/73] fix: fix model mapping cannot be deleted --- controller/relay-text.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/relay-text.go b/controller/relay-text.go index 7d3fe1de..79dca606 100644 --- a/controller/relay-text.go +++ b/controller/relay-text.go @@ -74,7 +74,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { // map model name modelMapping := c.GetString("model_mapping") isModelMapped := false - if modelMapping != "" { + if modelMapping != "" && modelMapping != "{}" { modelMap := make(map[string]string) err := json.Unmarshal([]byte(modelMapping), &modelMap) if err != nil { From b8cb86c2c16946bdffd8a07913af0060fa854b08 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 29 Jul 2023 19:32:06 +0800 Subject: [PATCH 10/73] chore: adjust ui --- web/src/components/ChannelsTable.js | 29 +++++++++++++++------------- web/src/components/UsersTable.js | 2 +- web/src/constants/toast.constants.js | 2 +- web/src/pages/Token/EditToken.js | 2 +- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/web/src/components/ChannelsTable.js b/web/src/components/ChannelsTable.js index 4ea6965d..0459619a 100644 --- a/web/src/components/ChannelsTable.js +++ b/web/src/components/ChannelsTable.js @@ -363,9 +363,12 @@ const ChannelsTable = () => { { + updateChannelBalance(channel.id, channel.name, idx); + }} style={{ cursor: 'pointer' }}> + {renderBalance(channel.type, channel.balance)} + } + content="点击更新" basic /> @@ -380,16 +383,16 @@ const ChannelsTable = () => { > 测试 - + {/* {*/} + {/* updateChannelBalance(channel.id, channel.name, idx);*/} + {/* }}*/} + {/*>*/} + {/* 更新余额*/} + {/**/} diff --git a/web/src/components/UsersTable.js b/web/src/components/UsersTable.js index 08ba961a..f8fb0a75 100644 --- a/web/src/components/UsersTable.js +++ b/web/src/components/UsersTable.js @@ -227,7 +227,7 @@ const UsersTable = () => { content={user.email ? user.email : '未绑定邮箱地址'} key={user.username} header={user.display_name ? user.display_name : user.username} - trigger={{renderText(user.username, 10)}} + trigger={{renderText(user.username, 15)}} hoverable /> diff --git a/web/src/constants/toast.constants.js b/web/src/constants/toast.constants.js index 8b212350..50684722 100644 --- a/web/src/constants/toast.constants.js +++ b/web/src/constants/toast.constants.js @@ -1,5 +1,5 @@ export const toastConstants = { - SUCCESS_TIMEOUT: 500, + SUCCESS_TIMEOUT: 1500, INFO_TIMEOUT: 3000, ERROR_TIMEOUT: 5000, WARNING_TIMEOUT: 10000, diff --git a/web/src/pages/Token/EditToken.js b/web/src/pages/Token/EditToken.js index a4b6044f..1f85520b 100644 --- a/web/src/pages/Token/EditToken.js +++ b/web/src/pages/Token/EditToken.js @@ -83,7 +83,7 @@ const EditToken = () => { if (isEdit) { showSuccess('令牌更新成功!'); } else { - showSuccess('令牌创建成功!'); + showSuccess('令牌创建成功,请在列表页面点击复制获取令牌!'); setInputs(originInputs); } } else { From 3e81d8af45077751e93d127edef6dcc592b7fc5e Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 29 Jul 2023 19:50:29 +0800 Subject: [PATCH 11/73] chore: update i18n --- i18n/en.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/i18n/en.json b/i18n/en.json index 3ef1b010..3c430a7e 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -503,5 +503,12 @@ "请输入 AZURE_OPENAI_ENDPOINT": "Please enter AZURE_OPENAI_ENDPOINT", "请输入自定义渠道的 Base URL": "Please enter the Base URL of the custom channel", "Homepage URL 填": "Fill in the Homepage URL", - "Authorization callback URL 填": "Fill in the Authorization callback URL" + "Authorization callback URL 填": "Fill in the Authorization callback URL", + "请为通道命名": "Please name the channel", + "此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:": "This is optional, used to modify the model name in the request body, it's a JSON string, the key is the model name in the request, and the value is the model name to be replaced, for example:", + "模型重定向": "Model redirection", + "请输入渠道对应的鉴权密钥": "Please enter the authentication key corresponding to the channel", + "注意,": "Note that, ", + ",图片演示。": "related image demo.", + "令牌创建成功,请在列表页面点击复制获取令牌!": "Token created successfully, please click copy on the list page to get the token!" } From 8a866078b2d74af49f42a12565356e86c879aa8e Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 29 Jul 2023 21:55:57 +0800 Subject: [PATCH 12/73] feat: support xunfei's llm (close #206) --- README.md | 1 + common/constants.go | 2 + common/model-ratio.go | 1 + controller/model.go | 9 + controller/relay-text.go | 113 ++++++---- controller/relay-xunfei.go | 274 +++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 + web/src/constants/channel.constants.js | 3 +- 9 files changed, 363 insertions(+), 43 deletions(-) create mode 100644 controller/relay-xunfei.go diff --git a/README.md b/README.md index 070f90bf..e01ea7d9 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 + [x] [Google PaLM2 系列模型](https://developers.generativeai.google) + [x] [百度文心一言系列模型](https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html) + [x] [阿里通义千问系列模型](https://help.aliyun.com/document_detail/2400395.html) + + [x] [讯飞星火认知大模型](https://www.xfyun.cn/doc/spark/Web.html) + [x] [智谱 ChatGLM 系列模型](https://bigmodel.cn) 2. 支持配置镜像以及众多第三方代理服务: + [x] [OpenAI-SB](https://openai-sb.com) diff --git a/common/constants.go b/common/constants.go index 5dbfa71c..c4bb6671 100644 --- a/common/constants.go +++ b/common/constants.go @@ -157,6 +157,7 @@ const ( ChannelTypeBaidu = 15 ChannelTypeZhipu = 16 ChannelTypeAli = 17 + ChannelTypeXunfei = 18 ) var ChannelBaseURLs = []string{ @@ -178,4 +179,5 @@ var ChannelBaseURLs = []string{ "https://aip.baidubce.com", // 15 "https://open.bigmodel.cn", // 16 "https://dashscope.aliyuncs.com", // 17 + "", // 18 } diff --git a/common/model-ratio.go b/common/model-ratio.go index 123451f7..5865b4dc 100644 --- a/common/model-ratio.go +++ b/common/model-ratio.go @@ -49,6 +49,7 @@ var ModelRatio = map[string]float64{ "chatglm_lite": 0.1429, // ¥0.002 / 1k tokens "qwen-v1": 0.8572, // TBD: https://help.aliyun.com/document_detail/2399482.html?spm=a2c4g.2399482.0.0.1ad347feilAgag "qwen-plus-v1": 0.5715, // Same as above + "SparkDesk": 0.8572, // TBD } func ModelRatio2JSONString() string { diff --git a/controller/model.go b/controller/model.go index 123b0a2f..c68aa50c 100644 --- a/controller/model.go +++ b/controller/model.go @@ -351,6 +351,15 @@ func init() { Root: "qwen-plus-v1", Parent: nil, }, + { + Id: "SparkDesk", + Object: "model", + Created: 1677649963, + OwnedBy: "xunfei", + Permission: permission, + Root: "SparkDesk", + Parent: nil, + }, } openAIModelsMap = make(map[string]OpenAIModels) for _, model := range openAIModels { diff --git a/controller/relay-text.go b/controller/relay-text.go index 79dca606..48e7176a 100644 --- a/controller/relay-text.go +++ b/controller/relay-text.go @@ -21,6 +21,7 @@ const ( APITypeBaidu APITypeZhipu APITypeAli + APITypeXunfei ) var httpClient *http.Client @@ -97,7 +98,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { apiType = APITypeZhipu case common.ChannelTypeAli: apiType = APITypeAli - + case common.ChannelTypeXunfei: + apiType = APITypeXunfei } baseURL := common.ChannelBaseURLs[channelType] requestURL := c.Request.URL.String() @@ -250,52 +252,60 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { } requestBody = bytes.NewBuffer(jsonStr) } - req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody) - if err != nil { - return errorWrapper(err, "new_request_failed", http.StatusInternalServerError) - } - apiKey := c.Request.Header.Get("Authorization") - apiKey = strings.TrimPrefix(apiKey, "Bearer ") - switch apiType { - case APITypeOpenAI: - if channelType == common.ChannelTypeAzure { - req.Header.Set("api-key", apiKey) - } else { - req.Header.Set("Authorization", c.Request.Header.Get("Authorization")) + + var req *http.Request + var resp *http.Response + isStream := textRequest.Stream + + if apiType != APITypeXunfei { // cause xunfei use websocket + req, err = http.NewRequest(c.Request.Method, fullRequestURL, requestBody) + if err != nil { + return errorWrapper(err, "new_request_failed", http.StatusInternalServerError) } - case APITypeClaude: - req.Header.Set("x-api-key", apiKey) - anthropicVersion := c.Request.Header.Get("anthropic-version") - if anthropicVersion == "" { - anthropicVersion = "2023-06-01" + apiKey := c.Request.Header.Get("Authorization") + apiKey = strings.TrimPrefix(apiKey, "Bearer ") + switch apiType { + case APITypeOpenAI: + if channelType == common.ChannelTypeAzure { + req.Header.Set("api-key", apiKey) + } else { + req.Header.Set("Authorization", c.Request.Header.Get("Authorization")) + } + case APITypeClaude: + req.Header.Set("x-api-key", apiKey) + anthropicVersion := c.Request.Header.Get("anthropic-version") + if anthropicVersion == "" { + anthropicVersion = "2023-06-01" + } + req.Header.Set("anthropic-version", anthropicVersion) + case APITypeZhipu: + token := getZhipuToken(apiKey) + req.Header.Set("Authorization", token) + case APITypeAli: + req.Header.Set("Authorization", "Bearer "+apiKey) + if textRequest.Stream { + req.Header.Set("X-DashScope-SSE", "enable") + } } - req.Header.Set("anthropic-version", anthropicVersion) - case APITypeZhipu: - token := getZhipuToken(apiKey) - req.Header.Set("Authorization", token) - case APITypeAli: - req.Header.Set("Authorization", "Bearer "+apiKey) - if textRequest.Stream { - req.Header.Set("X-DashScope-SSE", "enable") + req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type")) + req.Header.Set("Accept", c.Request.Header.Get("Accept")) + //req.Header.Set("Connection", c.Request.Header.Get("Connection")) + resp, err = httpClient.Do(req) + if err != nil { + return errorWrapper(err, "do_request_failed", http.StatusInternalServerError) } + err = req.Body.Close() + if err != nil { + 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) + } + isStream = strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream") } - req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type")) - req.Header.Set("Accept", c.Request.Header.Get("Accept")) - //req.Header.Set("Connection", c.Request.Header.Get("Connection")) - resp, err := httpClient.Do(req) - if err != nil { - return errorWrapper(err, "do_request_failed", http.StatusInternalServerError) - } - err = req.Body.Close() - if err != nil { - 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 TextResponse - isStream := strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream") var streamResponseText string defer func() { @@ -470,6 +480,25 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { } return nil } + case APITypeXunfei: + if isStream { + auth := c.Request.Header.Get("Authorization") + auth = strings.TrimPrefix(auth, "Bearer ") + splits := strings.Split(auth, "|") + if len(splits) != 3 { + return errorWrapper(errors.New("invalid auth"), "invalid_auth", http.StatusBadRequest) + } + err, usage := xunfeiStreamHandler(c, textRequest, splits[0], splits[1], splits[2]) + if err != nil { + return err + } + if usage != nil { + textResponse.Usage = *usage + } + return nil + } else { + return errorWrapper(errors.New("xunfei api does not support non-stream mode"), "invalid_api_type", http.StatusBadRequest) + } default: return errorWrapper(errors.New("unknown api type"), "unknown_api_type", http.StatusInternalServerError) } diff --git a/controller/relay-xunfei.go b/controller/relay-xunfei.go new file mode 100644 index 00000000..9343f216 --- /dev/null +++ b/controller/relay-xunfei.go @@ -0,0 +1,274 @@ +package controller + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" + "io" + "net/http" + "net/url" + "one-api/common" + "strings" + "time" +) + +// https://console.xfyun.cn/services/cbm +// https://www.xfyun.cn/doc/spark/Web.html + +type XunfeiMessage struct { + Role string `json:"role"` + Content string `json:"content"` +} + +type XunfeiChatRequest struct { + Header struct { + AppId string `json:"app_id"` + } `json:"header"` + Parameter struct { + Chat struct { + Domain string `json:"domain,omitempty"` + Temperature float64 `json:"temperature,omitempty"` + TopK int `json:"top_k,omitempty"` + MaxTokens int `json:"max_tokens,omitempty"` + Auditing bool `json:"auditing,omitempty"` + } `json:"chat"` + } `json:"parameter"` + Payload struct { + Message struct { + Text []XunfeiMessage `json:"text"` + } `json:"message"` + } `json:"payload"` +} + +type XunfeiChatResponseTextItem struct { + Content string `json:"content"` + Role string `json:"role"` + Index int `json:"index"` +} + +type XunfeiChatResponse struct { + Header struct { + Code int `json:"code"` + Message string `json:"message"` + Sid string `json:"sid"` + Status int `json:"status"` + } `json:"header"` + Payload struct { + Choices struct { + Status int `json:"status"` + Seq int `json:"seq"` + Text []XunfeiChatResponseTextItem `json:"text"` + } `json:"choices"` + } `json:"payload"` + Usage struct { + //Text struct { + // QuestionTokens string `json:"question_tokens"` + // PromptTokens string `json:"prompt_tokens"` + // CompletionTokens string `json:"completion_tokens"` + // TotalTokens string `json:"total_tokens"` + //} `json:"text"` + Text Usage `json:"text"` + } `json:"usage"` +} + +func requestOpenAI2Xunfei(request GeneralOpenAIRequest, xunfeiAppId string) *XunfeiChatRequest { + messages := make([]XunfeiMessage, 0, len(request.Messages)) + for _, message := range request.Messages { + if message.Role == "system" { + messages = append(messages, XunfeiMessage{ + Role: "user", + Content: message.Content, + }) + messages = append(messages, XunfeiMessage{ + Role: "assistant", + Content: "Okay", + }) + } else { + messages = append(messages, XunfeiMessage{ + Role: message.Role, + Content: message.Content, + }) + } + } + xunfeiRequest := XunfeiChatRequest{} + xunfeiRequest.Header.AppId = xunfeiAppId + xunfeiRequest.Parameter.Chat.Domain = "general" + xunfeiRequest.Parameter.Chat.Temperature = request.Temperature + xunfeiRequest.Parameter.Chat.TopK = request.N + xunfeiRequest.Parameter.Chat.MaxTokens = request.MaxTokens + xunfeiRequest.Payload.Message.Text = messages + return &xunfeiRequest +} + +func responseXunfei2OpenAI(response *XunfeiChatResponse) *OpenAITextResponse { + if len(response.Payload.Choices.Text) == 0 { + response.Payload.Choices.Text = []XunfeiChatResponseTextItem{ + { + Content: "", + }, + } + } + choice := OpenAITextResponseChoice{ + Index: 0, + Message: Message{ + Role: "assistant", + Content: response.Payload.Choices.Text[0].Content, + }, + } + fullTextResponse := OpenAITextResponse{ + Object: "chat.completion", + Created: common.GetTimestamp(), + Choices: []OpenAITextResponseChoice{choice}, + Usage: response.Usage.Text, + } + return &fullTextResponse +} + +func streamResponseXunfei2OpenAI(xunfeiResponse *XunfeiChatResponse) *ChatCompletionsStreamResponse { + if len(xunfeiResponse.Payload.Choices.Text) == 0 { + xunfeiResponse.Payload.Choices.Text = []XunfeiChatResponseTextItem{ + { + Content: "", + }, + } + } + var choice ChatCompletionsStreamResponseChoice + choice.Delta.Content = xunfeiResponse.Payload.Choices.Text[0].Content + response := ChatCompletionsStreamResponse{ + Object: "chat.completion.chunk", + Created: common.GetTimestamp(), + Model: "SparkDesk", + Choices: []ChatCompletionsStreamResponseChoice{choice}, + } + return &response +} + +func buildXunfeiAuthUrl(hostUrl string, apiKey, apiSecret string) string { + HmacWithShaToBase64 := func(algorithm, data, key string) string { + mac := hmac.New(sha256.New, []byte(key)) + mac.Write([]byte(data)) + encodeData := mac.Sum(nil) + return base64.StdEncoding.EncodeToString(encodeData) + } + ul, err := url.Parse(hostUrl) + if err != nil { + fmt.Println(err) + } + date := time.Now().UTC().Format(time.RFC1123) + signString := []string{"host: " + ul.Host, "date: " + date, "GET " + ul.Path + " HTTP/1.1"} + sign := strings.Join(signString, "\n") + sha := HmacWithShaToBase64("hmac-sha256", sign, apiSecret) + authUrl := fmt.Sprintf("hmac username=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", apiKey, + "hmac-sha256", "host date request-line", sha) + authorization := base64.StdEncoding.EncodeToString([]byte(authUrl)) + v := url.Values{} + v.Add("host", ul.Host) + v.Add("date", date) + v.Add("authorization", authorization) + callUrl := hostUrl + "?" + v.Encode() + return callUrl +} + +func xunfeiStreamHandler(c *gin.Context, textRequest GeneralOpenAIRequest, appId string, apiKey string, apiSecret string) (*OpenAIErrorWithStatusCode, *Usage) { + var usage Usage + d := websocket.Dialer{ + HandshakeTimeout: 5 * time.Second, + } + hostUrl := "wss://aichat.xf-yun.com/v1/chat" + conn, resp, err := d.Dial(buildXunfeiAuthUrl(hostUrl, apiKey, apiSecret), nil) + if err != nil || resp.StatusCode != 101 { + return errorWrapper(err, "dial_failed", http.StatusInternalServerError), nil + } + data := requestOpenAI2Xunfei(textRequest, appId) + err = conn.WriteJSON(data) + if err != nil { + return errorWrapper(err, "write_json_failed", http.StatusInternalServerError), nil + } + dataChan := make(chan XunfeiChatResponse) + stopChan := make(chan bool) + go func() { + for { + _, msg, err := conn.ReadMessage() + if err != nil { + common.SysError("error reading stream response: " + err.Error()) + break + } + var response XunfeiChatResponse + err = json.Unmarshal(msg, &response) + if err != nil { + common.SysError("error unmarshalling stream response: " + err.Error()) + break + } + dataChan <- response + if response.Payload.Choices.Status == 2 { + break + } + } + stopChan <- true + }() + c.Writer.Header().Set("Content-Type", "text/event-stream") + c.Writer.Header().Set("Cache-Control", "no-cache") + c.Writer.Header().Set("Connection", "keep-alive") + c.Writer.Header().Set("Transfer-Encoding", "chunked") + c.Writer.Header().Set("X-Accel-Buffering", "no") + c.Stream(func(w io.Writer) bool { + select { + case xunfeiResponse := <-dataChan: + usage.PromptTokens += xunfeiResponse.Usage.Text.PromptTokens + usage.CompletionTokens += xunfeiResponse.Usage.Text.CompletionTokens + usage.TotalTokens += xunfeiResponse.Usage.Text.TotalTokens + response := streamResponseXunfei2OpenAI(&xunfeiResponse) + jsonResponse, err := json.Marshal(response) + if err != nil { + common.SysError("error marshalling stream response: " + err.Error()) + return true + } + c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)}) + return true + case <-stopChan: + c.Render(-1, common.CustomEvent{Data: "data: [DONE]"}) + return false + } + }) + return nil, &usage +} + +func xunfeiHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStatusCode, *Usage) { + var xunfeiResponse XunfeiChatResponse + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return errorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil + } + err = resp.Body.Close() + if err != nil { + return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil + } + err = json.Unmarshal(responseBody, &xunfeiResponse) + if err != nil { + return errorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil + } + if xunfeiResponse.Header.Code != 0 { + return &OpenAIErrorWithStatusCode{ + OpenAIError: OpenAIError{ + Message: xunfeiResponse.Header.Message, + Type: "xunfei_error", + Param: "", + Code: xunfeiResponse.Header.Code, + }, + StatusCode: resp.StatusCode, + }, nil + } + fullTextResponse := responseXunfei2OpenAI(&xunfeiResponse) + jsonResponse, err := json.Marshal(fullTextResponse) + if err != nil { + return errorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil + } + c.Writer.Header().Set("Content-Type", "application/json") + c.Writer.WriteHeader(resp.StatusCode) + _, err = c.Writer.Write(jsonResponse) + return nil, &fullTextResponse.Usage +} diff --git a/go.mod b/go.mod index 2e0cf017..1d061520 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/go-redis/redis/v8 v8.11.5 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/uuid v1.3.0 + github.com/gorilla/websocket v1.5.0 github.com/pkoukk/tiktoken-go v0.1.1 golang.org/x/crypto v0.9.0 gorm.io/driver/mysql v1.4.3 diff --git a/go.sum b/go.sum index 7287206a..c6e4423c 100644 --- a/go.sum +++ b/go.sum @@ -67,6 +67,8 @@ github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyC github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= diff --git a/web/src/constants/channel.constants.js b/web/src/constants/channel.constants.js index 16df9894..f51c6c44 100644 --- a/web/src/constants/channel.constants.js +++ b/web/src/constants/channel.constants.js @@ -5,6 +5,7 @@ export const CHANNEL_OPTIONS = [ { key: 11, text: 'Google PaLM2', value: 11, color: 'orange' }, { key: 15, text: '百度文心千帆', value: 15, color: 'blue' }, { key: 17, text: '阿里通义千问', value: 17, color: 'orange' }, + { key: 18, text: '讯飞星火认知大模型', value: 18, color: 'blue' }, { key: 16, text: '智谱 ChatGLM', value: 16, color: 'violet' }, { key: 8, text: '自定义渠道', value: 8, color: 'pink' }, { key: 2, text: '代理:API2D', value: 2, color: 'blue' }, @@ -15,5 +16,5 @@ export const CHANNEL_OPTIONS = [ { key: 6, text: '代理:OpenAI Max', value: 6, color: 'violet' }, { key: 9, text: '代理:AI.LS', value: 9, color: 'yellow' }, { key: 12, text: '代理:API2GPT', value: 12, color: 'blue' }, - { key: 13, text: '代理:AIGC2D', value: 13, color: 'purple' }, + { key: 13, text: '代理:AIGC2D', value: 13, color: 'purple' } ]; \ No newline at end of file From ce9c8024a6b18bb9b1950da20d1fc6b5f6c2b3a7 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 29 Jul 2023 22:05:15 +0800 Subject: [PATCH 13/73] chore: update prompt for xunfei --- controller/relay-xunfei.go | 2 +- web/src/pages/Channel/EditChannel.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/controller/relay-xunfei.go b/controller/relay-xunfei.go index 9343f216..cd55df89 100644 --- a/controller/relay-xunfei.go +++ b/controller/relay-xunfei.go @@ -173,7 +173,7 @@ func buildXunfeiAuthUrl(hostUrl string, apiKey, apiSecret string) string { return callUrl } -func xunfeiStreamHandler(c *gin.Context, textRequest GeneralOpenAIRequest, appId string, apiKey string, apiSecret string) (*OpenAIErrorWithStatusCode, *Usage) { +func xunfeiStreamHandler(c *gin.Context, textRequest GeneralOpenAIRequest, appId string, apiSecret string, apiKey string) (*OpenAIErrorWithStatusCode, *Usage) { var usage Usage d := websocket.Dialer{ HandshakeTimeout: 5 * time.Second, diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index bb0567de..6974315e 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -53,6 +53,9 @@ const EditChannel = () => { case 16: localModels = ['chatglm_pro', 'chatglm_std', 'chatglm_lite']; break; + case 18: + localModels = ['SparkDesk']; + break; } setInputs((inputs) => ({ ...inputs, models: localModels })); } @@ -347,7 +350,7 @@ const EditChannel = () => { label='密钥' name='key' required - placeholder={inputs.type === 15 ? '请输入 access token,当前版本暂不支持自动刷新,请每 30 天更新一次' : '请输入渠道对应的鉴权密钥'} + placeholder={inputs.type === 15 ? '请输入 access token,当前版本暂不支持自动刷新,请每 30 天更新一次' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')} onChange={handleInputChange} value={inputs.key} autoComplete='new-password' From b7d0616ae0a9b520700e9337c43b4de64600c9f7 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 29 Jul 2023 22:09:10 +0800 Subject: [PATCH 14/73] chore: update title for xunfei --- web/src/constants/channel.constants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/constants/channel.constants.js b/web/src/constants/channel.constants.js index f51c6c44..a17ef374 100644 --- a/web/src/constants/channel.constants.js +++ b/web/src/constants/channel.constants.js @@ -5,7 +5,7 @@ export const CHANNEL_OPTIONS = [ { key: 11, text: 'Google PaLM2', value: 11, color: 'orange' }, { key: 15, text: '百度文心千帆', value: 15, color: 'blue' }, { key: 17, text: '阿里通义千问', value: 17, color: 'orange' }, - { key: 18, text: '讯飞星火认知大模型', value: 18, color: 'blue' }, + { key: 18, text: '讯飞星火认知', value: 18, color: 'blue' }, { key: 16, text: '智谱 ChatGLM', value: 16, color: 'violet' }, { key: 8, text: '自定义渠道', value: 8, color: 'pink' }, { key: 2, text: '代理:API2D', value: 2, color: 'blue' }, From fe8f216dd9da50050eb28fb469b7c823a9b7dec1 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 29 Jul 2023 22:32:05 +0800 Subject: [PATCH 15/73] refactor: update billing related code --- controller/relay-text.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/controller/relay-text.go b/controller/relay-text.go index 48e7176a..ceac4103 100644 --- a/controller/relay-text.go +++ b/controller/relay-text.go @@ -306,7 +306,6 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { } var textResponse TextResponse - var streamResponseText string defer func() { if consumeQuota { @@ -318,16 +317,10 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { if strings.HasPrefix(textRequest.Model, "gpt-4") { completionRatio = 2 } - if isStream && apiType != APITypeBaidu && apiType != APITypeZhipu && apiType != APITypeAli { - completionTokens = countTokenText(streamResponseText, textRequest.Model) - } else { - promptTokens = textResponse.Usage.PromptTokens - completionTokens = textResponse.Usage.CompletionTokens - if apiType == APITypeZhipu { - // zhipu's API does not return prompt tokens & completion tokens - promptTokens = textResponse.Usage.TotalTokens - } - } + + promptTokens = textResponse.Usage.PromptTokens + completionTokens = textResponse.Usage.CompletionTokens + quota = promptTokens + int(float64(completionTokens)*completionRatio) quota = int(float64(quota) * ratio) if ratio != 0 && quota <= 0 { @@ -365,7 +358,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { if err != nil { return err } - streamResponseText = responseText + textResponse.Usage.PromptTokens = promptTokens + textResponse.Usage.CompletionTokens = countTokenText(responseText, textRequest.Model) return nil } else { err, usage := openaiHandler(c, resp, consumeQuota) @@ -383,7 +377,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { if err != nil { return err } - streamResponseText = responseText + textResponse.Usage.PromptTokens = promptTokens + textResponse.Usage.CompletionTokens = countTokenText(responseText, textRequest.Model) return nil } else { err, usage := claudeHandler(c, resp, promptTokens, textRequest.Model) @@ -428,7 +423,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { if err != nil { return err } - streamResponseText = responseText + textResponse.Usage.PromptTokens = promptTokens + textResponse.Usage.CompletionTokens = countTokenText(responseText, textRequest.Model) return nil } else { err, usage := palmHandler(c, resp, promptTokens, textRequest.Model) @@ -449,6 +445,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { if usage != nil { textResponse.Usage = *usage } + // zhipu's API does not return prompt tokens & completion tokens + textResponse.Usage.PromptTokens = textResponse.Usage.TotalTokens return nil } else { err, usage := zhipuHandler(c, resp) @@ -458,6 +456,8 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { if usage != nil { textResponse.Usage = *usage } + // zhipu's API does not return prompt tokens & completion tokens + textResponse.Usage.PromptTokens = textResponse.Usage.TotalTokens return nil } case APITypeAli: From 065147b440b075321cfaecfca356dae5d36e592a Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 29 Jul 2023 23:52:18 +0800 Subject: [PATCH 16/73] fix: close connection when response ended --- controller/relay-xunfei.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/controller/relay-xunfei.go b/controller/relay-xunfei.go index cd55df89..c6d78a84 100644 --- a/controller/relay-xunfei.go +++ b/controller/relay-xunfei.go @@ -205,6 +205,10 @@ func xunfeiStreamHandler(c *gin.Context, textRequest GeneralOpenAIRequest, appId } dataChan <- response if response.Payload.Choices.Status == 2 { + err := conn.Close() + if err != nil { + common.SysError("error closing websocket connection: " + err.Error()) + } break } } From ec88c0c24092b442ed9aef4d2c10c5d16eabb9ea Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 29 Jul 2023 23:54:09 +0800 Subject: [PATCH 17/73] fix: prompt user that channel test is unavailable --- controller/channel-test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/controller/channel-test.go b/controller/channel-test.go index be658fa8..8465d51d 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -23,6 +23,8 @@ func testChannel(channel *model.Channel, request ChatRequest) (error, *OpenAIErr case common.ChannelTypeBaidu: fallthrough case common.ChannelTypeZhipu: + fallthrough + case common.ChannelTypeXunfei: return errors.New("该渠道类型当前版本不支持测试,请手动测试"), nil case common.ChannelTypeAzure: request.Model = "gpt-35-turbo" From 8cbbeb784f03e8626c6d44138ba55073c6911b73 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 30 Jul 2023 12:03:06 +0800 Subject: [PATCH 18/73] fix: ignore data if not have proper prefix --- controller/relay-openai.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/controller/relay-openai.go b/controller/relay-openai.go index 2bcb99dc..b986822c 100644 --- a/controller/relay-openai.go +++ b/controller/relay-openai.go @@ -34,6 +34,9 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*O if len(data) < 6 { // ignore blank line or wrong format continue } + if data[:6] != "data: " && data[:6] != "[DONE]" { + continue + } dataChan <- data data = data[6:] if !strings.HasPrefix(data, "[DONE]") { From 3fca6ff5349d0723f892ed8b5000eb6e336ac006 Mon Sep 17 00:00:00 2001 From: ckt <65409152+ckt1031@users.noreply.github.com> Date: Sun, 30 Jul 2023 12:44:41 +0800 Subject: [PATCH 19/73] feat: support email domain whitelist (#337) * feat: support email domain restriction * fix(SMTPToken): disable password auto complete * chore: update implementation --------- Co-authored-by: JustSong --- README.md | 2 +- common/constants.go | 13 ++++ controller/misc.go | 20 +++++- controller/option.go | 11 ++- model/option.go | 6 ++ web/src/components/SystemSetting.js | 104 ++++++++++++++++++++++++++-- 6 files changed, 147 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e01ea7d9..f32495b2 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 19. 支持通过系统访问令牌访问管理 API。 20. 支持 Cloudflare Turnstile 用户校验。 21. 支持用户管理,支持**多种用户登录注册方式**: - + 邮箱登录注册以及通过邮箱进行密码重置。 + + 邮箱登录注册(支持注册邮箱白名单)以及通过邮箱进行密码重置。 + [GitHub 开放授权](https://github.com/settings/applications/new)。 + 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。 diff --git a/common/constants.go b/common/constants.go index c4bb6671..eaaca803 100644 --- a/common/constants.go +++ b/common/constants.go @@ -42,6 +42,19 @@ var WeChatAuthEnabled = false var TurnstileCheckEnabled = false var RegisterEnabled = true +var EmailDomainRestrictionEnabled = false +var EmailDomainWhitelist = []string{ + "gmail.com", + "163.com", + "126.com", + "qq.com", + "outlook.com", + "hotmail.com", + "icloud.com", + "yahoo.com", + "foxmail.com", +} + var LogConsumeEnabled = true var SMTPServer = "" diff --git a/controller/misc.go b/controller/misc.go index 958a3716..2bcbb41f 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -3,10 +3,12 @@ package controller import ( "encoding/json" "fmt" - "github.com/gin-gonic/gin" "net/http" "one-api/common" "one-api/model" + "strings" + + "github.com/gin-gonic/gin" ) func GetStatus(c *gin.Context) { @@ -78,6 +80,22 @@ func SendEmailVerification(c *gin.Context) { }) return } + if common.EmailDomainRestrictionEnabled { + allowed := false + for _, domain := range common.EmailDomainWhitelist { + if strings.HasSuffix(email, "@"+domain) { + allowed = true + break + } + } + if !allowed { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "管理员启用了邮箱域名白名单,您的邮箱地址的域名不在白名单中", + }) + return + } + } if model.IsEmailAlreadyTaken(email) { c.JSON(http.StatusOK, gin.H{ "success": false, diff --git a/controller/option.go b/controller/option.go index abf0d5be..9cf4ff1b 100644 --- a/controller/option.go +++ b/controller/option.go @@ -2,11 +2,12 @@ package controller import ( "encoding/json" - "github.com/gin-gonic/gin" "net/http" "one-api/common" "one-api/model" "strings" + + "github.com/gin-gonic/gin" ) func GetOptions(c *gin.Context) { @@ -49,6 +50,14 @@ func UpdateOption(c *gin.Context) { }) return } + case "EmailDomainRestrictionEnabled": + if option.Value == "true" && len(common.EmailDomainWhitelist) == 0 { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "无法启用邮箱域名限制,请先填入限制的邮箱域名!", + }) + return + } case "WeChatAuthEnabled": if option.Value == "true" && common.WeChatServerAddress == "" { c.JSON(http.StatusOK, gin.H{ diff --git a/model/option.go b/model/option.go index e7bc6806..4ef4d260 100644 --- a/model/option.go +++ b/model/option.go @@ -39,6 +39,8 @@ func InitOptionMap() { common.OptionMap["DisplayInCurrencyEnabled"] = strconv.FormatBool(common.DisplayInCurrencyEnabled) common.OptionMap["DisplayTokenStatEnabled"] = strconv.FormatBool(common.DisplayTokenStatEnabled) common.OptionMap["ChannelDisableThreshold"] = strconv.FormatFloat(common.ChannelDisableThreshold, 'f', -1, 64) + common.OptionMap["EmailDomainRestrictionEnabled"] = strconv.FormatBool(common.EmailDomainRestrictionEnabled) + common.OptionMap["EmailDomainWhitelist"] = strings.Join(common.EmailDomainWhitelist, ",") common.OptionMap["SMTPServer"] = "" common.OptionMap["SMTPFrom"] = "" common.OptionMap["SMTPPort"] = strconv.Itoa(common.SMTPPort) @@ -141,6 +143,8 @@ func updateOptionMap(key string, value string) (err error) { common.TurnstileCheckEnabled = boolValue case "RegisterEnabled": common.RegisterEnabled = boolValue + case "EmailDomainRestrictionEnabled": + common.EmailDomainRestrictionEnabled = boolValue case "AutomaticDisableChannelEnabled": common.AutomaticDisableChannelEnabled = boolValue case "ApproximateTokenEnabled": @@ -154,6 +158,8 @@ func updateOptionMap(key string, value string) (err error) { } } switch key { + case "EmailDomainWhitelist": + common.EmailDomainWhitelist = strings.Split(value, ",") case "SMTPServer": common.SMTPServer = value case "SMTPPort": diff --git a/web/src/components/SystemSetting.js b/web/src/components/SystemSetting.js index 658e5294..88c82204 100644 --- a/web/src/components/SystemSetting.js +++ b/web/src/components/SystemSetting.js @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; -import { Divider, Form, Grid, Header, Message } from 'semantic-ui-react'; -import { API, removeTrailingSlash, showError, verifyJSON } from '../helpers'; +import { Button, Divider, Form, Grid, Header, Input, Message } from 'semantic-ui-react'; +import { API, removeTrailingSlash, showError } from '../helpers'; const SystemSetting = () => { let [inputs, setInputs] = useState({ @@ -26,9 +26,13 @@ const SystemSetting = () => { TurnstileSiteKey: '', TurnstileSecretKey: '', RegisterEnabled: '', + EmailDomainRestrictionEnabled: '', + EmailDomainWhitelist: '' }); const [originInputs, setOriginInputs] = useState({}); let [loading, setLoading] = useState(false); + const [EmailDomainWhitelist, setEmailDomainWhitelist] = useState([]); + const [restrictedDomainInput, setRestrictedDomainInput] = useState(''); const getOptions = async () => { const res = await API.get('/api/option/'); @@ -38,8 +42,15 @@ const SystemSetting = () => { data.forEach((item) => { newInputs[item.key] = item.value; }); - setInputs(newInputs); + setInputs({ + ...newInputs, + EmailDomainWhitelist: newInputs.EmailDomainWhitelist.split(',') + }); setOriginInputs(newInputs); + + setEmailDomainWhitelist(newInputs.EmailDomainWhitelist.split(',').map((item) => { + return { key: item, text: item, value: item }; + })); } else { showError(message); } @@ -58,6 +69,7 @@ const SystemSetting = () => { case 'GitHubOAuthEnabled': case 'WeChatAuthEnabled': case 'TurnstileCheckEnabled': + case 'EmailDomainRestrictionEnabled': case 'RegisterEnabled': value = inputs[key] === 'true' ? 'false' : 'true'; break; @@ -70,7 +82,12 @@ const SystemSetting = () => { }); const { success, message } = res.data; if (success) { - setInputs((inputs) => ({ ...inputs, [key]: value })); + if (key === 'EmailDomainWhitelist') { + value = value.split(','); + } + setInputs((inputs) => ({ + ...inputs, [key]: value + })); } else { showError(message); } @@ -88,7 +105,8 @@ const SystemSetting = () => { name === 'WeChatServerToken' || name === 'WeChatAccountQRCodeImageURL' || name === 'TurnstileSiteKey' || - name === 'TurnstileSecretKey' + name === 'TurnstileSecretKey' || + name === 'EmailDomainWhitelist' ) { setInputs((inputs) => ({ ...inputs, [name]: value })); } else { @@ -125,6 +143,16 @@ const SystemSetting = () => { } }; + + const submitEmailDomainWhitelist = async () => { + if ( + originInputs['EmailDomainWhitelist'] !== inputs.EmailDomainWhitelist.join(',') && + inputs.SMTPToken !== '' + ) { + await updateOption('EmailDomainWhitelist', inputs.EmailDomainWhitelist.join(',')); + } + }; + const submitWeChat = async () => { if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) { await updateOption( @@ -173,6 +201,22 @@ const SystemSetting = () => { } }; + const submitNewRestrictedDomain = () => { + const localDomainList = inputs.EmailDomainWhitelist; + if (restrictedDomainInput !== '' && !localDomainList.includes(restrictedDomainInput)) { + setRestrictedDomainInput(''); + setInputs({ + ...inputs, + EmailDomainWhitelist: [...localDomainList, restrictedDomainInput], + }); + setEmailDomainWhitelist([...EmailDomainWhitelist, { + key: restrictedDomainInput, + text: restrictedDomainInput, + value: restrictedDomainInput, + }]); + } + } + return ( @@ -239,6 +283,54 @@ const SystemSetting = () => { /> +
+ 配置邮箱域名白名单 + 用以防止恶意用户利用临时邮箱批量注册 +
+ + + + + + { + submitNewRestrictedDomain(); + }}>填入 + } + onKeyDown={(e) => { + if (e.key === 'Enter') { + submitNewRestrictedDomain(); + } + }} + autoComplete='new-password' + placeholder='输入新的允许的邮箱域名' + value={restrictedDomainInput} + onChange={(e, { value }) => { + setRestrictedDomainInput(value); + }} + /> + + 保存邮箱域名白名单设置 +
配置 SMTP 用以支持系统的邮件发送 @@ -284,7 +376,7 @@ const SystemSetting = () => { onChange={handleInputChange} type='password' autoComplete='new-password' - value={inputs.SMTPToken} + checked={inputs.RegisterEnabled === 'true'} placeholder='敏感信息不会发送到前端显示' /> From c9d2e42a9e827d966721533f3acbe0c03d4432dc Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 30 Jul 2023 22:20:42 +0800 Subject: [PATCH 20/73] fix: fix sse not ending properly in some case --- controller/relay-openai.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/controller/relay-openai.go b/controller/relay-openai.go index b986822c..8d7b432b 100644 --- a/controller/relay-openai.go +++ b/controller/relay-openai.go @@ -46,7 +46,7 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*O err := json.Unmarshal([]byte(data), &streamResponse) if err != nil { common.SysError("error unmarshalling stream response: " + err.Error()) - return + continue // just ignore the error } for _, choice := range streamResponse.Choices { responseText += choice.Delta.Content @@ -56,7 +56,7 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*O err := json.Unmarshal([]byte(data), &streamResponse) if err != nil { common.SysError("error unmarshalling stream response: " + err.Error()) - return + continue } for _, choice := range streamResponse.Choices { responseText += choice.Text From 30a7f1a1c7a4a23cb157dedf066a74b81617b242 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 30 Jul 2023 22:24:07 +0800 Subject: [PATCH 21/73] docs: update README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f32495b2..55b2ecec 100644 --- a/README.md +++ b/README.md @@ -314,6 +314,7 @@ https://openai.justsong.cn + 额度 = 分组倍率 * 模型倍率 * (提示 token 数 + 补全 token 数 * 补全倍率) + 其中补全倍率对于 GPT3.5 固定为 1.33,GPT4 为 2,与官方保持一致。 + 如果是非流模式,官方接口会返回消耗的总 token,但是你要注意提示和补全的消耗倍率不一样。 + + 注意,One API 的默认倍率就是官方倍率,是已经调整过的。 2. 账户额度足够为什么提示额度不足? + 请检查你的令牌额度是否足够,这个和账户额度是分开的。 + 令牌额度仅供用户设置最大使用量,用户可自由设置。 From c2c455c9807f0d92f4b4c4ba956fef8cf5ff9145 Mon Sep 17 00:00:00 2001 From: glzjin Date: Tue, 1 Aug 2023 23:51:28 +0800 Subject: [PATCH 22/73] fix: fix zhipu streaming (#349) * Fix #348 * chore: update implementation --------- Co-authored-by: JustSong --- controller/relay-zhipu.go | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/controller/relay-zhipu.go b/controller/relay-zhipu.go index 33d141c7..20a4fa42 100644 --- a/controller/relay-zhipu.go +++ b/controller/relay-zhipu.go @@ -194,8 +194,8 @@ func zhipuStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithSt if atEOF && len(data) == 0 { return 0, nil, nil } - if i := strings.Index(string(data), "\n"); i >= 0 { - return i + 1, data[0:i], nil + if i := strings.Index(string(data), "\n\n"); i >= 0 && strings.Index(string(data), ":") >= 0 { + return i + 2, data[0:i], nil } if atEOF { return len(data), data, nil @@ -208,14 +208,19 @@ func zhipuStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithSt go func() { for scanner.Scan() { data := scanner.Text() - data = strings.Trim(data, "\"") - if len(data) < 5 { // ignore blank line or wrong format - continue - } - if data[:5] == "data:" { - dataChan <- data[5:] - } else if data[:5] == "meta:" { - metaChan <- data[5:] + lines := strings.Split(data, "\n") + for i, line := range lines { + if len(line) < 5 { + continue + } + if line[:5] == "data:" { + dataChan <- line[5:] + if i != len(lines)-1 { + dataChan <- "\n" + } + } else if line[:5] == "meta:" { + metaChan <- line[5:] + } } } stopChan <- true From afcd1bd27b939bef31d70712ff2222c394fdfbf7 Mon Sep 17 00:00:00 2001 From: a497625414 <55976729+a497625414@users.noreply.github.com> Date: Wed, 2 Aug 2023 19:11:21 +0800 Subject: [PATCH 23/73] docs: update deploy-on-sealos (#351) --- README.en.md | 3 ++- README.md | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.en.md b/README.en.md index 1c5968bc..c43ee3af 100644 --- a/README.en.md +++ b/README.en.md @@ -173,7 +173,8 @@ If you encounter a blank page after deployment, refer to [#97](https://github.co Deploy on Sealos
-Please refer to [this tutorial](https://github.com/c121914yu/FastGPT/blob/main/docs/deploy/one-api/sealos.md). +[![](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-fastdeploy?templateName=one-api) +
diff --git a/README.md b/README.md index 55b2ecec..a7c06fc5 100644 --- a/README.md +++ b/README.md @@ -211,9 +211,9 @@ docker run --name chatgpt-web -d -p 3002:3002 -e OPENAI_API_BASE_URL=https://ope 部署到 Sealos
-> Sealos 可视化部署,仅需 1 分钟。 +> Sealos 可视化一键部署。 -参考这个[教程](https://github.com/c121914yu/FastGPT/blob/main/docs/deploy/one-api/sealos.md)中 1~5 步。 +[![](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-fastdeploy?templateName=one-api)
From 89bcaaf98983121645b057465cba3477b65cf7d1 Mon Sep 17 00:00:00 2001 From: a497625414 <55976729+a497625414@users.noreply.github.com> Date: Sun, 6 Aug 2023 13:19:54 +0800 Subject: [PATCH 24/73] docs: update readme (#359) * update-deploy-on-sealos * update-deploy-on-sealos --- README.en.md | 6 +++++- README.md | 8 +++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/README.en.md b/README.en.md index c43ee3af..2ea6e65e 100644 --- a/README.en.md +++ b/README.en.md @@ -137,7 +137,7 @@ The initial account username is `root` and password is `123456`. cd one-api/web npm install npm run build - + # Build the backend cd .. go mod download @@ -173,6 +173,10 @@ If you encounter a blank page after deployment, refer to [#97](https://github.co Deploy on Sealos
+> Sealos supports high concurrency, dynamic scaling, and stable operations for millions of users. + +> Click the button below to deploy with one click.👇 + [![](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-fastdeploy?templateName=one-api) diff --git a/README.md b/README.md index a7c06fc5..fcabd2c5 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ sudo service nginx restart cd one-api/web npm install npm run build - + # 构建后端 cd .. go mod download @@ -211,9 +211,11 @@ docker run --name chatgpt-web -d -p 3002:3002 -e OPENAI_API_BASE_URL=https://ope 部署到 Sealos
-> Sealos 可视化一键部署。 +> Sealos 的服务器在国外,不需要额外处理网络问题,支持高并发 & 动态伸缩。 -[![](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-fastdeploy?templateName=one-api) +点击以下按钮一键部署: + +[![Deploy-on-Sealos.svg](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-fastdeploy?templateName=one-api)
From 2d49ca6a0708bd37909eced9627ebe3111f19dfb Mon Sep 17 00:00:00 2001 From: glzjin Date: Sun, 6 Aug 2023 13:24:49 +0800 Subject: [PATCH 25/73] fix: fix SparkDesk not billed (#344) --- controller/relay-xunfei.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/controller/relay-xunfei.go b/controller/relay-xunfei.go index c6d78a84..1faf3294 100644 --- a/controller/relay-xunfei.go +++ b/controller/relay-xunfei.go @@ -63,16 +63,16 @@ type XunfeiChatResponse struct { Seq int `json:"seq"` Text []XunfeiChatResponseTextItem `json:"text"` } `json:"choices"` + Usage struct { + //Text struct { + // QuestionTokens string `json:"question_tokens"` + // PromptTokens string `json:"prompt_tokens"` + // CompletionTokens string `json:"completion_tokens"` + // TotalTokens string `json:"total_tokens"` + //} `json:"text"` + Text Usage `json:"text"` + } `json:"usage"` } `json:"payload"` - Usage struct { - //Text struct { - // QuestionTokens string `json:"question_tokens"` - // PromptTokens string `json:"prompt_tokens"` - // CompletionTokens string `json:"completion_tokens"` - // TotalTokens string `json:"total_tokens"` - //} `json:"text"` - Text Usage `json:"text"` - } `json:"usage"` } func requestOpenAI2Xunfei(request GeneralOpenAIRequest, xunfeiAppId string) *XunfeiChatRequest { @@ -123,7 +123,7 @@ func responseXunfei2OpenAI(response *XunfeiChatResponse) *OpenAITextResponse { Object: "chat.completion", Created: common.GetTimestamp(), Choices: []OpenAITextResponseChoice{choice}, - Usage: response.Usage.Text, + Usage: response.Payload.Usage.Text, } return &fullTextResponse } @@ -222,9 +222,9 @@ func xunfeiStreamHandler(c *gin.Context, textRequest GeneralOpenAIRequest, appId c.Stream(func(w io.Writer) bool { select { case xunfeiResponse := <-dataChan: - usage.PromptTokens += xunfeiResponse.Usage.Text.PromptTokens - usage.CompletionTokens += xunfeiResponse.Usage.Text.CompletionTokens - usage.TotalTokens += xunfeiResponse.Usage.Text.TotalTokens + usage.PromptTokens += xunfeiResponse.Payload.Usage.Text.PromptTokens + usage.CompletionTokens += xunfeiResponse.Payload.Usage.Text.CompletionTokens + usage.TotalTokens += xunfeiResponse.Payload.Usage.Text.TotalTokens response := streamResponseXunfei2OpenAI(&xunfeiResponse) jsonResponse, err := json.Marshal(response) if err != nil { From 1dfa190e7954a157890ec69c3648b7e9fc0d10c4 Mon Sep 17 00:00:00 2001 From: Miniers Date: Sun, 6 Aug 2023 13:56:59 +0800 Subject: [PATCH 26/73] feat: able to copy scheme of ama, opencat & chatgpt next web (#343) * Token Adds Option to Quickly Copy AMA and OpenCat URL Scheme * feat: add ChatGPT Next Web --------- Co-authored-by: JustSong --- web/src/components/TokensTable.js | 78 ++++++++++++++++++++++++------- 1 file changed, 62 insertions(+), 16 deletions(-) diff --git a/web/src/components/TokensTable.js b/web/src/components/TokensTable.js index b42f7df8..d45858f7 100644 --- a/web/src/components/TokensTable.js +++ b/web/src/components/TokensTable.js @@ -1,11 +1,17 @@ import React, { useEffect, useState } from 'react'; -import { Button, Form, Label, Modal, Pagination, Popup, Table } from 'semantic-ui-react'; +import { Button, Dropdown, Form, Label, Pagination, Popup, Table } from 'semantic-ui-react'; import { Link } from 'react-router-dom'; import { API, copy, showError, showSuccess, showWarning, timestamp2string } from '../helpers'; import { ITEMS_PER_PAGE } from '../constants'; import { renderQuota } from '../helpers/render'; +const COPY_OPTIONS = [ + { key: 'next', text: 'ChatGPT Next Web', value: 'next' }, + { key: 'ama', text: 'AMA 问天', value: 'ama' }, + { key: 'opencat', text: 'OpenCat', value: 'opencat' }, +]; + function renderTimestamp(timestamp) { return ( <> @@ -68,7 +74,40 @@ const TokensTable = () => { const refresh = async () => { setLoading(true); await loadTokens(activePage - 1); - } + }; + + const onCopy = async (type, key) => { + let status = localStorage.getItem('status'); + let serverAddress = ''; + if (status) { + status = JSON.parse(status); + serverAddress = status.server_address; + } + if (serverAddress === '') { + serverAddress = window.location.origin; + } + let encodedServerAddress = encodeURIComponent(serverAddress); + let url; + switch (type) { + case 'ama': + url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`; + break; + case 'opencat': + url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`; + break; + case 'next': + url = `https://chatgpt1.nextweb.fun/#/?settings=%7B%22key%22:%22sk-${key}%22,%22url%22:%22${serverAddress}%22%7D`; + break; + default: + url = `sk-${key}`; + } + if (await copy(url)) { + showSuccess('已复制到剪贴板!'); + } else { + showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。'); + setSearchKeyword(url); + } + }; useEffect(() => { loadTokens(0) @@ -235,21 +274,28 @@ const TokensTable = () => { {token.expired_time === -1 ? '永不过期' : renderTimestamp(token.expired_time)}
- + } + > + 复制 + + { + await onCopy(value, token.key); + }} + trigger={<>} + /> + + {' '} From 446337c329a2267fa1a7fd1f9125775e17cbb5f4 Mon Sep 17 00:00:00 2001 From: glzjin Date: Sun, 6 Aug 2023 17:40:31 +0800 Subject: [PATCH 27/73] fix: calculate usage if not given in non-stream mode (#352) --- controller/relay-openai.go | 14 +++++++++++++- controller/relay-text.go | 2 +- controller/relay.go | 5 +++-- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/controller/relay-openai.go b/controller/relay-openai.go index 8d7b432b..298dbe95 100644 --- a/controller/relay-openai.go +++ b/controller/relay-openai.go @@ -92,7 +92,7 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*O return nil, responseText } -func openaiHandler(c *gin.Context, resp *http.Response, consumeQuota bool) (*OpenAIErrorWithStatusCode, *Usage) { +func openaiHandler(c *gin.Context, resp *http.Response, consumeQuota bool, promptTokens int, model string) (*OpenAIErrorWithStatusCode, *Usage) { var textResponse TextResponse if consumeQuota { responseBody, err := io.ReadAll(resp.Body) @@ -132,5 +132,17 @@ func openaiHandler(c *gin.Context, resp *http.Response, consumeQuota bool) (*Ope if err != nil { return errorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil } + + if textResponse.Usage.TotalTokens == 0 { + completionTokens := 0 + for _, choice := range textResponse.Choices { + completionTokens += countTokenText(choice.Message.Content, model) + } + textResponse.Usage = Usage{ + PromptTokens: promptTokens, + CompletionTokens: completionTokens, + TotalTokens: promptTokens + completionTokens, + } + } return nil, &textResponse.Usage } diff --git a/controller/relay-text.go b/controller/relay-text.go index ceac4103..df4a6a99 100644 --- a/controller/relay-text.go +++ b/controller/relay-text.go @@ -362,7 +362,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { textResponse.Usage.CompletionTokens = countTokenText(responseText, textRequest.Model) return nil } else { - err, usage := openaiHandler(c, resp, consumeQuota) + err, usage := openaiHandler(c, resp, consumeQuota, promptTokens, textRequest.Model) if err != nil { return err } diff --git a/controller/relay.go b/controller/relay.go index 609ae2eb..dee9c94d 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -81,8 +81,9 @@ type OpenAIErrorWithStatusCode struct { } type TextResponse struct { - Usage `json:"usage"` - Error OpenAIError `json:"error"` + Choices []OpenAITextResponseChoice `json:"choices"` + Usage `json:"usage"` + Error OpenAIError `json:"error"` } type OpenAITextResponseChoice struct { From d96cf2e84d5d54b9d0aab4fc642b093c3ce5c155 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 6 Aug 2023 18:09:00 +0800 Subject: [PATCH 28/73] fix: fix stream mode determine related logic (close #360) --- controller/relay-text.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/relay-text.go b/controller/relay-text.go index df4a6a99..65f03bcf 100644 --- a/controller/relay-text.go +++ b/controller/relay-text.go @@ -302,7 +302,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { if err != nil { return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError) } - isStream = strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream") + isStream = isStream || strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream") } var textResponse TextResponse From b464e2907a1983a4702f852065f4fd46b123d1f2 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 6 Aug 2023 18:14:13 +0800 Subject: [PATCH 29/73] chore: update domain --- web/src/components/TokensTable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/TokensTable.js b/web/src/components/TokensTable.js index d45858f7..d6652c7c 100644 --- a/web/src/components/TokensTable.js +++ b/web/src/components/TokensTable.js @@ -96,7 +96,7 @@ const TokensTable = () => { url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`; break; case 'next': - url = `https://chatgpt1.nextweb.fun/#/?settings=%7B%22key%22:%22sk-${key}%22,%22url%22:%22${serverAddress}%22%7D`; + url = `https://chat.oneapi.pro/#/?settings=%7B%22key%22:%22sk-${key}%22,%22url%22:%22${serverAddress}%22%7D`; break; default: url = `sk-${key}`; From 5a62357c9356dd35ab06b9f7282550e9803c11df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yolo=C2=B0?= <136311867+yangfan-sys@users.noreply.github.com> Date: Sun, 6 Aug 2023 22:02:58 +0800 Subject: [PATCH 30/73] feat: add chat button for each token (#363) * fork * fork * chore: update style --------- Co-authored-by: JustSong --- i18n/en.json | 8 ++- web/src/components/PersonalSetting.js | 84 +++++++++++++++++++++---- web/src/components/TokensTable.js | 89 ++++++++++++++++++++++++--- web/src/pages/Token/EditToken.js | 13 ++-- web/src/pages/User/EditUser.js | 8 ++- 5 files changed, 173 insertions(+), 29 deletions(-) diff --git a/i18n/en.json b/i18n/en.json index 3c430a7e..8dac629e 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -3,6 +3,11 @@ "%d 点额度": "%d point quota", "尚未实现": "Not yet implemented", "余额不足": "Insufficient balance", + "危险操作": "Hazardous operations", + "输入你的账户名": "Enter your account name", + "确认删除": "Confirm Delete", + "确认绑定": "Confirm Binding", + "您正在删除自己的帐户,将清空所有数据且不可恢复": "You are deleting your account, all data will be cleared and unrecoverable.", "\"通道「%s」(#%d)已被禁用\"": "\"Channel %s (#%d) has been disabled\"", "通道「%s」(#%d)已被禁用,原因:%s": "Channel %s (#%d) has been disabled, reason: %s", "测试已在运行中": "Test is already running", @@ -427,7 +432,7 @@ "一分钟后过期": "Expires after one minute", "创建新的令牌": "Create New Token", "注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。": "Note that the quota of the token is only used to limit the maximum quota usage of the token itself, and the actual usage is limited by the remaining quota of the account.", - "设置为无限额度": "Set to unlimited quota", + "设为无限额度": "Set to unlimited quota", "更新令牌信息": "Update Token Information", "请输入充值码!": "Please enter the recharge code!", "请输入名称": "Please enter a name", @@ -493,6 +498,7 @@ "参数替换为你的部署名称(模型名称中的点会被剔除)": "Replace the parameter with your deployment name (dots in the model name will be removed)", "模型映射必须是合法的 JSON 格式!": "Model mapping must be in valid JSON format!", "取消无限额度": "Cancel unlimited quota", + "取消": "Cancel", "请输入新的剩余额度": "Please enter the new remaining quota", "请输入单个兑换码中包含的额度": "Please enter the quota included in a single redemption code", "请输入用户名": "Please enter username", diff --git a/web/src/components/PersonalSetting.js b/web/src/components/PersonalSetting.js index 108655d2..c7a303f9 100644 --- a/web/src/components/PersonalSetting.js +++ b/web/src/components/PersonalSetting.js @@ -25,6 +25,8 @@ const PersonalSetting = () => { const [loading, setLoading] = useState(false); const [disableButton, setDisableButton] = useState(false); const [countdown, setCountdown] = useState(30); + const [affLink, setAffLink] = useState(""); + const [systemToken, setSystemToken] = useState(""); useEffect(() => { let status = localStorage.getItem('status'); @@ -59,8 +61,10 @@ const PersonalSetting = () => { const res = await API.get('/api/user/token'); const { success, message, data } = res.data; if (success) { + setSystemToken(data); + setAffLink(""); await copy(data); - showSuccess(`令牌已重置并已复制到剪贴板:${data}`); + showSuccess(`令牌已重置并已复制到剪贴板`); } else { showError(message); } @@ -71,13 +75,27 @@ const PersonalSetting = () => { const { success, message, data } = res.data; if (success) { let link = `${window.location.origin}/register?aff=${data}`; + setAffLink(link); + setSystemToken(""); await copy(link); - showNotice(`邀请链接已复制到剪切板:${link}`); + showSuccess(`邀请链接已复制到剪切板`); } else { showError(message); } }; + const handleAffLinkClick = async (e) => { + e.target.select(); + await copy(e.target.value); + showSuccess(`邀请链接已复制到剪切板`); + }; + + const handleSystemTokenClick = async (e) => { + e.target.select(); + await copy(e.target.value); + showSuccess(`系统令牌已复制到剪切板`); + }; + const deleteAccount = async () => { if (inputs.self_account_deletion_confirmation !== userState.user.username) { showError('请输入你的账户名以确认删除!'); @@ -168,6 +186,25 @@ const PersonalSetting = () => { + + {systemToken && ( + + )} + {affLink && ( + + )}
账号绑定
{ @@ -262,6 +299,7 @@ const PersonalSetting = () => { ) : ( <> )} +
+
+ +
@@ -282,8 +329,9 @@ const PersonalSetting = () => { size={'tiny'} style={{ maxWidth: '450px' }} > - 确认删除自己的帐户 + 危险操作 + 您正在删除自己的帐户,将清空所有数据且不可恢复
{ ) : ( <> )} - +
+ +
+ +
diff --git a/web/src/components/TokensTable.js b/web/src/components/TokensTable.js index d6652c7c..b45f07df 100644 --- a/web/src/components/TokensTable.js +++ b/web/src/components/TokensTable.js @@ -12,6 +12,11 @@ const COPY_OPTIONS = [ { key: 'opencat', text: 'OpenCat', value: 'opencat' }, ]; +const OPEN_LINK_OPTIONS = [ + { key: 'ama', text: 'AMA 问天', value: 'ama' }, + { key: 'opencat', text: 'OpenCat', value: 'opencat' }, +]; + function renderTimestamp(timestamp) { return ( <> @@ -87,6 +92,15 @@ const TokensTable = () => { serverAddress = window.location.origin; } let encodedServerAddress = encodeURIComponent(serverAddress); + const nextLink = localStorage.getItem('chat_link'); + let nextUrl; + + if (nextLink) { + nextUrl = nextLink + `/#/?settings={"key":"sk-${key}"}`; + } else { + nextUrl = `https://chat.oneapi.pro/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; + } + let url; switch (type) { case 'ama': @@ -96,7 +110,7 @@ const TokensTable = () => { url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`; break; case 'next': - url = `https://chat.oneapi.pro/#/?settings=%7B%22key%22:%22sk-${key}%22,%22url%22:%22${serverAddress}%22%7D`; + url = nextUrl; break; default: url = `sk-${key}`; @@ -109,6 +123,42 @@ const TokensTable = () => { } }; + const onOpenLink = async (type, key) => { + let status = localStorage.getItem('status'); + let serverAddress = ''; + if (status) { + status = JSON.parse(status); + serverAddress = status.server_address; + } + if (serverAddress === '') { + serverAddress = window.location.origin; + } + let encodedServerAddress = encodeURIComponent(serverAddress); + const chatLink = localStorage.getItem('chat_link'); + let defaultUrl; + + if (chatLink) { + defaultUrl = chatLink + `/#/?settings={"key":"sk-${key}"}`; + } else { + defaultUrl = `https://chat.oneapi.pro/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; + } + let url; + switch (type) { + case 'ama': + url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`; + break; + + case 'opencat': + url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`; + break; + + default: + url = defaultUrl; + } + + window.open(url, '_blank'); + } + useEffect(() => { loadTokens(0) .then() @@ -274,28 +324,51 @@ const TokensTable = () => { {token.expired_time === -1 ? '永不过期' : renderTimestamp(token.expired_time)}
- + { - await onCopy(value, token.key); - }} + options={COPY_OPTIONS.map(option => ({ + ...option, + onClick: async () => { + await onCopy(option.value, token.key); + } + }))} trigger={<>} /> {' '} + + + ({ + ...option, + onClick: async () => { + await onOpenLink(option.value, token.key); + } + }))} + trigger={<>} + /> + + {' '} diff --git a/web/src/pages/Token/EditToken.js b/web/src/pages/Token/EditToken.js index 1f85520b..0ab37c29 100644 --- a/web/src/pages/Token/EditToken.js +++ b/web/src/pages/Token/EditToken.js @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { Button, Form, Header, Message, Segment } from 'semantic-ui-react'; -import { useParams } from 'react-router-dom'; +import { useParams, useNavigate } from 'react-router-dom'; import { API, showError, showSuccess, timestamp2string } from '../../helpers'; import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render'; @@ -17,11 +17,13 @@ const EditToken = () => { }; const [inputs, setInputs] = useState(originInputs); const { name, remain_quota, expired_time, unlimited_quota } = inputs; - + const navigate = useNavigate(); const handleInputChange = (e, { name, value }) => { setInputs((inputs) => ({ ...inputs, [name]: value })); }; - + const handleCancel = () => { + navigate("/token"); + } const setExpiredTime = (month, day, hour, minute) => { let now = new Date(); let timestamp = now.getTime() / 1000; @@ -150,8 +152,9 @@ const EditToken = () => { - + }}>{unlimited_quota ? '取消无限额度' : '设为无限额度'} + + diff --git a/web/src/pages/User/EditUser.js b/web/src/pages/User/EditUser.js index b1c77945..e8f96027 100644 --- a/web/src/pages/User/EditUser.js +++ b/web/src/pages/User/EditUser.js @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { Button, Form, Header, Segment } from 'semantic-ui-react'; -import { useParams } from 'react-router-dom'; +import { useParams, useNavigate } from 'react-router-dom'; import { API, showError, showSuccess } from '../../helpers'; import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render'; @@ -36,7 +36,10 @@ const EditUser = () => { showError(error.message); } }; - + const navigate = useNavigate(); + const handleCancel = () => { + navigate("/setting"); + } const loadUser = async () => { let res = undefined; if (userId) { @@ -176,6 +179,7 @@ const EditUser = () => { readOnly /> + From 7422b0d051992af08118fde81bf2257da01cc5b3 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 6 Aug 2023 22:07:31 +0800 Subject: [PATCH 31/73] chore: update prompt --- web/src/pages/Channel/EditChannel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index 6974315e..4cfec018 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -371,9 +371,9 @@ const EditChannel = () => { inputs.type !== 3 && inputs.type !== 8 && ( Date: Sun, 6 Aug 2023 22:09:05 +0800 Subject: [PATCH 32/73] chore: update i18n --- i18n/en.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/i18n/en.json b/i18n/en.json index 8dac629e..f53aad4c 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -516,5 +516,7 @@ "请输入渠道对应的鉴权密钥": "Please enter the authentication key corresponding to the channel", "注意,": "Note that, ", ",图片演示。": "related image demo.", - "令牌创建成功,请在列表页面点击复制获取令牌!": "Token created successfully, please click copy on the list page to get the token!" + "令牌创建成功,请在列表页面点击复制获取令牌!": "Token created successfully, please click copy on the list page to get the token!", + "代理": "Proxy", + "此项可选,用于通过代理站来进行 API 调用,请输入代理站地址,格式为:https://domain.com": "This is optional, used to make API calls through the proxy site, please enter the proxy site address, the format is: https://domain.com" } From 463b0b3c5176b2c468847cfee7a1dd0a6c4bfe03 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 6 Aug 2023 22:28:07 +0800 Subject: [PATCH 33/73] fix: no need to check turnstile when process deletion --- router/api-router.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/router/api-router.go b/router/api-router.go index 383133fa..cc330d7e 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -36,7 +36,7 @@ func SetApiRouter(router *gin.Engine) { { selfRoute.GET("/self", controller.GetSelf) selfRoute.PUT("/self", controller.UpdateSelf) - selfRoute.DELETE("/self", middleware.TurnstileCheck(), controller.DeleteSelf) + selfRoute.DELETE("/self", controller.DeleteSelf) selfRoute.GET("/token", controller.GenerateAccessToken) selfRoute.GET("/aff", controller.GetAffCode) selfRoute.POST("/topup", controller.TopUp) From 9c436921d13181fae8abe7ba70df664f75e7d8a0 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 6 Aug 2023 23:18:05 +0800 Subject: [PATCH 34/73] docs: update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fcabd2c5..ad90bb15 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 如果上面的镜像无法拉取,可以尝试使用 GitHub 的 Docker 镜像,将上面的 `justsong/one-api` 替换为 `ghcr.io/songquanpeng/one-api` 即可。 -如果你的并发量较大,推荐设置 `SQL_DSN`,详见下面[环境变量](#环境变量)一节。 +如果你的并发量较大,**务必**设置 `SQL_DSN`,详见下面[环境变量](#环境变量)一节。 更新命令:`docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR` @@ -213,7 +213,7 @@ docker run --name chatgpt-web -d -p 3002:3002 -e OPENAI_API_BASE_URL=https://ope > Sealos 的服务器在国外,不需要额外处理网络问题,支持高并发 & 动态伸缩。 -点击以下按钮一键部署: +点击以下按钮一键部署(部署后访问出现 404 请等待 3~5 分钟): [![Deploy-on-Sealos.svg](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-fastdeploy?templateName=one-api) From 29fa94e7d28140f42bf49d95268f3c12f7c6a0ea Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Fri, 11 Aug 2023 19:27:42 +0800 Subject: [PATCH 35/73] docs: update FastGPT's description (#388) --- README.en.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.en.md b/README.en.md index 2ea6e65e..92eb567f 100644 --- a/README.en.md +++ b/README.en.md @@ -283,7 +283,7 @@ If the channel ID is not provided, load balancing will be used to distribute the + Double-check that your interface address and API Key are correct. ## Related Projects -[FastGPT](https://github.com/c121914yu/FastGPT): Build an AI knowledge base in three minutes +[FastGPT](https://github.com/labring/FastGPT): Knowledge question answering system based on the LLM ## Note This project is an open-source project. Please use it in compliance with OpenAI's [Terms of Use](https://openai.com/policies/terms-of-use) and **applicable laws and regulations**. It must not be used for illegal purposes. diff --git a/README.md b/README.md index ad90bb15..9840fa19 100644 --- a/README.md +++ b/README.md @@ -333,7 +333,7 @@ https://openai.justsong.cn + 上游通道 429 了。 ## 相关项目 -[FastGPT](https://github.com/c121914yu/FastGPT): 三分钟搭建 AI 知识库 +[FastGPT](https://github.com/labring/FastGPT): 基于 LLM 大语言模型的知识库问答系统 ## 注意 From 3b36608bbd47eadcd581e264a430290a287143c4 Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 11 Aug 2023 19:53:01 +0800 Subject: [PATCH 36/73] fix: update no route handler --- controller/relay.go | 6 +++--- router/web-router.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/controller/relay.go b/controller/relay.go index dee9c94d..617e22b8 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -207,10 +207,10 @@ func RelayNotImplemented(c *gin.Context) { func RelayNotFound(c *gin.Context) { err := OpenAIError{ - Message: fmt.Sprintf("API not found: %s:%s", c.Request.Method, c.Request.URL.Path), - Type: "one_api_error", + Message: fmt.Sprintf("Invalid URL (%s %s)", c.Request.Method, c.Request.URL.Path), + Type: "invalid_request_error", Param: "", - Code: "api_not_found", + Code: "", } c.JSON(http.StatusNotFound, gin.H{ "error": err, diff --git a/router/web-router.go b/router/web-router.go index 19fc0c04..8f9c18a2 100644 --- a/router/web-router.go +++ b/router/web-router.go @@ -18,7 +18,7 @@ func SetWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) { router.Use(middleware.Cache()) router.Use(static.Serve("/", common.EmbedFolder(buildFS, "web/build"))) router.NoRoute(func(c *gin.Context) { - if strings.HasPrefix(c.Request.RequestURI, "/v1") { + if strings.HasPrefix(c.Request.RequestURI, "/v1") || strings.HasPrefix(c.Request.RequestURI, "/api") { controller.RelayNotFound(c) return } From 3a18cebe3490a4a56207bf0dea1e54aaa90c2f84 Mon Sep 17 00:00:00 2001 From: glzjin Date: Fri, 11 Aug 2023 22:24:11 +0800 Subject: [PATCH 37/73] fix: update tiktoken-go's version to fix resource consumption problem (#392, close #161) --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 1d061520..1d08a7d3 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.5.0 - github.com/pkoukk/tiktoken-go v0.1.1 + github.com/pkoukk/tiktoken-go v0.1.5 golang.org/x/crypto v0.9.0 gorm.io/driver/mysql v1.4.3 gorm.io/driver/sqlite v1.4.3 @@ -26,7 +26,7 @@ require ( github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/dlclark/regexp2 v1.8.1 // indirect + github.com/dlclark/regexp2 v1.10.0 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect diff --git a/go.sum b/go.sum index c6e4423c..b4281cb6 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0= -github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= +github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= @@ -112,8 +112,8 @@ github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZO github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkoukk/tiktoken-go v0.1.1 h1:jtkYlIECjyM9OW1w4rjPmTohK4arORP9V25y6TM6nXo= -github.com/pkoukk/tiktoken-go v0.1.1/go.mod h1:boMWvk9pQCOTx11pgu0DrIdrAKgQzzJKUP6vLXaz7Rw= +github.com/pkoukk/tiktoken-go v0.1.5 h1:hAlT4dCf6Uk50x8E7HQrddhH3EWMKUN+LArExQQsQx4= +github.com/pkoukk/tiktoken-go v0.1.5/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= From 2b088a167855c49425add5ffc54b4d687e83cd01 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 12 Aug 2023 09:29:29 +0800 Subject: [PATCH 38/73] fix: disable eslint when building (close #371, close #376) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4afbf100..22055553 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ WORKDIR /build COPY ./web . COPY ./VERSION . RUN npm install -RUN REACT_APP_VERSION=$(cat VERSION) npm run build +RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build FROM golang AS builder2 From 466005de07c668e620c2539a9c96230d2534102b Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 12 Aug 2023 10:05:25 +0800 Subject: [PATCH 39/73] fix: set connection limits for database --- README.md | 5 +++++ common/utils.go | 13 +++++++++++++ model/main.go | 11 ++++++++++- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9840fa19..2f9d0d39 100644 --- a/README.md +++ b/README.md @@ -281,6 +281,11 @@ graph LR + 注意需要提前建立数据库 `oneapi`,无需手动建表,程序将自动建表。 + 如果使用本地数据库:部署命令可添加 `--network="host"` 以使得容器内的程序可以访问到宿主机上的 MySQL。 + 如果使用云数据库:如果云服务器需要验证身份,需要在连接参数中添加 `?tls=skip-verify`。 + + 请根据你的数据库配置修改下列参数(或者保持默认值): + + `SQL_MAX_IDLE_CONNS`:最大空闲连接数,默认为 `10`。 + + `SQL_MAX_OPEN_CONNS`:最大打开连接数,默认为 `100`。 + + 如果报错 `Error 1040: Too many connections`,请适当减小该值。 + + `SQL_CONN_MAX_LIFETIME`:连接的最大生命周期,默认为 `60`,单位分钟。 4. `FRONTEND_BASE_URL`:设置之后将重定向页面请求到指定的地址,仅限从服务器设置。 + 例子:`FRONTEND_BASE_URL=https://openai.justsong.cn` 5. `SYNC_FREQUENCY`:设置之后将定期与数据库同步配置,单位为秒,未设置则不进行同步。 diff --git a/common/utils.go b/common/utils.go index 1329c1a0..bb9b7e0c 100644 --- a/common/utils.go +++ b/common/utils.go @@ -7,6 +7,7 @@ import ( "log" "math/rand" "net" + "os" "os/exec" "runtime" "strconv" @@ -177,3 +178,15 @@ func Max(a int, b int) int { return b } } + +func GetOrDefault(env string, defaultValue int) int { + if env == "" || os.Getenv(env) == "" { + return defaultValue + } + num, err := strconv.Atoi(os.Getenv(env)) + if err != nil { + SysError(fmt.Sprintf("failed to parse %s: %s, using default value: %d", env, err.Error(), defaultValue)) + return defaultValue + } + return num +} diff --git a/model/main.go b/model/main.go index 5bc5ce19..ddbc69aa 100644 --- a/model/main.go +++ b/model/main.go @@ -6,6 +6,7 @@ import ( "gorm.io/gorm" "one-api/common" "os" + "time" ) var DB *gorm.DB @@ -57,10 +58,18 @@ func InitDB() (err error) { common.SysLog("database connected") if err == nil { DB = db + sqlDB, err := DB.DB() + if err != nil { + return err + } + sqlDB.SetMaxIdleConns(common.GetOrDefault("SQL_MAX_IDLE_CONNS", 10)) + sqlDB.SetMaxOpenConns(common.GetOrDefault("SQL_MAX_OPEN_CONNS", 100)) + sqlDB.SetConnMaxLifetime(time.Second * time.Duration(common.GetOrDefault("SQL_MAX_LIFETIME", 60))) + if !common.IsMasterNode { return nil } - err := db.AutoMigrate(&Channel{}) + err = db.AutoMigrate(&Channel{}) if err != nil { return err } From f2159e1033a655ce9476f2d704051cdba5f6cd4d Mon Sep 17 00:00:00 2001 From: wood chen <95951386+woodchen-ink@users.noreply.github.com> Date: Sat, 12 Aug 2023 10:14:13 +0800 Subject: [PATCH 40/73] docs: update README (#374) --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2f9d0d39..0f7ca2a9 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,8 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 更新命令:`docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR` -`-p 3000:3000` 中的第一个 `3000` 是宿主机的端口,可以根据需要进行修改。 + +其中,`-p 3000:3000` 中的第一个 `3000` 是宿主机的端口,可以根据需要进行修改。 数据将会保存在宿主机的 `/home/ubuntu/data/one-api` 目录,请确保该目录存在且具有写入权限,或者更改为合适的目录。 From be780462f1d559949ea040ca06752485db5df971 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 12 Aug 2023 10:16:59 +0800 Subject: [PATCH 41/73] docs: update README --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0f7ca2a9..8e18d6b4 100644 --- a/README.md +++ b/README.md @@ -102,17 +102,16 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 ### 基于 Docker 进行部署 部署命令:`docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api` +其中,`-p 3000:3000` 中的第一个 `3000` 是宿主机的端口,可以根据需要进行修改。 + +数据将会保存在宿主机的 `/home/ubuntu/data/one-api` 目录,请确保该目录存在且具有写入权限,或者更改为合适的目录。 + 如果上面的镜像无法拉取,可以尝试使用 GitHub 的 Docker 镜像,将上面的 `justsong/one-api` 替换为 `ghcr.io/songquanpeng/one-api` 即可。 如果你的并发量较大,**务必**设置 `SQL_DSN`,详见下面[环境变量](#环境变量)一节。 更新命令:`docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR` - -其中,`-p 3000:3000` 中的第一个 `3000` 是宿主机的端口,可以根据需要进行修改。 - -数据将会保存在宿主机的 `/home/ubuntu/data/one-api` 目录,请确保该目录存在且具有写入权限,或者更改为合适的目录。 - Nginx 的参考配置: ``` server{ From 150d068e9f089fd9198a8523910863661a29998f Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 12 Aug 2023 10:20:54 +0800 Subject: [PATCH 42/73] chore: update prompt --- controller/relay.go | 2 +- i18n/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/controller/relay.go b/controller/relay.go index 617e22b8..e5d898f5 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -176,7 +176,7 @@ func Relay(c *gin.Context) { c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s?retry=%d", c.Request.URL.Path, retryTimes-1)) } else { if err.StatusCode == http.StatusTooManyRequests { - err.OpenAIError.Message = "当前分组负载已饱和,请稍后再试,或升级账户以提升服务质量。" + err.OpenAIError.Message = "当前分组上游负载已饱和,请稍后再试" } c.JSON(err.StatusCode, gin.H{ "error": err.OpenAIError, diff --git a/i18n/en.json b/i18n/en.json index f53aad4c..9ea33c38 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -39,7 +39,7 @@ "兑换码个数必须大于0": "The number of redemption codes must be greater than 0", "一次兑换码批量生成的个数不能大于 100": "The number of redemption codes generated in a batch cannot be greater than 100", "通过令牌「%s」使用模型 %s 消耗 %s(模型倍率 %.2f,分组倍率 %.2f)": "Using model %s with token %s consumes %s (model rate %.2f, group rate %.2f)", - "当前分组负载已饱和,请稍后再试,或升级账户以提升服务质量。": "The current group load is saturated, please try again later, or upgrade your account to improve service quality.", + "当前分组上游负载已饱和,请稍后再试": "The current group load is saturated, please try again later", "令牌名称长度必须在1-20之间": "The length of the token name must be between 1-20", "令牌已过期,无法启用,请先修改令牌过期时间,或者设置为永不过期": "The token has expired and cannot be enabled. Please modify the expiration time of the token, or set it to never expire.", "令牌可用额度已用尽,无法启用,请先修改令牌剩余额度,或者设置为无限额度": "The available quota of the token has been used up and cannot be enabled. Please modify the remaining quota of the token, or set it to unlimited quota", From c58f710227d5c71c606ad78f17f6f1ff19f65606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yolo=C2=B0?= <136311867+yangfan-sys@users.noreply.github.com> Date: Sat, 12 Aug 2023 10:49:30 +0800 Subject: [PATCH 43/73] feat: improve frontend (#387) * fork * fork * chore: update style --------- Co-authored-by: JustSong --- controller/token.go | 11 +++++-- i18n/en.json | 7 +++-- web/src/components/ChannelsTable.js | 4 +-- web/src/components/OtherSetting.js | 2 +- web/src/components/RedemptionsTable.js | 28 ++++++++++++------ web/src/components/SystemSetting.js | 34 +++++++++++++++++++++- web/src/helpers/utils.js | 13 +++++++-- web/src/pages/Channel/EditChannel.js | 8 ++++- web/src/pages/Home/index.js | 9 +++--- web/src/pages/Redemption/EditRedemption.js | 8 ++++- 10 files changed, 98 insertions(+), 26 deletions(-) diff --git a/controller/token.go b/controller/token.go index 5341ea3a..b05d820a 100644 --- a/controller/token.go +++ b/controller/token.go @@ -109,10 +109,10 @@ func AddToken(c *gin.Context) { }) return } - if len(token.Name) == 0 || len(token.Name) > 20 { + if len(token.Name) == 0 || len(token.Name) > 30 { c.JSON(http.StatusOK, gin.H{ "success": false, - "message": "令牌名称长度必须在1-20之间", + "message": "令牌名称过长", }) return } @@ -171,6 +171,13 @@ func UpdateToken(c *gin.Context) { }) return } + if len(token.Name) == 0 || len(token.Name) > 30 { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "令牌名称过长", + }) + return + } cleanToken, err := model.GetTokenByIds(token.Id, userId) if err != nil { c.JSON(http.StatusOK, gin.H{ diff --git a/i18n/en.json b/i18n/en.json index f53aad4c..78df1acf 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -40,7 +40,7 @@ "一次兑换码批量生成的个数不能大于 100": "The number of redemption codes generated in a batch cannot be greater than 100", "通过令牌「%s」使用模型 %s 消耗 %s(模型倍率 %.2f,分组倍率 %.2f)": "Using model %s with token %s consumes %s (model rate %.2f, group rate %.2f)", "当前分组负载已饱和,请稍后再试,或升级账户以提升服务质量。": "The current group load is saturated, please try again later, or upgrade your account to improve service quality.", - "令牌名称长度必须在1-20之间": "The length of the token name must be between 1-20", + "令牌名称过长": "Token name is too long", "令牌已过期,无法启用,请先修改令牌过期时间,或者设置为永不过期": "The token has expired and cannot be enabled. Please modify the expiration time of the token, or set it to never expire.", "令牌可用额度已用尽,无法启用,请先修改令牌剩余额度,或者设置为无限额度": "The available quota of the token has been used up and cannot be enabled. Please modify the remaining quota of the token, or set it to unlimited quota", "管理员关闭了密码登录": "The administrator has turned off password login", @@ -229,7 +229,7 @@ "已是最新版本": "Is the latest version", "检查更新": "Check for updates", "公告": "Announcement", - "在此输入新的公告内容": "Enter new announcement content here", + "在此输入新的公告内容,支持 Markdown & HTML 代码": "Enter the new announcement content here, supports Markdown & HTML code", "保存公告": "Save Announcement", "个性化设置": "Personalization Settings", "系统名称": "System Name", @@ -518,5 +518,6 @@ ",图片演示。": "related image demo.", "令牌创建成功,请在列表页面点击复制获取令牌!": "Token created successfully, please click copy on the list page to get the token!", "代理": "Proxy", - "此项可选,用于通过代理站来进行 API 调用,请输入代理站地址,格式为:https://domain.com": "This is optional, used to make API calls through the proxy site, please enter the proxy site address, the format is: https://domain.com" + "此项可选,用于通过代理站来进行 API 调用,请输入代理站地址,格式为:https://domain.com": "This is optional, used to make API calls through the proxy site, please enter the proxy site address, the format is: https://domain.com", + "取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?": "Canceling password login will cause all users (including administrators) who have not bound other login methods to be unable to log in via password, confirm cancel?" } diff --git a/web/src/components/ChannelsTable.js b/web/src/components/ChannelsTable.js index 0459619a..072f5b90 100644 --- a/web/src/components/ChannelsTable.js +++ b/web/src/components/ChannelsTable.js @@ -447,8 +447,8 @@ const ChannelsTable = () => { - + {/* */} { { > 复制 - + } + on='click' + flowing + hoverable > - 删除 - + + + + + + } { + return
; +}; +export default HTMLToastContent; export function isAdmin() { let user = localStorage.getItem('user'); if (!user) return false; @@ -107,8 +112,12 @@ export function showInfo(message) { toast.info(message, showInfoOptions); } -export function showNotice(message) { - toast.info(message, showNoticeOptions); +export function showNotice(message, isHTML = false) { + if (isHTML) { + toast(, showNoticeOptions); + } else { + toast.info(message, showNoticeOptions); + } } export function openPage(url) { diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index 4cfec018..0d7a4a01 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { Button, Form, Header, Input, Message, Segment } from 'semantic-ui-react'; -import { useParams } from 'react-router-dom'; +import { useParams, useNavigate } from 'react-router-dom'; import { API, showError, showInfo, showSuccess, verifyJSON } from '../../helpers'; import { CHANNEL_OPTIONS } from '../../constants'; @@ -12,9 +12,14 @@ const MODEL_MAPPING_EXAMPLE = { const EditChannel = () => { const params = useParams(); + const navigate = useNavigate(); const channelId = params.id; const isEdit = channelId !== undefined; const [loading, setLoading] = useState(isEdit); + const handleCancel = () => { + navigate('/channel'); + }; + const originInputs = { name: '', type: 1, @@ -381,6 +386,7 @@ const EditChannel = () => { ) } + diff --git a/web/src/pages/Home/index.js b/web/src/pages/Home/index.js index 20d42104..c9f4d445 100644 --- a/web/src/pages/Home/index.js +++ b/web/src/pages/Home/index.js @@ -14,10 +14,11 @@ const Home = () => { const { success, message, data } = res.data; if (success) { let oldNotice = localStorage.getItem('notice'); - if (data !== oldNotice && data !== '') { - showNotice(data); - localStorage.setItem('notice', data); - } + if (data !== oldNotice && data !== '') { + const htmlNotice = marked(data); + showNotice(htmlNotice, true); + localStorage.setItem('notice', data); + } } else { showError(message); } diff --git a/web/src/pages/Redemption/EditRedemption.js b/web/src/pages/Redemption/EditRedemption.js index df614ab5..7a33f770 100644 --- a/web/src/pages/Redemption/EditRedemption.js +++ b/web/src/pages/Redemption/EditRedemption.js @@ -1,11 +1,12 @@ import React, { useEffect, useState } from 'react'; import { Button, Form, Header, Segment } from 'semantic-ui-react'; -import { useParams } from 'react-router-dom'; +import { useParams, useNavigate } from 'react-router-dom'; import { API, downloadTextAsFile, showError, showSuccess } from '../../helpers'; import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render'; const EditRedemption = () => { const params = useParams(); + const navigate = useNavigate(); const redemptionId = params.id; const isEdit = redemptionId !== undefined; const [loading, setLoading] = useState(isEdit); @@ -17,6 +18,10 @@ const EditRedemption = () => { const [inputs, setInputs] = useState(originInputs); const { name, quota, count } = inputs; + const handleCancel = () => { + navigate('/redemption'); + }; + const handleInputChange = (e, { name, value }) => { setInputs((inputs) => ({ ...inputs, [name]: value })); }; @@ -113,6 +118,7 @@ const EditRedemption = () => { } + From 476a46ad7e9c25c3d4900a9e2f94ea8717491b37 Mon Sep 17 00:00:00 2001 From: igophper <34326532+igophper@users.noreply.github.com> Date: Sat, 12 Aug 2023 11:04:53 +0800 Subject: [PATCH 44/73] fix: fix finish_reason fileld not fully compatible with OpenAI (close #372, #373) * optimize:unify finish_reason field * refactor: use a global stop finish reason --------- Co-authored-by: JustSong --- controller/relay-ali.go | 5 ++++- controller/relay-baidu.go | 4 +++- controller/relay-claude.go | 5 ++++- controller/relay-palm.go | 2 +- controller/relay-utils.go | 2 ++ controller/relay-xunfei.go | 3 +++ controller/relay-zhipu.go | 3 +-- controller/relay.go | 2 +- 8 files changed, 19 insertions(+), 7 deletions(-) diff --git a/controller/relay-ali.go b/controller/relay-ali.go index e8437c27..e94abd6a 100644 --- a/controller/relay-ali.go +++ b/controller/relay-ali.go @@ -121,7 +121,10 @@ func responseAli2OpenAI(response *AliChatResponse) *OpenAITextResponse { func streamResponseAli2OpenAI(aliResponse *AliChatResponse) *ChatCompletionsStreamResponse { var choice ChatCompletionsStreamResponseChoice choice.Delta.Content = aliResponse.Output.Text - choice.FinishReason = aliResponse.Output.FinishReason + if aliResponse.Output.FinishReason != "null" { + finishReason := aliResponse.Output.FinishReason + choice.FinishReason = &finishReason + } response := ChatCompletionsStreamResponse{ Id: aliResponse.RequestId, Object: "chat.completion.chunk", diff --git a/controller/relay-baidu.go b/controller/relay-baidu.go index 7960e8ee..664bbd11 100644 --- a/controller/relay-baidu.go +++ b/controller/relay-baidu.go @@ -120,7 +120,9 @@ func responseBaidu2OpenAI(response *BaiduChatResponse) *OpenAITextResponse { func streamResponseBaidu2OpenAI(baiduResponse *BaiduChatStreamResponse) *ChatCompletionsStreamResponse { var choice ChatCompletionsStreamResponseChoice choice.Delta.Content = baiduResponse.Result - choice.FinishReason = "stop" + if baiduResponse.IsEnd { + choice.FinishReason = &stopFinishReason + } response := ChatCompletionsStreamResponse{ Id: baiduResponse.Id, Object: "chat.completion.chunk", diff --git a/controller/relay-claude.go b/controller/relay-claude.go index 1d67fa7b..052e5605 100644 --- a/controller/relay-claude.go +++ b/controller/relay-claude.go @@ -81,7 +81,10 @@ func requestOpenAI2Claude(textRequest GeneralOpenAIRequest) *ClaudeRequest { func streamResponseClaude2OpenAI(claudeResponse *ClaudeResponse) *ChatCompletionsStreamResponse { var choice ChatCompletionsStreamResponseChoice choice.Delta.Content = claudeResponse.Completion - choice.FinishReason = stopReasonClaude2OpenAI(claudeResponse.StopReason) + finishReason := stopReasonClaude2OpenAI(claudeResponse.StopReason) + if finishReason != "null" { + choice.FinishReason = &finishReason + } var response ChatCompletionsStreamResponse response.Object = "chat.completion.chunk" response.Model = claudeResponse.Model diff --git a/controller/relay-palm.go b/controller/relay-palm.go index 74624c7f..0053c9b8 100644 --- a/controller/relay-palm.go +++ b/controller/relay-palm.go @@ -94,7 +94,7 @@ func streamResponsePaLM2OpenAI(palmResponse *PaLMChatResponse) *ChatCompletionsS if len(palmResponse.Candidates) > 0 { choice.Delta.Content = palmResponse.Candidates[0].Content } - choice.FinishReason = "stop" + choice.FinishReason = &stopFinishReason var response ChatCompletionsStreamResponse response.Object = "chat.completion.chunk" response.Model = "palm2" diff --git a/controller/relay-utils.go b/controller/relay-utils.go index 2133d8be..3695e119 100644 --- a/controller/relay-utils.go +++ b/controller/relay-utils.go @@ -6,6 +6,8 @@ import ( "one-api/common" ) +var stopFinishReason = "stop" + var tokenEncoderMap = map[string]*tiktoken.Tiktoken{} func getTokenEncoder(model string) *tiktoken.Tiktoken { diff --git a/controller/relay-xunfei.go b/controller/relay-xunfei.go index 1faf3294..48472456 100644 --- a/controller/relay-xunfei.go +++ b/controller/relay-xunfei.go @@ -138,6 +138,9 @@ func streamResponseXunfei2OpenAI(xunfeiResponse *XunfeiChatResponse) *ChatComple } var choice ChatCompletionsStreamResponseChoice choice.Delta.Content = xunfeiResponse.Payload.Choices.Text[0].Content + if xunfeiResponse.Payload.Choices.Status == 2 { + choice.FinishReason = &stopFinishReason + } response := ChatCompletionsStreamResponse{ Object: "chat.completion.chunk", Created: common.GetTimestamp(), diff --git a/controller/relay-zhipu.go b/controller/relay-zhipu.go index 20a4fa42..b125f1e7 100644 --- a/controller/relay-zhipu.go +++ b/controller/relay-zhipu.go @@ -163,7 +163,6 @@ func responseZhipu2OpenAI(response *ZhipuResponse) *OpenAITextResponse { func streamResponseZhipu2OpenAI(zhipuResponse string) *ChatCompletionsStreamResponse { var choice ChatCompletionsStreamResponseChoice choice.Delta.Content = zhipuResponse - choice.FinishReason = "" response := ChatCompletionsStreamResponse{ Object: "chat.completion.chunk", Created: common.GetTimestamp(), @@ -176,7 +175,7 @@ func streamResponseZhipu2OpenAI(zhipuResponse string) *ChatCompletionsStreamResp func streamMetaResponseZhipu2OpenAI(zhipuResponse *ZhipuStreamMetaResponse) (*ChatCompletionsStreamResponse, *Usage) { var choice ChatCompletionsStreamResponseChoice choice.Delta.Content = "" - choice.FinishReason = "stop" + choice.FinishReason = &stopFinishReason response := ChatCompletionsStreamResponse{ Id: zhipuResponse.RequestId, Object: "chat.completion.chunk", diff --git a/controller/relay.go b/controller/relay.go index 617e22b8..030b27f7 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -124,7 +124,7 @@ type ChatCompletionsStreamResponseChoice struct { Delta struct { Content string `json:"content"` } `json:"delta"` - FinishReason string `json:"finish_reason,omitempty"` + FinishReason *string `json:"finish_reason"` } type ChatCompletionsStreamResponse struct { From 7e2bca7e9cc06015a3d5c0ef8c90e195a1abd2cd Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 12 Aug 2023 11:30:13 +0800 Subject: [PATCH 45/73] docs: update README --- README.en.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.en.md b/README.en.md index 92eb567f..8a041da8 100644 --- a/README.en.md +++ b/README.en.md @@ -190,7 +190,7 @@ If you encounter a blank page after deployment, refer to [#97](https://github.co > Zeabur's servers are located overseas, automatically solving network issues, and the free quota is sufficient for personal usage. 1. First, fork the code. -2. Go to [Zeabur](https://zeabur.com/), log in, and enter the console. +2. Go to [Zeabur](https://zeabur.com?referralCode=songquanpeng), log in, and enter the console. 3. Create a new project. In Service -> Add Service, select Marketplace, and choose MySQL. Note down the connection parameters (username, password, address, and port). 4. Copy the connection parameters and run ```create database `one-api` ``` to create the database. 5. Then, in Service -> Add Service, select Git (authorization is required for the first use) and choose your forked repository. diff --git a/README.md b/README.md index 8e18d6b4..02127100 100644 --- a/README.md +++ b/README.md @@ -227,7 +227,7 @@ docker run --name chatgpt-web -d -p 3002:3002 -e OPENAI_API_BASE_URL=https://ope > Zeabur 的服务器在国外,自动解决了网络的问题,同时免费的额度也足够个人使用。 1. 首先 fork 一份代码。 -2. 进入 [Zeabur](https://zeabur.com/),登录,进入控制台。 +2. 进入 [Zeabur](https://zeabur.com?referralCode=songquanpeng),登录,进入控制台。 3. 新建一个 Project,在 Service -> Add Service 选择 Marketplace,选择 MySQL,并记下连接参数(用户名、密码、地址、端口)。 4. 复制链接参数,运行 ```create database `one-api` ``` 创建数据库。 5. 然后在 Service -> Add Service,选择 Git(第一次使用需要先授权),选择你 fork 的仓库。 From 821c559e893ac59a4a0777ab04777b8581b63223 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 12 Aug 2023 12:17:01 +0800 Subject: [PATCH 46/73] docs: update README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 02127100..1f3a4a5e 100644 --- a/README.md +++ b/README.md @@ -338,7 +338,8 @@ https://openai.justsong.cn + 上游通道 429 了。 ## 相关项目 -[FastGPT](https://github.com/labring/FastGPT): 基于 LLM 大语言模型的知识库问答系统 +* [FastGPT](https://github.com/labring/FastGPT): 基于 LLM 大语言模型的知识库问答系统 +* [ChatGPT Next Web](https://github.com/Yidadaa/ChatGPT-Next-Web): 一键拥有你自己的跨平台 ChatGPT 应用 ## 注意 From e42119b73d493f3202cb4c315d2ab3c16315ebe4 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 12 Aug 2023 14:18:04 +0800 Subject: [PATCH 47/73] chore: use unknown as placeholder --- web/src/pages/Home/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/pages/Home/index.js b/web/src/pages/Home/index.js index c9f4d445..63d6d77a 100644 --- a/web/src/pages/Home/index.js +++ b/web/src/pages/Home/index.js @@ -65,7 +65,7 @@ const Home = () => { 系统信息总览

名称:{statusState?.status?.system_name}

-

版本:{statusState?.status?.version}

+

版本:{statusState?.status?.version ? statusState?.status?.version : "unknown"}

源码: Date: Sat, 12 Aug 2023 16:58:29 +0800 Subject: [PATCH 48/73] fix: fix token name too long --- controller/token.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/controller/token.go b/controller/token.go index b05d820a..8642122c 100644 --- a/controller/token.go +++ b/controller/token.go @@ -109,7 +109,7 @@ func AddToken(c *gin.Context) { }) return } - if len(token.Name) == 0 || len(token.Name) > 30 { + if len(token.Name) > 30 { c.JSON(http.StatusOK, gin.H{ "success": false, "message": "令牌名称过长", @@ -171,7 +171,7 @@ func UpdateToken(c *gin.Context) { }) return } - if len(token.Name) == 0 || len(token.Name) > 30 { + if len(token.Name) > 30 { c.JSON(http.StatusOK, gin.H{ "success": false, "message": "令牌名称过长", From 7bddc73b963e98a69095242f1aa6f79cfc865a72 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 12 Aug 2023 18:10:15 +0800 Subject: [PATCH 49/73] perf: flush response after response handled (close #364) --- common/constants.go | 2 ++ controller/relay-text.go | 4 ++-- main.go | 3 +++ model/main.go | 3 +++ 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/common/constants.go b/common/constants.go index eaaca803..4b9df311 100644 --- a/common/constants.go +++ b/common/constants.go @@ -55,6 +55,8 @@ var EmailDomainWhitelist = []string{ "foxmail.com", } +var DebugEnabled = os.Getenv("DEBUG") == "true" + var LogConsumeEnabled = true var SMTPServer = "" diff --git a/controller/relay-text.go b/controller/relay-text.go index 65f03bcf..a6a276f3 100644 --- a/controller/relay-text.go +++ b/controller/relay-text.go @@ -5,13 +5,12 @@ 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" ) const ( @@ -308,6 +307,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { var textResponse TextResponse defer func() { + c.Writer.Flush() if consumeQuota { quota := 0 completionRatio := 1.0 diff --git a/main.go b/main.go index d6d0c75b..f4d20373 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,9 @@ func main() { if os.Getenv("GIN_MODE") != "debug" { gin.SetMode(gin.ReleaseMode) } + if common.DebugEnabled { + common.SysLog("running in debug mode") + } // Initialize SQL Database err := model.InitDB() if err != nil { diff --git a/model/main.go b/model/main.go index ddbc69aa..fcc134d8 100644 --- a/model/main.go +++ b/model/main.go @@ -57,6 +57,9 @@ func InitDB() (err error) { } common.SysLog("database connected") if err == nil { + if common.DebugEnabled { + db = db.Debug() + } DB = db sqlDB, err := DB.DB() if err != nil { From eae9b6e60779d02132c6caf7b5155c2c98f7cf4f Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 12 Aug 2023 18:25:18 +0800 Subject: [PATCH 50/73] fix: add lock when update quota (close #399) --- model/channel.go | 2 +- model/token.go | 4 ++-- model/user.go | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/model/channel.go b/model/channel.go index 7cc9fa9b..b0d6e644 100644 --- a/model/channel.go +++ b/model/channel.go @@ -141,7 +141,7 @@ func UpdateChannelStatusById(id int, status int) { } func UpdateChannelUsedQuota(id int, quota int) { - err := DB.Model(&Channel{}).Where("id = ?", id).Update("used_quota", gorm.Expr("used_quota + ?", quota)).Error + err := DB.Set("gorm:query_option", "FOR UPDATE").Model(&Channel{}).Where("id = ?", id).Update("used_quota", gorm.Expr("used_quota + ?", quota)).Error if err != nil { common.SysError("failed to update channel used quota: " + err.Error()) } diff --git a/model/token.go b/model/token.go index 7cd226c6..0e2395ad 100644 --- a/model/token.go +++ b/model/token.go @@ -131,7 +131,7 @@ func IncreaseTokenQuota(id int, quota int) (err error) { if quota < 0 { return errors.New("quota 不能为负数!") } - err = DB.Model(&Token{}).Where("id = ?", id).Updates( + err = DB.Set("gorm:query_option", "FOR UPDATE").Model(&Token{}).Where("id = ?", id).Updates( map[string]interface{}{ "remain_quota": gorm.Expr("remain_quota + ?", quota), "used_quota": gorm.Expr("used_quota - ?", quota), @@ -144,7 +144,7 @@ func DecreaseTokenQuota(id int, quota int) (err error) { if quota < 0 { return errors.New("quota 不能为负数!") } - err = DB.Model(&Token{}).Where("id = ?", id).Updates( + err = DB.Set("gorm:query_option", "FOR UPDATE").Model(&Token{}).Where("id = ?", id).Updates( map[string]interface{}{ "remain_quota": gorm.Expr("remain_quota - ?", quota), "used_quota": gorm.Expr("used_quota + ?", quota), diff --git a/model/user.go b/model/user.go index 7c771840..c7080450 100644 --- a/model/user.go +++ b/model/user.go @@ -275,7 +275,7 @@ func IncreaseUserQuota(id int, quota int) (err error) { if quota < 0 { return errors.New("quota 不能为负数!") } - err = DB.Model(&User{}).Where("id = ?", id).Update("quota", gorm.Expr("quota + ?", quota)).Error + err = DB.Set("gorm:query_option", "FOR UPDATE").Model(&User{}).Where("id = ?", id).Update("quota", gorm.Expr("quota + ?", quota)).Error return err } @@ -283,7 +283,7 @@ func DecreaseUserQuota(id int, quota int) (err error) { if quota < 0 { return errors.New("quota 不能为负数!") } - err = DB.Model(&User{}).Where("id = ?", id).Update("quota", gorm.Expr("quota - ?", quota)).Error + err = DB.Set("gorm:query_option", "FOR UPDATE").Model(&User{}).Where("id = ?", id).Update("quota", gorm.Expr("quota - ?", quota)).Error return err } @@ -293,7 +293,7 @@ func GetRootUserEmail() (email string) { } func UpdateUserUsedQuotaAndRequestCount(id int, quota int) { - err := DB.Model(&User{}).Where("id = ?", id).Updates( + err := DB.Set("gorm:query_option", "FOR UPDATE").Model(&User{}).Where("id = ?", id).Updates( map[string]interface{}{ "used_quota": gorm.Expr("used_quota + ?", quota), "request_count": gorm.Expr("request_count + ?", 1), From e0b4f96b5bce6e5b9526d0a51a5509507276306a Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 12 Aug 2023 19:20:12 +0800 Subject: [PATCH 51/73] feat: support PostgreSQL now --- README.md | 6 ++++-- go.mod | 6 +++++- go.sum | 10 ++++++++++ model/main.go | 41 +++++++++++++++++++++++++---------------- 4 files changed, 44 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 1f3a4a5e..a53c8b9d 100644 --- a/README.md +++ b/README.md @@ -276,8 +276,10 @@ graph LR + 例子:`REDIS_CONN_STRING=redis://default:redispw@localhost:49153` 2. `SESSION_SECRET`:设置之后将使用固定的会话密钥,这样系统重新启动后已登录用户的 cookie 将依旧有效。 + 例子:`SESSION_SECRET=random_string` -3. `SQL_DSN`:设置之后将使用指定数据库而非 SQLite,请使用 MySQL 8.0 版本。 - + 例子:`SQL_DSN=root:123456@tcp(localhost:3306)/oneapi` +3. `SQL_DSN`:设置之后将使用指定数据库而非 SQLite,请使用 MySQL 或 PostgreSQL。 + + 例子: + + MySQL:`SQL_DSN=root:123456@tcp(localhost:3306)/oneapi` + + PostgreSQL:`SQL_DSN=postgres://postgres:123456@localhost:5432/oneapi` + 注意需要提前建立数据库 `oneapi`,无需手动建表,程序将自动建表。 + 如果使用本地数据库:部署命令可添加 `--network="host"` 以使得容器内的程序可以访问到宿主机上的 MySQL。 + 如果使用云数据库:如果云服务器需要验证身份,需要在连接参数中添加 `?tls=skip-verify`。 diff --git a/go.mod b/go.mod index 1d08a7d3..79b01f93 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( golang.org/x/crypto v0.9.0 gorm.io/driver/mysql v1.4.3 gorm.io/driver/sqlite v1.4.3 - gorm.io/gorm v1.24.0 + gorm.io/gorm v1.25.0 ) require ( @@ -36,6 +36,9 @@ require ( github.com/gorilla/context v1.1.1 // indirect github.com/gorilla/securecookie v1.1.1 // indirect github.com/gorilla/sessions v1.2.1 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.3.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -54,4 +57,5 @@ require ( golang.org/x/text v0.9.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/postgres v1.5.2 // indirect ) diff --git a/go.sum b/go.sum index b4281cb6..810e7819 100644 --- a/go.sum +++ b/go.sum @@ -69,6 +69,12 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU= +github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= @@ -187,9 +193,13 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/mysql v1.4.3 h1:/JhWJhO2v17d8hjApTltKNADm7K7YI2ogkR7avJUL3k= gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c= +gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0= +gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8= gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= gorm.io/gorm v1.24.0 h1:j/CoiSm6xpRpmzbFJsQHYj+I8bGYWLXVHeYEyyKlF74= gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= +gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU= +gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/model/main.go b/model/main.go index fcc134d8..213db58c 100644 --- a/model/main.go +++ b/model/main.go @@ -2,10 +2,12 @@ package model import ( "gorm.io/driver/mysql" + "gorm.io/driver/postgres" "gorm.io/driver/sqlite" "gorm.io/gorm" "one-api/common" "os" + "strings" "time" ) @@ -34,28 +36,35 @@ func createRootAccountIfNeed() error { return nil } -func CountTable(tableName string) (num int64) { - DB.Table(tableName).Count(&num) - return -} - -func InitDB() (err error) { - var db *gorm.DB +func chooseDB() (*gorm.DB, error) { if os.Getenv("SQL_DSN") != "" { + dsn := os.Getenv("SQL_DSN") + if strings.HasPrefix(dsn, "postgres://") { + // Use PostgreSQL + common.SysLog("using PostgreSQL as database") + return gorm.Open(postgres.New(postgres.Config{ + DSN: dsn, + PreferSimpleProtocol: true, // disables implicit prepared statement usage + }), &gorm.Config{ + PrepareStmt: true, // precompile SQL + }) + } // Use MySQL common.SysLog("using MySQL as database") - db, err = gorm.Open(mysql.Open(os.Getenv("SQL_DSN")), &gorm.Config{ - PrepareStmt: true, // precompile SQL - }) - } else { - // Use SQLite - common.SysLog("SQL_DSN not set, using SQLite as database") - common.UsingSQLite = true - db, err = gorm.Open(sqlite.Open(common.SQLitePath), &gorm.Config{ + return gorm.Open(mysql.Open(dsn), &gorm.Config{ PrepareStmt: true, // precompile SQL }) } - common.SysLog("database connected") + // Use SQLite + common.SysLog("SQL_DSN not set, using SQLite as database") + common.UsingSQLite = true + return gorm.Open(sqlite.Open(common.SQLitePath), &gorm.Config{ + PrepareStmt: true, // precompile SQL + }) +} + +func InitDB() (err error) { + db, err := chooseDB() if err == nil { if common.DebugEnabled { db = db.Debug() From 0e9ff8825efc7668be6a2d0c7df9960d5121c26b Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 12 Aug 2023 19:36:31 +0800 Subject: [PATCH 52/73] perf: use a goroutine to handle quota post consumption (#364) --- controller/relay-text.go | 83 +++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/controller/relay-text.go b/controller/relay-text.go index a6a276f3..1bb463fa 100644 --- a/controller/relay-text.go +++ b/controller/relay-text.go @@ -305,51 +305,54 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { } var textResponse TextResponse + tokenName := c.GetString("token_name") + channelId := c.GetInt("channel_id") defer func() { - c.Writer.Flush() - if consumeQuota { - quota := 0 - completionRatio := 1.0 - if strings.HasPrefix(textRequest.Model, "gpt-3.5") { - completionRatio = 1.333333 - } - if strings.HasPrefix(textRequest.Model, "gpt-4") { - completionRatio = 2 - } + // c.Writer.Flush() + go func() { + if consumeQuota { + quota := 0 + completionRatio := 1.0 + if strings.HasPrefix(textRequest.Model, "gpt-3.5") { + completionRatio = 1.333333 + } + if strings.HasPrefix(textRequest.Model, "gpt-4") { + completionRatio = 2 + } - promptTokens = textResponse.Usage.PromptTokens - completionTokens = textResponse.Usage.CompletionTokens + promptTokens = textResponse.Usage.PromptTokens + completionTokens = textResponse.Usage.CompletionTokens - quota = promptTokens + int(float64(completionTokens)*completionRatio) - quota = int(float64(quota) * ratio) - if ratio != 0 && quota <= 0 { - quota = 1 + quota = promptTokens + int(float64(completionTokens)*completionRatio) + quota = int(float64(quota) * ratio) + if ratio != 0 && quota <= 0 { + quota = 1 + } + totalTokens := promptTokens + completionTokens + if totalTokens == 0 { + // in this case, must be some error happened + // we cannot just return, because we may have to return the pre-consumed quota + quota = 0 + } + quotaDelta := quota - preConsumedQuota + err := model.PostConsumeTokenQuota(tokenId, quotaDelta) + 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 { + logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio) + model.RecordConsumeLog(userId, promptTokens, completionTokens, textRequest.Model, tokenName, quota, logContent) + model.UpdateUserUsedQuotaAndRequestCount(userId, quota) + + model.UpdateChannelUsedQuota(channelId, quota) + } } - totalTokens := promptTokens + completionTokens - if totalTokens == 0 { - // in this case, must be some error happened - // we cannot just return, because we may have to return the pre-consumed quota - quota = 0 - } - quotaDelta := quota - preConsumedQuota - err := model.PostConsumeTokenQuota(tokenId, quotaDelta) - 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, promptTokens, completionTokens, textRequest.Model, tokenName, quota, logContent) - model.UpdateUserUsedQuotaAndRequestCount(userId, quota) - channelId := c.GetInt("channel_id") - model.UpdateChannelUsedQuota(channelId, quota) - } - } + }() }() switch apiType { case APITypeOpenAI: From ca512f6a38809f62a060391e838e750bc224be50 Mon Sep 17 00:00:00 2001 From: igophper <34326532+igophper@users.noreply.github.com> Date: Sat, 12 Aug 2023 21:55:18 +0800 Subject: [PATCH 53/73] refactor: add set event stream headers func (#402) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 封装流式传输响应头的设置 --- controller/relay-ali.go | 6 +----- controller/relay-baidu.go | 6 +----- controller/relay-claude.go | 6 +----- controller/relay-openai.go | 6 +----- controller/relay-palm.go | 6 +----- controller/relay-utils.go | 9 +++++++++ controller/relay-xunfei.go | 6 +----- controller/relay-zhipu.go | 6 +----- 8 files changed, 16 insertions(+), 35 deletions(-) diff --git a/controller/relay-ali.go b/controller/relay-ali.go index e94abd6a..014f6b84 100644 --- a/controller/relay-ali.go +++ b/controller/relay-ali.go @@ -166,11 +166,7 @@ func aliStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStat } stopChan <- true }() - c.Writer.Header().Set("Content-Type", "text/event-stream") - c.Writer.Header().Set("Cache-Control", "no-cache") - c.Writer.Header().Set("Connection", "keep-alive") - c.Writer.Header().Set("Transfer-Encoding", "chunked") - c.Writer.Header().Set("X-Accel-Buffering", "no") + setEventStreamHeaders(c) lastResponseText := "" c.Stream(func(w io.Writer) bool { select { diff --git a/controller/relay-baidu.go b/controller/relay-baidu.go index 664bbd11..118e87a6 100644 --- a/controller/relay-baidu.go +++ b/controller/relay-baidu.go @@ -191,11 +191,7 @@ func baiduStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithSt } stopChan <- true }() - c.Writer.Header().Set("Content-Type", "text/event-stream") - c.Writer.Header().Set("Cache-Control", "no-cache") - c.Writer.Header().Set("Connection", "keep-alive") - c.Writer.Header().Set("Transfer-Encoding", "chunked") - c.Writer.Header().Set("X-Accel-Buffering", "no") + setEventStreamHeaders(c) c.Stream(func(w io.Writer) bool { select { case data := <-dataChan: diff --git a/controller/relay-claude.go b/controller/relay-claude.go index 052e5605..1f4a3e7b 100644 --- a/controller/relay-claude.go +++ b/controller/relay-claude.go @@ -141,11 +141,7 @@ func claudeStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithS } stopChan <- true }() - c.Writer.Header().Set("Content-Type", "text/event-stream") - c.Writer.Header().Set("Cache-Control", "no-cache") - c.Writer.Header().Set("Connection", "keep-alive") - c.Writer.Header().Set("Transfer-Encoding", "chunked") - c.Writer.Header().Set("X-Accel-Buffering", "no") + setEventStreamHeaders(c) c.Stream(func(w io.Writer) bool { select { case data := <-dataChan: diff --git a/controller/relay-openai.go b/controller/relay-openai.go index 298dbe95..6bdfbc08 100644 --- a/controller/relay-openai.go +++ b/controller/relay-openai.go @@ -66,11 +66,7 @@ func openaiStreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*O } stopChan <- true }() - c.Writer.Header().Set("Content-Type", "text/event-stream") - c.Writer.Header().Set("Cache-Control", "no-cache") - c.Writer.Header().Set("Connection", "keep-alive") - c.Writer.Header().Set("Transfer-Encoding", "chunked") - c.Writer.Header().Set("X-Accel-Buffering", "no") + setEventStreamHeaders(c) c.Stream(func(w io.Writer) bool { select { case data := <-dataChan: diff --git a/controller/relay-palm.go b/controller/relay-palm.go index 0053c9b8..a705b318 100644 --- a/controller/relay-palm.go +++ b/controller/relay-palm.go @@ -143,11 +143,7 @@ func palmStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithSta dataChan <- string(jsonResponse) stopChan <- true }() - c.Writer.Header().Set("Content-Type", "text/event-stream") - c.Writer.Header().Set("Cache-Control", "no-cache") - c.Writer.Header().Set("Connection", "keep-alive") - c.Writer.Header().Set("Transfer-Encoding", "chunked") - c.Writer.Header().Set("X-Accel-Buffering", "no") + setEventStreamHeaders(c) c.Stream(func(w io.Writer) bool { select { case data := <-dataChan: diff --git a/controller/relay-utils.go b/controller/relay-utils.go index 3695e119..5b3e0274 100644 --- a/controller/relay-utils.go +++ b/controller/relay-utils.go @@ -2,6 +2,7 @@ package controller import ( "fmt" + "github.com/gin-gonic/gin" "github.com/pkoukk/tiktoken-go" "one-api/common" ) @@ -106,3 +107,11 @@ func shouldDisableChannel(err *OpenAIError) bool { } return false } + +func setEventStreamHeaders(c *gin.Context) { + c.Writer.Header().Set("Content-Type", "text/event-stream") + c.Writer.Header().Set("Cache-Control", "no-cache") + c.Writer.Header().Set("Connection", "keep-alive") + c.Writer.Header().Set("Transfer-Encoding", "chunked") + c.Writer.Header().Set("X-Accel-Buffering", "no") +} diff --git a/controller/relay-xunfei.go b/controller/relay-xunfei.go index 48472456..87037e34 100644 --- a/controller/relay-xunfei.go +++ b/controller/relay-xunfei.go @@ -217,11 +217,7 @@ func xunfeiStreamHandler(c *gin.Context, textRequest GeneralOpenAIRequest, appId } stopChan <- true }() - c.Writer.Header().Set("Content-Type", "text/event-stream") - c.Writer.Header().Set("Cache-Control", "no-cache") - c.Writer.Header().Set("Connection", "keep-alive") - c.Writer.Header().Set("Transfer-Encoding", "chunked") - c.Writer.Header().Set("X-Accel-Buffering", "no") + setEventStreamHeaders(c) c.Stream(func(w io.Writer) bool { select { case xunfeiResponse := <-dataChan: diff --git a/controller/relay-zhipu.go b/controller/relay-zhipu.go index b125f1e7..7a4a582d 100644 --- a/controller/relay-zhipu.go +++ b/controller/relay-zhipu.go @@ -224,11 +224,7 @@ func zhipuStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithSt } stopChan <- true }() - c.Writer.Header().Set("Content-Type", "text/event-stream") - c.Writer.Header().Set("Cache-Control", "no-cache") - c.Writer.Header().Set("Connection", "keep-alive") - c.Writer.Header().Set("Transfer-Encoding", "chunked") - c.Writer.Header().Set("X-Accel-Buffering", "no") + setEventStreamHeaders(c) c.Stream(func(w io.Writer) bool { select { case data := <-dataChan: From af20063a8ddd4f4103f8d58f7ae2f1ee9568defd Mon Sep 17 00:00:00 2001 From: igophper <34326532+igophper@users.noreply.github.com> Date: Sat, 12 Aug 2023 23:41:44 +0800 Subject: [PATCH 54/73] feat: able to refresh baidu access token automatically (#400, close #401) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat:baidu channel support apiKey and secretKey 添加百度文心渠道时支持填写secretKey|apiKey或者accessToken,支持自动刷新accessToken * fix * fix * fix * chore: update implementation --------- Co-authored-by: JustSong Co-authored-by: JustSong <39998050+songquanpeng@users.noreply.github.com> --- controller/relay-baidu.go | 79 +++++++++++++++++++++++++--- controller/relay-text.go | 11 +++- i18n/en.json | 3 +- web/src/pages/Channel/EditChannel.js | 2 +- 4 files changed, 86 insertions(+), 9 deletions(-) diff --git a/controller/relay-baidu.go b/controller/relay-baidu.go index 118e87a6..d66391bc 100644 --- a/controller/relay-baidu.go +++ b/controller/relay-baidu.go @@ -3,22 +3,22 @@ package controller import ( "bufio" "encoding/json" + "errors" + "fmt" "github.com/gin-gonic/gin" "io" "net/http" "one-api/common" "strings" + "sync" + "time" ) // https://cloud.baidu.com/doc/WENXINWORKSHOP/s/flfmc9do2 type BaiduTokenResponse struct { - RefreshToken string `json:"refresh_token"` - ExpiresIn int `json:"expires_in"` - SessionKey string `json:"session_key"` - AccessToken string `json:"access_token"` - Scope string `json:"scope"` - SessionSecret string `json:"session_secret"` + ExpiresIn int `json:"expires_in"` + AccessToken string `json:"access_token"` } type BaiduMessage struct { @@ -73,6 +73,16 @@ type BaiduEmbeddingResponse struct { BaiduError } +type BaiduAccessToken struct { + AccessToken string `json:"access_token"` + Error string `json:"error,omitempty"` + ErrorDescription string `json:"error_description,omitempty"` + ExpiresIn int64 `json:"expires_in,omitempty"` + ExpiresAt time.Time `json:"-"` +} + +var baiduTokenStore sync.Map + func requestOpenAI2Baidu(request GeneralOpenAIRequest) *BaiduChatRequest { messages := make([]BaiduMessage, 0, len(request.Messages)) for _, message := range request.Messages { @@ -295,3 +305,60 @@ func baiduEmbeddingHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWit _, err = c.Writer.Write(jsonResponse) return nil, &fullTextResponse.Usage } + +func getBaiduAccessToken(apiKey string) (string, error) { + if val, ok := baiduTokenStore.Load(apiKey); ok { + var accessToken BaiduAccessToken + if accessToken, ok = val.(BaiduAccessToken); ok { + // soon this will expire + if time.Now().Add(time.Hour).After(accessToken.ExpiresAt) { + go func() { + _, _ = getBaiduAccessTokenHelper(apiKey) + }() + } + return accessToken.AccessToken, nil + } + } + accessToken, err := getBaiduAccessTokenHelper(apiKey) + if err != nil { + return "", err + } + if accessToken == nil { + return "", errors.New("getBaiduAccessToken return a nil token") + } + return (*accessToken).AccessToken, nil +} + +func getBaiduAccessTokenHelper(apiKey string) (*BaiduAccessToken, error) { + parts := strings.Split(apiKey, "|") + if len(parts) != 2 { + return nil, errors.New("invalid baidu apikey") + } + req, err := http.NewRequest("POST", fmt.Sprintf("https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=%s&client_secret=%s", + parts[0], parts[1]), nil) + if err != nil { + return nil, err + } + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Accept", "application/json") + res, err := impatientHTTPClient.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + var accessToken BaiduAccessToken + err = json.NewDecoder(res.Body).Decode(&accessToken) + if err != nil { + return nil, err + } + if accessToken.Error != "" { + return nil, errors.New(accessToken.Error + ": " + accessToken.ErrorDescription) + } + if accessToken.AccessToken == "" { + return nil, errors.New("getBaiduAccessTokenHelper get empty access token") + } + accessToken.ExpiresAt = time.Now().Add(time.Duration(accessToken.ExpiresIn) * time.Second) + baiduTokenStore.Store(apiKey, accessToken) + return &accessToken, nil +} diff --git a/controller/relay-text.go b/controller/relay-text.go index 1bb463fa..e8dab514 100644 --- a/controller/relay-text.go +++ b/controller/relay-text.go @@ -11,6 +11,7 @@ import ( "one-api/common" "one-api/model" "strings" + "time" ) const ( @@ -24,9 +25,13 @@ const ( ) var httpClient *http.Client +var impatientHTTPClient *http.Client func init() { httpClient = &http.Client{} + impatientHTTPClient = &http.Client{ + Timeout: 5 * time.Second, + } } func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { @@ -145,7 +150,11 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { } apiKey := c.Request.Header.Get("Authorization") apiKey = strings.TrimPrefix(apiKey, "Bearer ") - fullRequestURL += "?access_token=" + apiKey // TODO: access token expire in 30 days + var err error + if apiKey, err = getBaiduAccessToken(apiKey); err != nil { + return errorWrapper(err, "invalid_baidu_config", http.StatusInternalServerError) + } + fullRequestURL += "?access_token=" + apiKey case APITypePaLM: fullRequestURL = "https://generativelanguage.googleapis.com/v1beta2/models/chat-bison-001:generateMessage" if baseURL != "" { diff --git a/i18n/en.json b/i18n/en.json index 67ce8a56..a9402419 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -519,5 +519,6 @@ "令牌创建成功,请在列表页面点击复制获取令牌!": "Token created successfully, please click copy on the list page to get the token!", "代理": "Proxy", "此项可选,用于通过代理站来进行 API 调用,请输入代理站地址,格式为:https://domain.com": "This is optional, used to make API calls through the proxy site, please enter the proxy site address, the format is: https://domain.com", - "取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?": "Canceling password login will cause all users (including administrators) who have not bound other login methods to be unable to log in via password, confirm cancel?" + "取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?": "Canceling password login will cause all users (including administrators) who have not bound other login methods to be unable to log in via password, confirm cancel?", + "按照如下格式输入:": "Enter in the following format:" } diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index 0d7a4a01..b5fb524e 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -355,7 +355,7 @@ const EditChannel = () => { label='密钥' name='key' required - placeholder={inputs.type === 15 ? '请输入 access token,当前版本暂不支持自动刷新,请每 30 天更新一次' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')} + placeholder={inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')} onChange={handleInputChange} value={inputs.key} autoComplete='new-password' From 4ef5e2020ceecc534073d7e045197f7be05233ce Mon Sep 17 00:00:00 2001 From: igophper <34326532+igophper@users.noreply.github.com> Date: Sun, 13 Aug 2023 00:45:04 +0800 Subject: [PATCH 55/73] fix: fix baidu's embedding api (#398) * fix:judge baidu embeddings input type * chore: add back update all channels balance * chore: update default configuration for sql connection --------- Co-authored-by: JustSong --- README.md | 4 ++-- controller/relay-baidu.go | 8 ++++++-- model/main.go | 4 ++-- web/src/components/ChannelsTable.js | 6 +++--- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a53c8b9d..29f45e95 100644 --- a/README.md +++ b/README.md @@ -284,8 +284,8 @@ graph LR + 如果使用本地数据库:部署命令可添加 `--network="host"` 以使得容器内的程序可以访问到宿主机上的 MySQL。 + 如果使用云数据库:如果云服务器需要验证身份,需要在连接参数中添加 `?tls=skip-verify`。 + 请根据你的数据库配置修改下列参数(或者保持默认值): - + `SQL_MAX_IDLE_CONNS`:最大空闲连接数,默认为 `10`。 - + `SQL_MAX_OPEN_CONNS`:最大打开连接数,默认为 `100`。 + + `SQL_MAX_IDLE_CONNS`:最大空闲连接数,默认为 `100`。 + + `SQL_MAX_OPEN_CONNS`:最大打开连接数,默认为 `1000`。 + 如果报错 `Error 1040: Too many connections`,请适当减小该值。 + `SQL_CONN_MAX_LIFETIME`:连接的最大生命周期,默认为 `60`,单位分钟。 4. `FRONTEND_BASE_URL`:设置之后将重定向页面请求到指定的地址,仅限从服务器设置。 diff --git a/controller/relay-baidu.go b/controller/relay-baidu.go index d66391bc..78b4c226 100644 --- a/controller/relay-baidu.go +++ b/controller/relay-baidu.go @@ -150,8 +150,12 @@ func embeddingRequestOpenAI2Baidu(request GeneralOpenAIRequest) *BaiduEmbeddingR switch request.Input.(type) { case string: baiduEmbeddingRequest.Input = []string{request.Input.(string)} - case []string: - baiduEmbeddingRequest.Input = request.Input.([]string) + case []any: + for _, item := range request.Input.([]any) { + if str, isStr := item.(string); isStr { + baiduEmbeddingRequest.Input = append(baiduEmbeddingRequest.Input, str) + } + } } return &baiduEmbeddingRequest } diff --git a/model/main.go b/model/main.go index 213db58c..d422c4e0 100644 --- a/model/main.go +++ b/model/main.go @@ -74,8 +74,8 @@ func InitDB() (err error) { if err != nil { return err } - sqlDB.SetMaxIdleConns(common.GetOrDefault("SQL_MAX_IDLE_CONNS", 10)) - sqlDB.SetMaxOpenConns(common.GetOrDefault("SQL_MAX_OPEN_CONNS", 100)) + sqlDB.SetMaxIdleConns(common.GetOrDefault("SQL_MAX_IDLE_CONNS", 100)) + sqlDB.SetMaxOpenConns(common.GetOrDefault("SQL_MAX_OPEN_CONNS", 1000)) sqlDB.SetConnMaxLifetime(time.Second * time.Duration(common.GetOrDefault("SQL_MAX_LIFETIME", 60))) if !common.IsMasterNode { diff --git a/web/src/components/ChannelsTable.js b/web/src/components/ChannelsTable.js index 072f5b90..5eb39783 100644 --- a/web/src/components/ChannelsTable.js +++ b/web/src/components/ChannelsTable.js @@ -368,7 +368,7 @@ const ChannelsTable = () => { }} style={{ cursor: 'pointer' }}> {renderBalance(channel.type, channel.balance)} } - content="点击更新" + content='点击更新' basic /> @@ -447,8 +447,8 @@ const ChannelsTable = () => { - {/* */} + Date: Sun, 13 Aug 2023 00:50:26 +0800 Subject: [PATCH 56/73] chore: update variable name --- controller/relay-baidu.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/relay-baidu.go b/controller/relay-baidu.go index 78b4c226..ad20d6d6 100644 --- a/controller/relay-baidu.go +++ b/controller/relay-baidu.go @@ -152,7 +152,7 @@ func embeddingRequestOpenAI2Baidu(request GeneralOpenAIRequest) *BaiduEmbeddingR baiduEmbeddingRequest.Input = []string{request.Input.(string)} case []any: for _, item := range request.Input.([]any) { - if str, isStr := item.(string); isStr { + if str, ok := item.(string); ok { baiduEmbeddingRequest.Input = append(baiduEmbeddingRequest.Input, str) } } From cac61b9f66b04d70c5a9f3b90de1641fb213336a Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 13 Aug 2023 00:51:48 +0800 Subject: [PATCH 57/73] Revert "fix: add lock when update quota (close #399)" This reverts commit eae9b6e60779d02132c6caf7b5155c2c98f7cf4f. --- model/channel.go | 2 +- model/token.go | 4 ++-- model/user.go | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/model/channel.go b/model/channel.go index b0d6e644..7cc9fa9b 100644 --- a/model/channel.go +++ b/model/channel.go @@ -141,7 +141,7 @@ func UpdateChannelStatusById(id int, status int) { } func UpdateChannelUsedQuota(id int, quota int) { - err := DB.Set("gorm:query_option", "FOR UPDATE").Model(&Channel{}).Where("id = ?", id).Update("used_quota", gorm.Expr("used_quota + ?", quota)).Error + err := DB.Model(&Channel{}).Where("id = ?", id).Update("used_quota", gorm.Expr("used_quota + ?", quota)).Error if err != nil { common.SysError("failed to update channel used quota: " + err.Error()) } diff --git a/model/token.go b/model/token.go index 0e2395ad..7cd226c6 100644 --- a/model/token.go +++ b/model/token.go @@ -131,7 +131,7 @@ func IncreaseTokenQuota(id int, quota int) (err error) { if quota < 0 { return errors.New("quota 不能为负数!") } - err = DB.Set("gorm:query_option", "FOR UPDATE").Model(&Token{}).Where("id = ?", id).Updates( + err = DB.Model(&Token{}).Where("id = ?", id).Updates( map[string]interface{}{ "remain_quota": gorm.Expr("remain_quota + ?", quota), "used_quota": gorm.Expr("used_quota - ?", quota), @@ -144,7 +144,7 @@ func DecreaseTokenQuota(id int, quota int) (err error) { if quota < 0 { return errors.New("quota 不能为负数!") } - err = DB.Set("gorm:query_option", "FOR UPDATE").Model(&Token{}).Where("id = ?", id).Updates( + err = DB.Model(&Token{}).Where("id = ?", id).Updates( map[string]interface{}{ "remain_quota": gorm.Expr("remain_quota - ?", quota), "used_quota": gorm.Expr("used_quota + ?", quota), diff --git a/model/user.go b/model/user.go index c7080450..7c771840 100644 --- a/model/user.go +++ b/model/user.go @@ -275,7 +275,7 @@ func IncreaseUserQuota(id int, quota int) (err error) { if quota < 0 { return errors.New("quota 不能为负数!") } - err = DB.Set("gorm:query_option", "FOR UPDATE").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 } @@ -283,7 +283,7 @@ func DecreaseUserQuota(id int, quota int) (err error) { if quota < 0 { return errors.New("quota 不能为负数!") } - err = DB.Set("gorm:query_option", "FOR UPDATE").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 } @@ -293,7 +293,7 @@ func GetRootUserEmail() (email string) { } func UpdateUserUsedQuotaAndRequestCount(id int, quota int) { - err := DB.Set("gorm:query_option", "FOR UPDATE").Model(&User{}).Where("id = ?", id).Updates( + err := DB.Model(&User{}).Where("id = ?", id).Updates( map[string]interface{}{ "used_quota": gorm.Expr("used_quota + ?", quota), "request_count": gorm.Expr("request_count + ?", 1), From da1d81998fd5fe7389176f52c8b8c0f93b20f7d6 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 13 Aug 2023 01:16:27 +0800 Subject: [PATCH 58/73] docs: update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 29f45e95..e2979961 100644 --- a/README.md +++ b/README.md @@ -279,7 +279,7 @@ graph LR 3. `SQL_DSN`:设置之后将使用指定数据库而非 SQLite,请使用 MySQL 或 PostgreSQL。 + 例子: + MySQL:`SQL_DSN=root:123456@tcp(localhost:3306)/oneapi` - + PostgreSQL:`SQL_DSN=postgres://postgres:123456@localhost:5432/oneapi` + + PostgreSQL:`SQL_DSN=postgres://postgres:123456@localhost:5432/oneapi`(适配中,欢迎反馈) + 注意需要提前建立数据库 `oneapi`,无需手动建表,程序将自动建表。 + 如果使用本地数据库:部署命令可添加 `--network="host"` 以使得容器内的程序可以访问到宿主机上的 MySQL。 + 如果使用云数据库:如果云服务器需要验证身份,需要在连接参数中添加 `?tls=skip-verify`。 From e4bacc45d62318735ecad7262dae7533c26b3b25 Mon Sep 17 00:00:00 2001 From: Jungley Date: Sun, 13 Aug 2023 22:55:43 +0800 Subject: [PATCH 59/73] ci: add stage caching to Dockerfile (#408) --- Dockerfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 22055553..ffb8c21b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,10 @@ FROM node:16 as builder WORKDIR /build +COPY web/package.json . +RUN npm install COPY ./web . COPY ./VERSION . -RUN npm install RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build FROM golang AS builder2 @@ -13,9 +14,10 @@ ENV GO111MODULE=on \ GOOS=linux WORKDIR /build +ADD go.mod go.sum ./ +RUN go mod download COPY . . COPY --from=builder /build/build ./web/build -RUN go mod download RUN go build -ldflags "-s -w -X 'one-api/common.Version=$(cat VERSION)' -extldflags '-static'" -o one-api FROM alpine From 90b4cac7f352fcbb9771ba5cd3ece6b5e105bace Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Wed, 16 Aug 2023 20:33:31 +0900 Subject: [PATCH 60/73] docs: add Japanese README (#425) --- README.en.md | 2 +- README.ja.md | 298 +++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 2 +- 3 files changed, 300 insertions(+), 2 deletions(-) create mode 100644 README.ja.md diff --git a/README.en.md b/README.en.md index 8a041da8..783c140c 100644 --- a/README.en.md +++ b/README.en.md @@ -1,5 +1,5 @@

- 中文 | English + 中文 | English | 日本語

diff --git a/README.ja.md b/README.ja.md new file mode 100644 index 00000000..8a852935 --- /dev/null +++ b/README.ja.md @@ -0,0 +1,298 @@ +

+ 中文 | English | 日本語 +

+ +

+ one-api logo +

+ +
+ +# One API + +_✨ 標準的な OpenAI API フォーマットを通じてすべての LLM にアクセスでき、導入と利用が容易です ✨_ + +
+ +

+ + license + + + release + + + docker pull + + + release + + + GoReportCard + +

+ +

+ デプロイチュートリアル + · + 使用方法 + · + フィードバック + · + スクリーンショット + · + ライブデモ + · + FAQ + · + 関連プロジェクト + · + 寄付 +

+ +> **警告**: この README は ChatGPT によって翻訳されています。翻訳ミスを発見した場合は遠慮なく PR を投稿してください。 + +> **警告**: 英語版の Docker イメージは `justsong/one-api-ja` です。 + +> **注**: Docker からプルされた最新のイメージは、`alpha` リリースかもしれません。安定性が必要な場合は、手動でバージョンを指定してください。 + +## 特徴 +1. 複数の大型モデルをサポート: + + [x] [OpenAI ChatGPT シリーズモデル](https://platform.openai.com/docs/guides/gpt/chat-completions-api) ([Azure OpenAI API](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference) をサポート) + + [x] [Anthropic Claude シリーズモデル](https://anthropic.com) + + [x] [Google PaLM2 シリーズモデル](https://developers.generativeai.google) + + [x] [Baidu Wenxin Yiyuan シリーズモデル](https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html) + + [x] [Alibaba Tongyi Qianwen シリーズモデル](https://help.aliyun.com/document_detail/2400395.html) + + [x] [Zhipu ChatGLM シリーズモデル](https://bigmodel.cn) +2. **ロードバランシング**による複数チャンネルへのアクセスをサポート。 +3. ストリーム伝送によるタイプライター的効果を可能にする**ストリームモード**に対応。 +4. **マルチマシンデプロイ**に対応。[詳細はこちら](#multi-machine-deployment)を参照。 +5. トークンの有効期限や使用回数を設定できる**トークン管理**に対応しています。 +6. **バウチャー管理**に対応しており、バウチャーの一括生成やエクスポートが可能です。バウチャーは口座残高の補充に利用できます。 +7. **チャンネル管理**に対応し、チャンネルの一括作成が可能。 +8. グループごとに異なるレートを設定するための**ユーザーグループ**と**チャンネルグループ**をサポートしています。 +9. チャンネル**モデルリスト設定**に対応。 +10. **クォータ詳細チェック**をサポート。 +11. **ユーザー招待報酬**をサポートします。 +12. 米ドルでの残高表示が可能。 +13. 新規ユーザー向けのお知らせ公開、リチャージリンク設定、初期残高設定に対応。 +14. 豊富な**カスタマイズ**オプションを提供します: + 1. システム名、ロゴ、フッターのカスタマイズが可能。 + 2. HTML と Markdown コードを使用したホームページとアバウトページのカスタマイズ、または iframe を介したスタンドアロンウェブページの埋め込みをサポートしています。 +15. システム・アクセストークンによる管理 API アクセスをサポートする。 +16. Cloudflare Turnstile によるユーザー認証に対応。 +17. ユーザー管理と複数のユーザーログイン/登録方法をサポート: + + 電子メールによるログイン/登録とパスワードリセット。 + + [GitHub OAuth](https://github.com/settings/applications/new)。 + + WeChat 公式アカウントの認証([WeChat Server](https://github.com/songquanpeng/wechat-server)の追加導入が必要)。 +18. 他の主要なモデル API が利用可能になった場合、即座にサポートし、カプセル化する。 + +## デプロイメント +### Docker デプロイメント +デプロイコマンド: `docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api-ja`。 + +コマンドを更新する: `docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrr/watchtower -cR`。 + +`-p 3000:3000` の最初の `3000` はホストのポートで、必要に応じて変更できます。 + +データはホストの `/home/ubuntu/data/one-api` ディレクトリに保存される。このディレクトリが存在し、書き込み権限があることを確認する、もしくは適切なディレクトリに変更してください。 + +Nginxリファレンス設定: +``` +server{ + server_name openai.justsong.cn; # ドメイン名は適宜変更 + + location / { + client_max_body_size 64m; + proxy_http_version 1.1; + proxy_pass http://localhost:3000; # それに応じてポートを変更 + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_cache_bypass $http_upgrade; + proxy_set_header Accept-Encoding gzip; + proxy_read_timeout 300s; # GPT-4 はより長いタイムアウトが必要 + } +} +``` + +次に、Let's Encrypt certbot を使って HTTPS を設定します: +```bash +# Ubuntu に certbot をインストール: +sudo snap install --classic certbot +sudo ln -s /snap/bin/certbot /usr/bin/certbot +# 証明書の生成と Nginx 設定の変更 +sudo certbot --nginx +# プロンプトに従う +# Nginx を再起動 +sudo service nginx restart +``` + +初期アカウントのユーザー名は `root` で、パスワードは `123456` です。 + +### マニュアルデプロイ +1. [GitHub Releases](https://github.com/songquanpeng/one-api/releases/latest) から実行ファイルをダウンロードする、もしくはソースからコンパイルする: + ```shell + git clone https://github.com/songquanpeng/one-api.git + + # フロントエンドのビルド + cd one-api/web + npm install + npm run build + + # バックエンドのビルド + cd .. + go mod download + go build -ldflags "-s -w" -o one-api + ``` +2. 実行: + ```shell + chmod u+x one-api + ./one-api --port 3000 --log-dir ./logs + ``` +3. [http://localhost:3000/](http://localhost:3000/) にアクセスし、ログインする。初期アカウントのユーザー名は `root`、パスワードは `123456` である。 + +より詳細なデプロイのチュートリアルについては、[このページ](https://iamazing.cn/page/how-to-deploy-a-website) を参照してください。 + +### マルチマシンデプロイ +1. すべてのサーバに同じ `SESSION_SECRET` を設定する。 +2. `SQL_DSN` を設定し、SQLite の代わりに MySQL を使用する。すべてのサーバは同じデータベースに接続する。 +3. マスターノード以外のノードの `NODE_TYPE` を `slave` に設定する。 +4. データベースから定期的に設定を同期するサーバーには `SYNC_FREQUENCY` を設定する。 +5. マスター以外のノードでは、オプションで `FRONTEND_BASE_URL` を設定して、ページ要求をマスターサーバーにリダイレクトすることができます。 +6. マスター以外のノードには Redis を個別にインストールし、`REDIS_CONN_STRING` を設定して、キャッシュの有効期限が切れていないときにデータベースにゼロレイテンシーでアクセスできるようにする。 +7. メインサーバーでもデータベースへのアクセスが高レイテンシになる場合は、Redis を有効にし、`SYNC_FREQUENCY` を設定してデータベースから定期的に設定を同期する必要がある。 + +Please refer to the [environment variables](#environment-variables) section for details on using environment variables. + +### コントロールパネル(例: Baota)への展開 +詳しい手順は [#175](https://github.com/songquanpeng/one-api/issues/175) を参照してください。 + +配置後に空白のページが表示される場合は、[#97](https://github.com/songquanpeng/one-api/issues/97) を参照してください。 + +### サードパーティプラットフォームへのデプロイ +
+Sealos へのデプロイ +
+ +> Sealos は、高い同時実行性、ダイナミックなスケーリング、数百万人のユーザーに対する安定した運用をサポートしています。 + +> 下のボタンをクリックすると、ワンクリックで展開できます。👇 + +[![](https://raw.githubusercontent.com/labring-actions/templates/main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-fastdeploy?templateName=one-api) + + +
+
+ +
+Zeabur へのデプロイ +
+ +> Zeabur のサーバーは海外にあるため、ネットワークの問題は自動的に解決されます。 + +1. まず、コードをフォークする。 +2. [Zeabur](https://zeabur.com?referralCode=songquanpeng) にアクセスしてログインし、コンソールに入る。 +3. 新しいプロジェクトを作成します。Service -> Add ServiceでMarketplace を選択し、MySQL を選択する。接続パラメータ(ユーザー名、パスワード、アドレス、ポート)をメモします。 +4. 接続パラメータをコピーし、```create database `one-api` ``` を実行してデータベースを作成する。 +5. その後、Service -> Add Service で Git を選択し(最初の使用には認証が必要です)、フォークしたリポジトリを選択します。 +6. 自動デプロイが開始されますが、一旦キャンセルしてください。Variable タブで `PORT` に `3000` を追加し、`SQL_DSN` に `:@tcp(:)/one-api` を追加します。変更を保存する。SQL_DSN` が設定されていないと、データが永続化されず、再デプロイ後にデータが失われるので注意すること。 +7. 再デプロイを選択します。 +8. Domains タブで、"my-one-api" のような適切なドメイン名の接頭辞を選択する。最終的なドメイン名は "my-one-api.zeabur.app" となります。独自のドメイン名を CNAME することもできます。 +9. デプロイが完了するのを待ち、生成されたドメイン名をクリックして One API にアクセスします。 + +
+
+ +## コンフィグ +システムは箱から出してすぐに使えます。 + +環境変数やコマンドラインパラメータを設定することで、システムを構成することができます。 + +システム起動後、`root` ユーザーとしてログインし、さらにシステムを設定します。 + +## 使用方法 +`Channels` ページで API Key を追加し、`Tokens` ページでアクセストークンを追加する。 + +アクセストークンを使って One API にアクセスすることができる。使い方は [OpenAI API](https://platform.openai.com/docs/api-reference/introduction) と同じです。 + +OpenAI API が使用されている場所では、API Base に One API のデプロイアドレスを設定することを忘れないでください(例: `https://openai.justsong.cn`)。API Key は One API で生成されたトークンでなければなりません。 + +具体的な API Base のフォーマットは、使用しているクライアントに依存することに注意してください。 + +```mermaid +graph LR + A(ユーザ) + A --->|リクエスト| B(One API) + B -->|中継リクエスト| C(OpenAI) + B -->|中継リクエスト| D(Azure) + B -->|中継リクエスト| E(その他のダウンストリームチャンネル) +``` + +現在のリクエストにどのチャネルを使うかを指定するには、トークンの後に チャネル ID を追加します: 例えば、`Authorization: Bearer ONE_API_KEY-CHANNEL_ID` のようにします。 +チャンネル ID を指定するためには、トークンは管理者によって作成される必要があることに注意してください。 + +もしチャネル ID が指定されない場合、ロードバランシングによってリクエストが複数のチャネルに振り分けられます。 + +### 環境変数 +1. `REDIS_CONN_STRING`: 設定すると、リクエストレート制限のためのストレージとして、メモリの代わりに Redis が使われる。 + + 例: `REDIS_CONN_STRING=redis://default:redispw@localhost:49153` +2. `SESSION_SECRET`: 設定すると、固定セッションキーが使用され、システムの再起動後もログインユーザーのクッキーが有効であることが保証されます。 + + 例: `SESSION_SECRET=random_string` +3. `SQL_DSN`: 設定すると、SQLite の代わりに指定したデータベースが使用されます。MySQL バージョン 8.0 を使用してください。 + + 例: `SQL_DSN=root:123456@tcp(localhost:3306)/oneapi` +4. `FRONTEND_BASE_URL`: 設定されると、バックエンドアドレスではなく、指定されたフロントエンドアドレスが使われる。 + + 例: `FRONTEND_BASE_URL=https://openai.justsong.cn` +5. `SYNC_FREQUENCY`: 設定された場合、システムは定期的にデータベースからコンフィグを秒単位で同期する。設定されていない場合、同期は行われません。 + + 例: `SYNC_FREQUENCY=60` +6. `NODE_TYPE`: 設定すると、ノードのタイプを指定する。有効な値は `master` と `slave` である。設定されていない場合、デフォルトは `master`。 + + 例: `NODE_TYPE=slave` +7. `CHANNEL_UPDATE_FREQUENCY`: 設定すると、チャンネル残高を分単位で定期的に更新する。設定されていない場合、更新は行われません。 + + 例: `CHANNEL_UPDATE_FREQUENCY=1440` +8. `CHANNEL_TEST_FREQUENCY`: 設定すると、チャンネルを定期的にテストする。設定されていない場合、テストは行われません。 + + 例: `CHANNEL_TEST_FREQUENCY=1440` +9. `POLLING_INTERVAL`: チャネル残高の更新とチャネルの可用性をテストするときのリクエスト間の時間間隔 (秒)。デフォルトは間隔なし。 + + 例: `POLLING_INTERVAL=5` + +### コマンドラインパラメータ +1. `--port `: サーバがリッスンするポート番号を指定。デフォルトは `3000` です。 + + 例: `--port 3000` +2. `--log-dir `: ログディレクトリを指定。設定しない場合、ログは保存されません。 + + 例: `--log-dir ./logs` +3. `--version`: システムのバージョン番号を表示して終了する。 +4. `--help`: コマンドの使用法ヘルプとパラメータの説明を表示。 + +## スクリーンショット +![channel](https://user-images.githubusercontent.com/39998050/233837954-ae6683aa-5c4f-429f-a949-6645a83c9490.png) +![token](https://user-images.githubusercontent.com/39998050/233837971-dab488b7-6d96-43af-b640-a168e8d1c9bf.png) + +## FAQ +1. ノルマとは何か?どのように計算されますか?One API にはノルマ計算の問題はありますか? + + ノルマ = グループ倍率 * モデル倍率 * (プロンプトトークンの数 + 完了トークンの数 * 完了倍率) + + 完了倍率は、公式の定義と一致するように、GPT3.5 では 1.33、GPT4 では 2 に固定されています。 + + ストリームモードでない場合、公式 API は消費したトークンの総数を返す。ただし、プロンプトとコンプリートの消費倍率は異なるので注意してください。 +2. アカウント残高は十分なのに、"insufficient quota" と表示されるのはなぜですか? + + トークンのクォータが十分かどうかご確認ください。トークンクォータはアカウント残高とは別のものです。 + + トークンクォータは最大使用量を設定するためのもので、ユーザーが自由に設定できます。 +3. チャンネルを使おうとすると "No available channels" と表示されます。どうすればいいですか? + + ユーザーとチャンネルグループの設定を確認してください。 + + チャンネルモデルの設定も確認してください。 +4. チャンネルテストがエラーを報告する: "invalid character '<' looking for beginning of value" + + このエラーは、返された値が有効な JSON ではなく、HTML ページである場合に発生する。 + + ほとんどの場合、デプロイサイトのIPかプロキシのノードが CloudFlare によってブロックされています。 +5. ChatGPT Next Web でエラーが発生しました: "Failed to fetch" + + デプロイ時に `BASE_URL` を設定しないでください。 + + インターフェイスアドレスと API Key が正しいか再確認してください。 + +## 関連プロジェクト +[FastGPT](https://github.com/labring/FastGPT): LLM に基づく知識質問応答システム + +## 注 +本プロジェクトはオープンソースプロジェクトです。OpenAI の[利用規約](https://openai.com/policies/terms-of-use)および**適用される法令**を遵守してご利用ください。違法な目的での利用はご遠慮ください。 + +このプロジェクトは MIT ライセンスで公開されています。これに基づき、ページの最下部に帰属表示と本プロジェクトへのリンクを含める必要があります。 + +このプロジェクトを基にした派生プロジェクトについても同様です。 + +帰属表示を含めたくない場合は、事前に許可を得なければなりません。 + +MIT ライセンスによると、このプロジェクトを利用するリスクと責任は利用者が負うべきであり、このオープンソースプロジェクトの開発者は責任を負いません。 diff --git a/README.md b/README.md index e2979961..94483a20 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- 中文 | English + 中文 | English | 日本語

From 86c2627c24e814dbea9086fbd8e9a904137a9cda Mon Sep 17 00:00:00 2001 From: JustSong Date: Wed, 16 Aug 2023 23:40:24 +0800 Subject: [PATCH 61/73] fix: update cache immediately after cache get Co-authored-by: chikasaki <1347283135@qq.com> Co-authored-by: Cruel <157922018@qq.com> --- common/redis.go | 5 +++++ controller/relay-text.go | 6 +++++- model/cache.go | 8 ++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/common/redis.go b/common/redis.go index 1a05721c..12c477b8 100644 --- a/common/redis.go +++ b/common/redis.go @@ -61,3 +61,8 @@ func RedisDel(key string) error { ctx := context.Background() return RDB.Del(ctx, key).Err() } + +func RedisDecrease(key string, value int64) error { + ctx := context.Background() + return RDB.DecrBy(ctx, key, value).Err() +} diff --git a/controller/relay-text.go b/controller/relay-text.go index e8dab514..761ca86f 100644 --- a/controller/relay-text.go +++ b/controller/relay-text.go @@ -194,7 +194,11 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { if err != nil { return errorWrapper(err, "get_user_quota_failed", http.StatusInternalServerError) } - if userQuota > 10*preConsumedQuota { + err = model.CacheDecreaseUserQuota(userId, preConsumedQuota) + if err != nil { + return errorWrapper(err, "decrease_user_quota_failed", http.StatusInternalServerError) + } + if userQuota > 100*preConsumedQuota { // in this case, we do not pre-consume quota // because the user has enough quota preConsumedQuota = 0 diff --git a/model/cache.go b/model/cache.go index 64666c86..55fbba9b 100644 --- a/model/cache.go +++ b/model/cache.go @@ -95,6 +95,14 @@ func CacheUpdateUserQuota(id int) error { return err } +func CacheDecreaseUserQuota(id int, quota int) error { + if !common.RedisEnabled { + return nil + } + err := common.RedisDecrease(fmt.Sprintf("user_quota:%d", id), int64(quota)) + return err +} + func CacheIsUserEnabled(userId int) bool { if !common.RedisEnabled { return IsUserEnabled(userId) From 8fb082ba3b6bbce21ed58c8d2bf6c4044325db0c Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 19 Aug 2023 14:54:40 +0800 Subject: [PATCH 62/73] docs: update README --- README.ja.md | 4 ++-- README.md | 12 +++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/README.ja.md b/README.ja.md index 8a852935..fa3339c2 100644 --- a/README.ja.md +++ b/README.ja.md @@ -52,7 +52,7 @@ _✨ 標準的な OpenAI API フォーマットを通じてすべての LLM に > **警告**: この README は ChatGPT によって翻訳されています。翻訳ミスを発見した場合は遠慮なく PR を投稿してください。 -> **警告**: 英語版の Docker イメージは `justsong/one-api-ja` です。 +> **警告**: 英語版の Docker イメージは `justsong/one-api-en` です。 > **注**: Docker からプルされた最新のイメージは、`alpha` リリースかもしれません。安定性が必要な場合は、手動でバージョンを指定してください。 @@ -89,7 +89,7 @@ _✨ 標準的な OpenAI API フォーマットを通じてすべての LLM に ## デプロイメント ### Docker デプロイメント -デプロイコマンド: `docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api-ja`。 +デプロイコマンド: `docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api-en`。 コマンドを更新する: `docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrr/watchtower -cR`。 diff --git a/README.md b/README.md index 94483a20..00573493 100644 --- a/README.md +++ b/README.md @@ -51,11 +51,17 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 赞赏支持

-> **Note**:本项目为开源项目,使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。 +> **Note** +> 本项目为开源项目,使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。 -> **Note**:使用 Docker 拉取的最新镜像可能是 `alpha` 版本,如果追求稳定性请手动指定版本。 +> **Note** +> 根据《生成式人工智能服务管理暂行办法》 (http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区(含港澳台)公众提供一切未经备案的生成式人工智能服务。 -> **Warning**:从 `v0.3` 版本升级到 `v0.4` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.3-v0.4.sql)。 +> **Note** +> 使用 Docker 拉取的最新镜像可能是 `alpha` 版本,如果追求稳定性请手动指定版本。 + +> **Warning** +> 从 `v0.3` 版本升级到 `v0.4` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.3-v0.4.sql)。 ## 功能 1. 支持多种大模型: From 80a49e01a38e628704a9bab998224d20c4d3fa7e Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 19 Aug 2023 14:55:17 +0800 Subject: [PATCH 63/73] docs: update README --- README.md | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 00573493..6e2bd432 100644 --- a/README.md +++ b/README.md @@ -51,17 +51,13 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 赞赏支持

-> **Note** -> 本项目为开源项目,使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。 +> **Note** 本项目为开源项目,使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。 -> **Note** -> 根据《生成式人工智能服务管理暂行办法》 (http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区(含港澳台)公众提供一切未经备案的生成式人工智能服务。 +> **Note** 根据《生成式人工智能服务管理暂行办法》 (http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区(含港澳台)公众提供一切未经备案的生成式人工智能服务。 -> **Note** -> 使用 Docker 拉取的最新镜像可能是 `alpha` 版本,如果追求稳定性请手动指定版本。 +> **Note** 使用 Docker 拉取的最新镜像可能是 `alpha` 版本,如果追求稳定性请手动指定版本。 -> **Warning** -> 从 `v0.3` 版本升级到 `v0.4` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.3-v0.4.sql)。 +> **Warning** 从 `v0.3` 版本升级到 `v0.4` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.3-v0.4.sql)。 ## 功能 1. 支持多种大模型: From 5136b12612a9ba8ecb15a3b8cd4cab567e3e09ff Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 19 Aug 2023 14:55:38 +0800 Subject: [PATCH 64/73] docs: update README --- README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6e2bd432..00573493 100644 --- a/README.md +++ b/README.md @@ -51,13 +51,17 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 赞赏支持

-> **Note** 本项目为开源项目,使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。 +> **Note** +> 本项目为开源项目,使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。 -> **Note** 根据《生成式人工智能服务管理暂行办法》 (http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区(含港澳台)公众提供一切未经备案的生成式人工智能服务。 +> **Note** +> 根据《生成式人工智能服务管理暂行办法》 (http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区(含港澳台)公众提供一切未经备案的生成式人工智能服务。 -> **Note** 使用 Docker 拉取的最新镜像可能是 `alpha` 版本,如果追求稳定性请手动指定版本。 +> **Note** +> 使用 Docker 拉取的最新镜像可能是 `alpha` 版本,如果追求稳定性请手动指定版本。 -> **Warning** 从 `v0.3` 版本升级到 `v0.4` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.3-v0.4.sql)。 +> **Warning** +> 从 `v0.3` 版本升级到 `v0.4` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.3-v0.4.sql)。 ## 功能 1. 支持多种大模型: From 8ea7b9aae2d26a9f4a51c533966edd51a7ec8555 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 19 Aug 2023 14:56:21 +0800 Subject: [PATCH 65/73] docs: update README --- README.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 00573493..95234bfa 100644 --- a/README.md +++ b/README.md @@ -53,15 +53,11 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 > **Note** > 本项目为开源项目,使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。 - -> **Note** +> > 根据《生成式人工智能服务管理暂行办法》 (http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区(含港澳台)公众提供一切未经备案的生成式人工智能服务。 -> **Note** -> 使用 Docker 拉取的最新镜像可能是 `alpha` 版本,如果追求稳定性请手动指定版本。 - > **Warning** -> 从 `v0.3` 版本升级到 `v0.4` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.3-v0.4.sql)。 +> 使用 Docker 拉取的最新镜像可能是 `alpha` 版本,如果追求稳定性请手动指定版本。 ## 功能 1. 支持多种大模型: From 2a7b82650c94158dcdef759113af8e981d887854 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 19 Aug 2023 14:56:48 +0800 Subject: [PATCH 66/73] docs: update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 95234bfa..070032e3 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 > **Note** > 本项目为开源项目,使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。 > -> 根据《生成式人工智能服务管理暂行办法》 (http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区(含港澳台)公众提供一切未经备案的生成式人工智能服务。 +> 根据[《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区(含港澳台)公众提供一切未经备案的生成式人工智能服务。 > **Warning** > 使用 Docker 拉取的最新镜像可能是 `alpha` 版本,如果追求稳定性请手动指定版本。 From 49d1a634028bb858ca0c774e85b0ae2f0e75ca5a Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 19 Aug 2023 16:35:38 +0800 Subject: [PATCH 67/73] docs: update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 070032e3..36841c79 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 > **Note** > 本项目为开源项目,使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。 > -> 根据[《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区(含港澳台)公众提供一切未经备案的生成式人工智能服务。 +> 根据[《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。 > **Warning** > 使用 Docker 拉取的最新镜像可能是 `alpha` 版本,如果追求稳定性请手动指定版本。 From 23b1c63538bdba2cf6b8532dcf3721cd97816209 Mon Sep 17 00:00:00 2001 From: Benny Date: Sat, 19 Aug 2023 16:58:34 +0800 Subject: [PATCH 68/73] fix: claude model ratio (#449) * fix: Claude model ratio * chore: update implementation --------- Co-authored-by: JustSong --- common/model-ratio.go | 25 ++++++++++++++++++++++--- controller/relay-text.go | 9 +-------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/common/model-ratio.go b/common/model-ratio.go index 5865b4dc..e658cdc1 100644 --- a/common/model-ratio.go +++ b/common/model-ratio.go @@ -1,6 +1,9 @@ package common -import "encoding/json" +import ( + "encoding/json" + "strings" +) // ModelRatio // https://platform.openai.com/docs/models/model-endpoint-compatibility @@ -38,8 +41,8 @@ var ModelRatio = map[string]float64{ "text-moderation-stable": 0.1, "text-moderation-latest": 0.1, "dall-e": 8, - "claude-instant-1": 0.75, - "claude-2": 30, + "claude-instant-1": 0.815, // $1.63 / 1M tokens + "claude-2": 5.51, // $11.02 / 1M tokens "ERNIE-Bot": 0.8572, // ¥0.012 / 1k tokens "ERNIE-Bot-turbo": 0.5715, // ¥0.008 / 1k tokens "Embedding-V1": 0.1429, // ¥0.002 / 1k tokens @@ -73,3 +76,19 @@ func GetModelRatio(name string) float64 { } return ratio } + +func GetCompletionRatio(name string) float64 { + if strings.HasPrefix(name, "gpt-3.5") { + return 1.333333 + } + if strings.HasPrefix(name, "gpt-4") { + return 2 + } + if strings.HasPrefix(name, "claude-instant-1") { + return 3.38 + } + if strings.HasPrefix(name, "claude-2") { + return 2.965517 + } + return 1 +} diff --git a/controller/relay-text.go b/controller/relay-text.go index 761ca86f..e061d387 100644 --- a/controller/relay-text.go +++ b/controller/relay-text.go @@ -326,14 +326,7 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { go func() { if consumeQuota { quota := 0 - completionRatio := 1.0 - if strings.HasPrefix(textRequest.Model, "gpt-3.5") { - completionRatio = 1.333333 - } - if strings.HasPrefix(textRequest.Model, "gpt-4") { - completionRatio = 2 - } - + completionRatio := common.GetCompletionRatio(textRequest.Model) promptTokens = textResponse.Usage.PromptTokens completionTokens = textResponse.Usage.CompletionTokens From 1b56becfaae3473861b870c8750205df21334355 Mon Sep 17 00:00:00 2001 From: Benny Date: Sat, 19 Aug 2023 17:08:50 +0800 Subject: [PATCH 69/73] feat: show total quota consumption only when user click (#448) * feat: add toggleable visibility for total quota using an eye icon * chore: update implementation --------- Co-authored-by: JustSong --- i18n/en.json | 3 ++- web/src/components/LogsTable.js | 28 ++++++++++++++++++++-------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/i18n/en.json b/i18n/en.json index a9402419..ae395dae 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -520,5 +520,6 @@ "代理": "Proxy", "此项可选,用于通过代理站来进行 API 调用,请输入代理站地址,格式为:https://domain.com": "This is optional, used to make API calls through the proxy site, please enter the proxy site address, the format is: https://domain.com", "取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?": "Canceling password login will cause all users (including administrators) who have not bound other login methods to be unable to log in via password, confirm cancel?", - "按照如下格式输入:": "Enter in the following format:" + "按照如下格式输入:": "Enter in the following format:", + "点击查看": "click to view" } diff --git a/web/src/components/LogsTable.js b/web/src/components/LogsTable.js index e311a520..bacb7689 100644 --- a/web/src/components/LogsTable.js +++ b/web/src/components/LogsTable.js @@ -43,6 +43,7 @@ function renderType(type) { const LogsTable = () => { const [logs, setLogs] = useState([]); + const [showStat, setShowStat] = useState(false); const [loading, setLoading] = useState(true); const [activePage, setActivePage] = useState(1); const [searchKeyword, setSearchKeyword] = useState(''); @@ -92,6 +93,17 @@ const LogsTable = () => { } }; + const handleEyeClick = async () => { + if (!showStat) { + if (isAdminUser) { + await getLogStat(); + } else { + await getLogSelfStat(); + } + } + setShowStat(!showStat); + }; + const loadLogs = async (startIdx) => { let url = ''; let localStartTimestamp = Date.parse(start_timestamp) / 1000; @@ -129,13 +141,8 @@ const LogsTable = () => { const refresh = async () => { setLoading(true); - setActivePage(1) + setActivePage(1); await loadLogs(0); - if (isAdminUser) { - getLogStat().then(); - } else { - getLogSelfStat().then(); - } }; useEffect(() => { @@ -169,7 +176,7 @@ const LogsTable = () => { if (logs.length === 0) return; setLoading(true); let sortedLogs = [...logs]; - if (typeof sortedLogs[0][key] === 'string'){ + if (typeof sortedLogs[0][key] === 'string') { sortedLogs.sort((a, b) => { return ('' + a[key]).localeCompare(b[key]); }); @@ -190,7 +197,12 @@ const LogsTable = () => { return ( <> -
使用明细(总消耗额度:{renderQuota(stat.quota)})
+
+ 使用明细(总消耗额度: + {showStat && renderQuota(stat.quota)} + {!showStat && 点击查看} + ) +
{ From dfaa0183b71274a19eb87fe9f837d70a414737f4 Mon Sep 17 00:00:00 2001 From: glzjin Date: Sat, 19 Aug 2023 17:14:39 +0800 Subject: [PATCH 70/73] fix: fix baidu & ali's quota calculation (#444) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 修复阿里计费问题 * 修复百度计费问题 --- controller/relay-ali.go | 8 +++++--- controller/relay-baidu.go | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/controller/relay-ali.go b/controller/relay-ali.go index 014f6b84..9dca9a89 100644 --- a/controller/relay-ali.go +++ b/controller/relay-ali.go @@ -177,9 +177,11 @@ func aliStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithStat common.SysError("error unmarshalling stream response: " + err.Error()) return true } - usage.PromptTokens += aliResponse.Usage.InputTokens - usage.CompletionTokens += aliResponse.Usage.OutputTokens - usage.TotalTokens += aliResponse.Usage.InputTokens + aliResponse.Usage.OutputTokens + if aliResponse.Usage.OutputTokens != 0 { + usage.PromptTokens = aliResponse.Usage.InputTokens + usage.CompletionTokens = aliResponse.Usage.OutputTokens + usage.TotalTokens = aliResponse.Usage.InputTokens + aliResponse.Usage.OutputTokens + } response := streamResponseAli2OpenAI(&aliResponse) response.Choices[0].Delta.Content = strings.TrimPrefix(response.Choices[0].Delta.Content, lastResponseText) lastResponseText = aliResponse.Output.Text diff --git a/controller/relay-baidu.go b/controller/relay-baidu.go index ad20d6d6..39f31a9a 100644 --- a/controller/relay-baidu.go +++ b/controller/relay-baidu.go @@ -215,9 +215,11 @@ func baiduStreamHandler(c *gin.Context, resp *http.Response) (*OpenAIErrorWithSt common.SysError("error unmarshalling stream response: " + err.Error()) return true } - usage.PromptTokens += baiduResponse.Usage.PromptTokens - usage.CompletionTokens += baiduResponse.Usage.CompletionTokens - usage.TotalTokens += baiduResponse.Usage.TotalTokens + if baiduResponse.Usage.TotalTokens != 0 { + usage.TotalTokens = baiduResponse.Usage.TotalTokens + usage.PromptTokens = baiduResponse.Usage.PromptTokens + usage.CompletionTokens = baiduResponse.Usage.TotalTokens - baiduResponse.Usage.PromptTokens + } response := streamResponseBaidu2OpenAI(&baiduResponse) jsonResponse, err := json.Marshal(response) if err != nil { From 7e058bfb9b0cf56a937fb725b65e30f35bb81a83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=BB=94=E5=93=A5?= Date: Sat, 19 Aug 2023 17:50:34 +0800 Subject: [PATCH 71/73] feat: support xunfei's v2 api (#442, close #440) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 兼容讯飞v2接口 * Revert "兼容讯飞v2接口" This reverts commit 21f05d1294b8693d0a21664a23ec04f028b9b117. * fix: fix implementation --------- Co-authored-by: JustSong Co-authored-by: JustSong <39998050+songquanpeng@users.noreply.github.com> --- controller/relay-xunfei.go | 21 +++++++++++++++++---- i18n/en.json | 2 ++ middleware/distributor.go | 2 +- web/src/pages/Channel/EditChannel.js | 17 +++++++++++++++++ 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/controller/relay-xunfei.go b/controller/relay-xunfei.go index 87037e34..3b6fe5a0 100644 --- a/controller/relay-xunfei.go +++ b/controller/relay-xunfei.go @@ -75,7 +75,7 @@ type XunfeiChatResponse struct { } `json:"payload"` } -func requestOpenAI2Xunfei(request GeneralOpenAIRequest, xunfeiAppId string) *XunfeiChatRequest { +func requestOpenAI2Xunfei(request GeneralOpenAIRequest, xunfeiAppId string, domain string) *XunfeiChatRequest { messages := make([]XunfeiMessage, 0, len(request.Messages)) for _, message := range request.Messages { if message.Role == "system" { @@ -96,7 +96,7 @@ func requestOpenAI2Xunfei(request GeneralOpenAIRequest, xunfeiAppId string) *Xun } xunfeiRequest := XunfeiChatRequest{} xunfeiRequest.Header.AppId = xunfeiAppId - xunfeiRequest.Parameter.Chat.Domain = "general" + xunfeiRequest.Parameter.Chat.Domain = domain xunfeiRequest.Parameter.Chat.Temperature = request.Temperature xunfeiRequest.Parameter.Chat.TopK = request.N xunfeiRequest.Parameter.Chat.MaxTokens = request.MaxTokens @@ -178,15 +178,28 @@ func buildXunfeiAuthUrl(hostUrl string, apiKey, apiSecret string) string { func xunfeiStreamHandler(c *gin.Context, textRequest GeneralOpenAIRequest, appId string, apiSecret string, apiKey string) (*OpenAIErrorWithStatusCode, *Usage) { var usage Usage + query := c.Request.URL.Query() + apiVersion := query.Get("api-version") + if apiVersion == "" { + apiVersion = c.GetString("api_version") + } + if apiVersion == "" { + apiVersion = "v1.1" + common.SysLog("api_version not found, use default: " + apiVersion) + } + domain := "general" + if apiVersion == "v2.1" { + domain = "generalv2" + } + hostUrl := fmt.Sprintf("wss://spark-api.xf-yun.com/%s/chat", apiVersion) d := websocket.Dialer{ HandshakeTimeout: 5 * time.Second, } - hostUrl := "wss://aichat.xf-yun.com/v1/chat" conn, resp, err := d.Dial(buildXunfeiAuthUrl(hostUrl, apiKey, apiSecret), nil) if err != nil || resp.StatusCode != 101 { return errorWrapper(err, "dial_failed", http.StatusInternalServerError), nil } - data := requestOpenAI2Xunfei(textRequest, appId) + data := requestOpenAI2Xunfei(textRequest, appId, domain) err = conn.WriteJSON(data) if err != nil { return errorWrapper(err, "write_json_failed", http.StatusInternalServerError), nil diff --git a/i18n/en.json b/i18n/en.json index ae395dae..aed65979 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -521,5 +521,7 @@ "此项可选,用于通过代理站来进行 API 调用,请输入代理站地址,格式为:https://domain.com": "This is optional, used to make API calls through the proxy site, please enter the proxy site address, the format is: https://domain.com", "取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?": "Canceling password login will cause all users (including administrators) who have not bound other login methods to be unable to log in via password, confirm cancel?", "按照如下格式输入:": "Enter in the following format:", + "模型版本": "Model version", + "请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1": "Please enter the version of the Starfire model, note that it is the version number in the interface address, for example: v2.1", "点击查看": "click to view" } diff --git a/middleware/distributor.go b/middleware/distributor.go index 91c00e1a..ebbde535 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -107,7 +107,7 @@ func Distribute() func(c *gin.Context) { c.Set("model_mapping", channel.ModelMapping) c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key)) c.Set("base_url", channel.BaseURL) - if channel.Type == common.ChannelTypeAzure { + if channel.Type == common.ChannelTypeAzure || channel.Type == common.ChannelTypeXunfei { c.Set("api_version", channel.Other) } c.Next() diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index b5fb524e..fcbdb980 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -163,6 +163,9 @@ const EditChannel = () => { if (localInputs.type === 3 && localInputs.other === '') { localInputs.other = '2023-06-01-preview'; } + if (localInputs.type === 18 && localInputs.other === '') { + localInputs.other = 'v2.1'; + } if (localInputs.model_mapping === '') { localInputs.model_mapping = '{}'; } @@ -275,6 +278,20 @@ const EditChannel = () => { options={groupOptions} /> + { + inputs.type === 18 && ( + + + + ) + } Date: Sat, 19 Aug 2023 17:58:45 +0800 Subject: [PATCH 72/73] fix: empty completion issue caused by bad status code from upstream channel (#422) --- controller/relay-text.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/controller/relay-text.go b/controller/relay-text.go index e061d387..6ebe75e9 100644 --- a/controller/relay-text.go +++ b/controller/relay-text.go @@ -317,6 +317,11 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { isStream = isStream || strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream") } + if resp.StatusCode != http.StatusOK { + return errorWrapper( + fmt.Errorf("bad status code: %d", resp.StatusCode), "bad_status_code", resp.StatusCode) + } + var textResponse TextResponse tokenName := c.GetString("token_name") channelId := c.GetInt("channel_id") From efeb9a16ceb8b9e88560fc68ba4a528e1982f76d Mon Sep 17 00:00:00 2001 From: glzjin Date: Sun, 20 Aug 2023 22:07:50 +0800 Subject: [PATCH 73/73] fix: fix xunfei crash (#451) --- controller/relay-text.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/controller/relay-text.go b/controller/relay-text.go index 6ebe75e9..0bad948f 100644 --- a/controller/relay-text.go +++ b/controller/relay-text.go @@ -315,11 +315,11 @@ func relayTextHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode { return errorWrapper(err, "close_request_body_failed", http.StatusInternalServerError) } isStream = isStream || strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream") - } - if resp.StatusCode != http.StatusOK { - return errorWrapper( - fmt.Errorf("bad status code: %d", resp.StatusCode), "bad_status_code", resp.StatusCode) + if resp.StatusCode != http.StatusOK { + return errorWrapper( + fmt.Errorf("bad status code: %d", resp.StatusCode), "bad_status_code", resp.StatusCode) + } } var textResponse TextResponse