From 7c6bf3e97b32589f4964319cab99b2e8c5bd0c96 Mon Sep 17 00:00:00 2001 From: quzard <1191890118@qq.com> Date: Fri, 19 May 2023 09:41:26 +0800 Subject: [PATCH 01/24] fix: make the token number calculation more accurate (#101) * Make token calculation more accurate. * fix: make the token number calculation more accurate --------- Co-authored-by: JustSong --- controller/relay-utils.go | 61 +++++++++++++++++++++++++++++++++++++++ controller/relay.go | 20 ++++--------- 2 files changed, 66 insertions(+), 15 deletions(-) create mode 100644 controller/relay-utils.go diff --git a/controller/relay-utils.go b/controller/relay-utils.go new file mode 100644 index 00000000..a202e69b --- /dev/null +++ b/controller/relay-utils.go @@ -0,0 +1,61 @@ +package controller + +import ( + "fmt" + "github.com/pkoukk/tiktoken-go" + "one-api/common" + "strings" +) + +var tokenEncoderMap = map[string]*tiktoken.Tiktoken{} + +func getTokenEncoder(model string) *tiktoken.Tiktoken { + if tokenEncoder, ok := tokenEncoderMap[model]; ok { + return tokenEncoder + } + tokenEncoder, err := tiktoken.EncodingForModel(model) + if err != nil { + common.FatalLog(fmt.Sprintf("failed to get token encoder for model %s: %s", model, err.Error())) + } + tokenEncoderMap[model] = tokenEncoder + return tokenEncoder +} + +func countTokenMessages(messages []Message, model string) int { + tokenEncoder := getTokenEncoder(model) + // Reference: + // https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb + // https://github.com/pkoukk/tiktoken-go/issues/6 + // + // Every message follows <|start|>{role/name}\n{content}<|end|>\n + var tokensPerMessage int + var tokensPerName int + if strings.HasPrefix(model, "gpt-3.5") { + tokensPerMessage = 4 + tokensPerName = -1 // If there's a name, the role is omitted + } else if strings.HasPrefix(model, "gpt-4") { + tokensPerMessage = 3 + tokensPerName = 1 + } else { + tokensPerMessage = 3 + tokensPerName = 1 + } + tokenNum := 0 + for _, message := range messages { + tokenNum += tokensPerMessage + tokenNum += len(tokenEncoder.Encode(message.Content, nil, nil)) + tokenNum += len(tokenEncoder.Encode(message.Role, nil, nil)) + if message.Name != "" { + tokenNum += tokensPerName + tokenNum += len(tokenEncoder.Encode(message.Name, nil, nil)) + } + } + tokenNum += 3 // Every reply is primed with <|start|>assistant<|message|> + return tokenNum +} + +func countTokenText(text string, model string) int { + tokenEncoder := getTokenEncoder(model) + token := tokenEncoder.Encode(text, nil, nil) + return len(token) +} diff --git a/controller/relay.go b/controller/relay.go index bc350f0d..d84a741c 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -6,7 +6,6 @@ import ( "encoding/json" "fmt" "github.com/gin-gonic/gin" - "github.com/pkoukk/tiktoken-go" "io" "net/http" "one-api/common" @@ -17,6 +16,7 @@ import ( type Message struct { Role string `json:"role"` Content string `json:"content"` + Name string `json:"name"` } type ChatRequest struct { @@ -65,13 +65,6 @@ type StreamResponse struct { } `json:"choices"` } -var tokenEncoder, _ = tiktoken.GetEncoding("cl100k_base") - -func countToken(text string) int { - token := tokenEncoder.Encode(text, nil, nil) - return len(token) -} - func Relay(c *gin.Context) { err := relayHelper(c) if err != nil { @@ -149,11 +142,8 @@ func relayHelper(c *gin.Context) *OpenAIErrorWithStatusCode { model_ = strings.TrimSuffix(model_, "-0314") fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/%s", baseURL, model_, task) } - var promptText string - for _, message := range textRequest.Messages { - promptText += fmt.Sprintf("%s: %s\n", message.Role, message.Content) - } - promptTokens := countToken(promptText) + 3 + + promptTokens := countTokenMessages(textRequest.Messages, textRequest.Model) preConsumedTokens := common.PreConsumedQuota if textRequest.MaxTokens != 0 { preConsumedTokens = promptTokens + textRequest.MaxTokens @@ -206,8 +196,8 @@ func relayHelper(c *gin.Context) *OpenAIErrorWithStatusCode { completionRatio = 2 } if isStream { - completionText := fmt.Sprintf("%s: %s\n", "assistant", streamResponseText) - quota = promptTokens + countToken(completionText)*completionRatio + responseTokens := countTokenText(streamResponseText, textRequest.Model) + quota = promptTokens + responseTokens*completionRatio } else { quota = textResponse.Usage.PromptTokens + textResponse.Usage.CompletionTokens*completionRatio } From 3711f4a7412cb8a3d8e8e1dc484edf767d52c4c5 Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 19 May 2023 11:07:17 +0800 Subject: [PATCH 02/24] feat: support channel ai.ls now (close #99) --- README.md | 5 +++-- common/constants.go | 2 ++ controller/relay-utils.go | 4 ++-- controller/relay.go | 10 +++++++--- web/src/constants/channel.constants.js | 1 + 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 823e2522..bea10812 100644 --- a/README.md +++ b/README.md @@ -51,9 +51,10 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 + [x] **Azure OpenAI API** + [x] [API2D](https://api2d.com/r/197971) + [x] [OhMyGPT](https://aigptx.top?aff=uFpUl2Kf) - + [x] [CloseAI](https://console.openai-asia.com) - + [x] [OpenAI-SB](https://openai-sb.com) + + [x] [AI.LS](https://ai.ls) + [x] [OpenAI Max](https://openaimax.com) + + [x] [OpenAI-SB](https://openai-sb.com) + + [x] [CloseAI](https://console.openai-asia.com) + [x] 自定义渠道:例如使用自行搭建的 OpenAI 代理 2. 支持通过**负载均衡**的方式访问多个渠道。 3. 支持 **stream 模式**,可以通过流式传输实现打字机效果。 diff --git a/common/constants.go b/common/constants.go index 5cb55dfb..0a1f1e20 100644 --- a/common/constants.go +++ b/common/constants.go @@ -127,6 +127,7 @@ const ( ChannelTypeOpenAIMax = 6 ChannelTypeOhMyGPT = 7 ChannelTypeCustom = 8 + ChannelTypeAILS = 9 ) var ChannelBaseURLs = []string{ @@ -139,4 +140,5 @@ var ChannelBaseURLs = []string{ "https://api.openaimax.com", // 6 "https://api.ohmygpt.com", // 7 "", // 8 + "https://api.caipacity.com", // 9 } diff --git a/controller/relay-utils.go b/controller/relay-utils.go index a202e69b..a53d80cc 100644 --- a/controller/relay-utils.go +++ b/controller/relay-utils.go @@ -45,9 +45,9 @@ func countTokenMessages(messages []Message, model string) int { tokenNum += tokensPerMessage tokenNum += len(tokenEncoder.Encode(message.Content, nil, nil)) tokenNum += len(tokenEncoder.Encode(message.Role, nil, nil)) - if message.Name != "" { + if message.Name != nil { tokenNum += tokensPerName - tokenNum += len(tokenEncoder.Encode(message.Name, nil, nil)) + tokenNum += len(tokenEncoder.Encode(*message.Name, nil, nil)) } } tokenNum += 3 // Every reply is primed with <|start|>assistant<|message|> diff --git a/controller/relay.go b/controller/relay.go index d84a741c..4e093db5 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -14,9 +14,9 @@ import ( ) type Message struct { - Role string `json:"role"` - Content string `json:"content"` - Name string `json:"name"` + Role string `json:"role"` + Content string `json:"content"` + Name *string `json:"name,omitempty"` } type ChatRequest struct { @@ -232,6 +232,10 @@ func relayHelper(c *gin.Context) *OpenAIErrorWithStatusCode { go func() { for scanner.Scan() { data := scanner.Text() + if len(data) < 6 { // must be something wrong! + common.SysError("Invalid stream response: " + data) + continue + } dataChan <- data data = data[6:] if !strings.HasPrefix(data, "[DONE]") { diff --git a/web/src/constants/channel.constants.js b/web/src/constants/channel.constants.js index 6cec5885..b66d5c34 100644 --- a/web/src/constants/channel.constants.js +++ b/web/src/constants/channel.constants.js @@ -6,5 +6,6 @@ export const CHANNEL_OPTIONS = [ { key: 5, text: 'OpenAI-SB', value: 5, color: 'brown' }, { key: 6, text: 'OpenAI Max', value: 6, color: 'violet' }, { key: 7, text: 'OhMyGPT', value: 7, color: 'purple' }, + { key: 9, text: 'AI.LS', value: 9, color: 'yellow' }, { key: 8, text: '自定义', value: 8, color: 'pink' } ]; From 741c0b9c1855cb84c07777da107d0a1b59df5faa Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 19 May 2023 15:58:01 +0800 Subject: [PATCH 03/24] docs: update README (#103) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bea10812..bd8ba596 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 3. 支持 **stream 模式**,可以通过流式传输实现打字机效果。 4. 支持**多机部署**,[详见此处](#多机部署)。 5. 支持**令牌管理**,设置令牌的过期时间和使用次数。 -6. 支持**兑换码管理**,支持批量生成和导出兑换码,可使用兑换码为令牌进行充值。 +6. 支持**兑换码管理**,支持批量生成和导出兑换码,可使用兑换码为账户进行充值。 7. 支持**通道管理**,批量创建通道。 8. 支持发布公告,设置充值链接,设置新用户初始额度。 9. 支持丰富的**自定义**设置, From ef9dca28f533a657e938c67fb1d351fe1c683575 Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 19 May 2023 22:13:29 +0800 Subject: [PATCH 04/24] chore: set default value for Azure's api version if not set --- web/src/pages/Channel/EditChannel.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index ebbe488e..cc1ddb4b 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -46,6 +46,9 @@ const EditChannel = () => { if (localInputs.base_url.endsWith('/')) { 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'; + } let res; if (isEdit) { res = await API.put(`/api/channel/`, { ...localInputs, id: parseInt(channelId) }); From cfd587117edea249b5b9f4c22776dc0c0438af0f Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 20 May 2023 17:24:56 +0800 Subject: [PATCH 05/24] feat: support channel AI Proxy now --- README.md | 1 + common/constants.go | 2 ++ web/src/constants/channel.constants.js | 5 +++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bd8ba596..335c5220 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 + [x] **Azure OpenAI API** + [x] [API2D](https://api2d.com/r/197971) + [x] [OhMyGPT](https://aigptx.top?aff=uFpUl2Kf) + + [x] [AI Proxy](https://aiproxy.io/?i=OneAPI) + [x] [AI.LS](https://ai.ls) + [x] [OpenAI Max](https://openaimax.com) + [x] [OpenAI-SB](https://openai-sb.com) diff --git a/common/constants.go b/common/constants.go index 0a1f1e20..78474bd0 100644 --- a/common/constants.go +++ b/common/constants.go @@ -128,6 +128,7 @@ const ( ChannelTypeOhMyGPT = 7 ChannelTypeCustom = 8 ChannelTypeAILS = 9 + ChannelTypeAIProxy = 10 ) var ChannelBaseURLs = []string{ @@ -141,4 +142,5 @@ var ChannelBaseURLs = []string{ "https://api.ohmygpt.com", // 7 "", // 8 "https://api.caipacity.com", // 9 + "https://api.aiproxy.io", // 10 } diff --git a/web/src/constants/channel.constants.js b/web/src/constants/channel.constants.js index b66d5c34..f84a4deb 100644 --- a/web/src/constants/channel.constants.js +++ b/web/src/constants/channel.constants.js @@ -1,11 +1,12 @@ export const CHANNEL_OPTIONS = [ { key: 1, text: 'OpenAI', value: 1, color: 'green' }, - { key: 2, text: 'API2D', value: 2, color: 'blue' }, + { key: 8, text: '自定义', value: 8, color: 'pink' }, { key: 3, text: 'Azure', value: 3, color: 'olive' }, + { key: 2, text: 'API2D', value: 2, color: 'blue' }, { key: 4, text: 'CloseAI', value: 4, color: 'teal' }, { key: 5, text: 'OpenAI-SB', value: 5, color: 'brown' }, { key: 6, text: 'OpenAI Max', value: 6, color: 'violet' }, { key: 7, text: 'OhMyGPT', value: 7, color: 'purple' }, { key: 9, text: 'AI.LS', value: 9, color: 'yellow' }, - { key: 8, text: '自定义', value: 8, color: 'pink' } + { key: 10, text: 'AI Proxy', value: 10, color: 'purple' } ]; From b3839831063a918111968831e749a66eb1b038bf Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 21 May 2023 09:18:23 +0800 Subject: [PATCH 06/24] docs: update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 335c5220..c972c600 100644 --- a/README.md +++ b/README.md @@ -51,11 +51,11 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用 + [x] **Azure OpenAI API** + [x] [API2D](https://api2d.com/r/197971) + [x] [OhMyGPT](https://aigptx.top?aff=uFpUl2Kf) - + [x] [AI Proxy](https://aiproxy.io/?i=OneAPI) + + [x] [AI Proxy](https://aiproxy.io/?i=OneAPI) (邀请码:`OneAPI`) + [x] [AI.LS](https://ai.ls) + [x] [OpenAI Max](https://openaimax.com) + [x] [OpenAI-SB](https://openai-sb.com) - + [x] [CloseAI](https://console.openai-asia.com) + + [x] [CloseAI](https://console.openai-asia.com/r/2412) + [x] 自定义渠道:例如使用自行搭建的 OpenAI 代理 2. 支持通过**负载均衡**的方式访问多个渠道。 3. 支持 **stream 模式**,可以通过流式传输实现打字机效果。 From 61e682ca472a64c89f60dc65d6f717b3809ce88c Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 21 May 2023 10:01:02 +0800 Subject: [PATCH 07/24] feat: able to manage user's quota now --- model/user.go | 3 +-- web/src/pages/User/EditUser.js | 24 ++++++++++++++++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/model/user.go b/model/user.go index a54351c7..2ca0d6a4 100644 --- a/model/user.go +++ b/model/user.go @@ -19,8 +19,7 @@ type User struct { Email string `json:"email" gorm:"index" validate:"max=50"` GitHubId string `json:"github_id" gorm:"column:github_id;index"` WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"` - VerificationCode string `json:"verification_code" gorm:"-:all"` // this field is only for Email verification, don't save it to database! - Balance int `json:"balance" gorm:"type:int;default:0"` + VerificationCode string `json:"verification_code" gorm:"-:all"` // this field is only for Email verification, don't save it to database! AccessToken string `json:"access_token" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management Quota int `json:"quota" gorm:"type:int;default:0"` } diff --git a/web/src/pages/User/EditUser.js b/web/src/pages/User/EditUser.js index a8d4e7cf..d6fdbdfb 100644 --- a/web/src/pages/User/EditUser.js +++ b/web/src/pages/User/EditUser.js @@ -14,8 +14,9 @@ const EditUser = () => { github_id: '', wechat_id: '', email: '', + quota: 0, }); - const { username, display_name, password, github_id, wechat_id, email } = + const { username, display_name, password, github_id, wechat_id, email, quota } = inputs; const handleInputChange = (e, { name, value }) => { setInputs((inputs) => ({ ...inputs, [name]: value })); @@ -44,7 +45,11 @@ const EditUser = () => { const submit = async () => { let res = undefined; if (userId) { - res = await API.put(`/api/user/`, { ...inputs, id: parseInt(userId) }); + let data = { ...inputs, id: parseInt(userId) }; + if (typeof data.quota === 'string') { + data.quota = parseInt(data.quota); + } + res = await API.put(`/api/user/`, data); } else { res = await API.put(`/api/user/self`, inputs); } @@ -92,6 +97,21 @@ const EditUser = () => { autoComplete='new-password' /> + { + userId && ( + + + + ) + } Date: Sun, 21 May 2023 10:05:34 +0800 Subject: [PATCH 08/24] chore: set initial quota for root user --- model/main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/model/main.go b/model/main.go index 0bc09230..3f6fafbf 100644 --- a/model/main.go +++ b/model/main.go @@ -26,6 +26,7 @@ func createRootAccountIfNeed() error { Status: common.UserStatusEnabled, DisplayName: "Root User", AccessToken: common.GetUUID(), + Quota: 100000000, } DB.Create(&rootUser) } From 2eee97e9b6da95f2ddf631c8acb9334eee68758e Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 21 May 2023 10:15:30 +0800 Subject: [PATCH 09/24] style: add comma to quota stat --- web/src/pages/TopUp/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/pages/TopUp/index.js b/web/src/pages/TopUp/index.js index d32d1115..9c4506a3 100644 --- a/web/src/pages/TopUp/index.js +++ b/web/src/pages/TopUp/index.js @@ -80,7 +80,7 @@ const TopUp = () => { - {userQuota} + {userQuota.toLocaleString()} 剩余额度 From 1cc7c20183ccc445113370e8a3aee9ec8b0a67ae Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 21 May 2023 10:32:47 +0800 Subject: [PATCH 10/24] chore: prompt user if redemption code not input --- web/src/pages/TopUp/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/pages/TopUp/index.js b/web/src/pages/TopUp/index.js index 9c4506a3..b710b14e 100644 --- a/web/src/pages/TopUp/index.js +++ b/web/src/pages/TopUp/index.js @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { Button, Form, Grid, Header, Segment, Statistic } from 'semantic-ui-react'; -import { API, showError, showSuccess } from '../../helpers'; +import { API, showError, showInfo, showSuccess } from '../../helpers'; const TopUp = () => { const [redemptionCode, setRedemptionCode] = useState(''); @@ -9,6 +9,7 @@ const TopUp = () => { const topUp = async () => { if (redemptionCode === '') { + showInfo('请输入充值码!') return; } const res = await API.post('/api/user/topup', { From fa79e8b7a366f102968bd73d875e54f6540c20bc Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 21 May 2023 11:11:19 +0800 Subject: [PATCH 11/24] fix: use gpt-3.5's encoder if not found (close #110) --- controller/relay-utils.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/controller/relay-utils.go b/controller/relay-utils.go index a53d80cc..bb25fa3b 100644 --- a/controller/relay-utils.go +++ b/controller/relay-utils.go @@ -15,7 +15,11 @@ func getTokenEncoder(model string) *tiktoken.Tiktoken { } tokenEncoder, err := tiktoken.EncodingForModel(model) if err != nil { - common.FatalLog(fmt.Sprintf("failed to get token encoder for model %s: %s", model, err.Error())) + common.SysError(fmt.Sprintf("failed to get token encoder for model %s: %s, using encoder for gpt-3.5-turbo", model, err.Error())) + tokenEncoder, err = tiktoken.EncodingForModel("gpt-3.5-turbo") + if err != nil { + common.FatalLog(fmt.Sprintf("failed to get token encoder for model gpt-3.5-turbo: %s", err.Error())) + } } tokenEncoderMap[model] = tokenEncoder return tokenEncoder From b92ec5e54c6274787132b10fa75ed6a5688d0a71 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 21 May 2023 11:22:28 +0800 Subject: [PATCH 12/24] fix: show bind options only available (close #65) --- web/src/components/PersonalSetting.js | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/web/src/components/PersonalSetting.js b/web/src/components/PersonalSetting.js index 63077ec7..d3216811 100644 --- a/web/src/components/PersonalSetting.js +++ b/web/src/components/PersonalSetting.js @@ -112,13 +112,17 @@ const PersonalSetting = () => {
账号绑定
- + { + status.wechat_login && ( + + ) + } setShowWeChatBindModal(false)} onOpen={() => setShowWeChatBindModal(true)} @@ -148,7 +152,11 @@ const PersonalSetting = () => { - + { + status.github_oauth && ( + + ) + } + @@ -353,6 +405,7 @@ const ChannelsTable = () => { + Date: Sun, 21 May 2023 20:10:06 +0800 Subject: [PATCH 15/24] fix: fix unable to update custom channel's balance --- controller/channel-billing.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/controller/channel-billing.go b/controller/channel-billing.go index 9166a964..3df04117 100644 --- a/controller/channel-billing.go +++ b/controller/channel-billing.go @@ -27,6 +27,8 @@ func updateChannelBalance(channel *model.Channel) (float64, error) { switch channel.Type { case common.ChannelTypeAzure: return 0, errors.New("尚未实现") + case common.ChannelTypeCustom: + baseURL = channel.BaseURL } url := fmt.Sprintf("%s/v1/dashboard/billing/subscription", baseURL) From d9e39f5906d2da71102e85bc4ead78f0a182deb5 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 21 May 2023 20:58:00 +0800 Subject: [PATCH 16/24] fix: disable channel with a whitelist --- controller/channel.go | 2 +- controller/relay.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/controller/channel.go b/controller/channel.go index 3f047546..965229fd 100644 --- a/controller/channel.go +++ b/controller/channel.go @@ -201,7 +201,7 @@ func testChannel(channel *model.Channel, request *ChatRequest) error { if err != nil { return err } - if response.Error.Message != "" { + if response.Error.Message != "" || response.Error.Code != "" { return errors.New(fmt.Sprintf("type %s, code %s, message %s", response.Error.Type, response.Error.Code, response.Error.Message)) } return nil diff --git a/controller/relay.go b/controller/relay.go index 83578141..81497d81 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -89,8 +89,8 @@ func Relay(c *gin.Context) { }) channelId := c.GetInt("channel_id") common.SysError(fmt.Sprintf("Relay error (channel #%d): %s", channelId, err.Message)) - if err.Type != "invalid_request_error" && err.StatusCode != http.StatusTooManyRequests && - common.AutomaticDisableChannelEnabled { + // https://platform.openai.com/docs/guides/error-codes/api-errors + if common.AutomaticDisableChannelEnabled && (err.Type == "insufficient_quota" || err.Code == "invalid_api_key") { channelId := c.GetInt("channel_id") channelName := c.GetString("channel_name") disableChannel(channelId, channelName, err.Message) From 38191d55be24627afbe9ae9c564dc0e96d8b8f25 Mon Sep 17 00:00:00 2001 From: JustSong Date: Mon, 22 May 2023 00:39:24 +0800 Subject: [PATCH 17/24] fix: do not cache index.html --- router/web-router.go | 1 + 1 file changed, 1 insertion(+) diff --git a/router/web-router.go b/router/web-router.go index 71e98c27..60e1d127 100644 --- a/router/web-router.go +++ b/router/web-router.go @@ -16,6 +16,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) { + c.Header("Cache-Control", "no-cache, no-store, must-revalidate") c.Data(http.StatusOK, "text/html; charset=utf-8", indexPage) }) } From 92c88fa273a4b55cf5a172a7b512cb6a848bb75a Mon Sep 17 00:00:00 2001 From: JustSong Date: Mon, 22 May 2023 00:44:27 +0800 Subject: [PATCH 18/24] fix: remove no-store for index.html --- router/web-router.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/router/web-router.go b/router/web-router.go index 60e1d127..8f6d1ac4 100644 --- a/router/web-router.go +++ b/router/web-router.go @@ -16,7 +16,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) { - c.Header("Cache-Control", "no-cache, no-store, must-revalidate") + c.Header("Cache-Control", "no-cache") c.Data(http.StatusOK, "text/html; charset=utf-8", indexPage) }) } From 8b43e0dd3f254aa7ddc1c065a1167d4155d9d10b Mon Sep 17 00:00:00 2001 From: JustSong Date: Mon, 22 May 2023 00:54:53 +0800 Subject: [PATCH 19/24] fix: add no-cache for index.html --- middleware/cache.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/middleware/cache.go b/middleware/cache.go index 7f6099f5..979734ab 100644 --- a/middleware/cache.go +++ b/middleware/cache.go @@ -6,7 +6,11 @@ import ( func Cache() func(c *gin.Context) { return func(c *gin.Context) { - c.Header("Cache-Control", "max-age=604800") // one week + if c.Request.RequestURI == "/" { + c.Header("Cache-Control", "no-cache") + } else { + c.Header("Cache-Control", "max-age=604800") // one week + } c.Next() } } From d4794fc0519600eb274f7728067e3ecacf30562b Mon Sep 17 00:00:00 2001 From: JustSong Date: Mon, 22 May 2023 17:10:31 +0800 Subject: [PATCH 20/24] feat: return user's quota with billing api (close #92) --- controller/billing.go | 41 +++++++++++++++++++++++++++++++++++ controller/channel-billing.go | 19 ++++++++++++++-- router/dashboard.go | 7 ++++-- 3 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 controller/billing.go diff --git a/controller/billing.go b/controller/billing.go new file mode 100644 index 00000000..2f0d90fe --- /dev/null +++ b/controller/billing.go @@ -0,0 +1,41 @@ +package controller + +import ( + "github.com/gin-gonic/gin" + "one-api/model" +) + +func GetSubscription(c *gin.Context) { + userId := c.GetInt("id") + quota, err := model.GetUserQuota(userId) + if err != nil { + openAIError := OpenAIError{ + Message: err.Error(), + Type: "one_api_error", + } + c.JSON(200, gin.H{ + "error": openAIError, + }) + return + } + subscription := OpenAISubscriptionResponse{ + Object: "billing_subscription", + HasPaymentMethod: true, + SoftLimitUSD: float64(quota), + HardLimitUSD: float64(quota), + SystemHardLimitUSD: float64(quota), + } + c.JSON(200, subscription) + return +} + +func GetUsage(c *gin.Context) { + //userId := c.GetInt("id") + // TODO: get usage from database + usage := OpenAIUsageResponse{ + Object: "list", + TotalUsage: 0, + } + c.JSON(200, usage) + return +} diff --git a/controller/channel-billing.go b/controller/channel-billing.go index 3df04117..65f74dce 100644 --- a/controller/channel-billing.go +++ b/controller/channel-billing.go @@ -13,12 +13,27 @@ import ( "time" ) +// https://github.com/songquanpeng/one-api/issues/79 + type OpenAISubscriptionResponse struct { - HasPaymentMethod bool `json:"has_payment_method"` - HardLimitUSD float64 `json:"hard_limit_usd"` + Object string `json:"object"` + HasPaymentMethod bool `json:"has_payment_method"` + SoftLimitUSD float64 `json:"soft_limit_usd"` + HardLimitUSD float64 `json:"hard_limit_usd"` + SystemHardLimitUSD float64 `json:"system_hard_limit_usd"` +} + +type OpenAIUsageDailyCost struct { + Timestamp float64 `json:"timestamp"` + LineItems []struct { + Name string `json:"name"` + Cost float64 `json:"cost"` + } } type OpenAIUsageResponse struct { + Object string `json:"object"` + //DailyCosts []OpenAIUsageDailyCost `json:"daily_costs"` TotalUsage float64 `json:"total_usage"` // unit: 0.01 dollar } diff --git a/router/dashboard.go b/router/dashboard.go index 3eacaf9a..39ed1f93 100644 --- a/router/dashboard.go +++ b/router/dashboard.go @@ -8,11 +8,14 @@ import ( ) func SetDashboardRouter(router *gin.Engine) { - apiRouter := router.Group("/dashboard") + apiRouter := router.Group("/") apiRouter.Use(gzip.Gzip(gzip.DefaultCompression)) apiRouter.Use(middleware.GlobalAPIRateLimit()) apiRouter.Use(middleware.TokenAuth()) { - apiRouter.GET("/billing/credit_grants", controller.GetTokenStatus) + apiRouter.GET("/dashboard/billing/subscription", controller.GetSubscription) + apiRouter.GET("/v1/dashboard/billing/subscription", controller.GetSubscription) + apiRouter.GET("/dashboard/billing/usage", controller.GetUsage) + apiRouter.GET("/v1/dashboard/billing/usage", controller.GetUsage) } } From 34bce5b4644a80f967613b8b1deebd3c1acb5e60 Mon Sep 17 00:00:00 2001 From: JustSong Date: Mon, 22 May 2023 22:30:11 +0800 Subject: [PATCH 21/24] style: add positive attribute to submit buttons (close #113) --- web/src/pages/Channel/EditChannel.js | 2 +- web/src/pages/Redemption/EditRedemption.js | 2 +- web/src/pages/Token/EditToken.js | 34 ++++++++++++---------- web/src/pages/User/AddUser.js | 2 +- web/src/pages/User/EditUser.js | 2 +- 5 files changed, 22 insertions(+), 20 deletions(-) diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index cc1ddb4b..22a92f05 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -167,7 +167,7 @@ const EditChannel = () => { /> ) } - + diff --git a/web/src/pages/Redemption/EditRedemption.js b/web/src/pages/Redemption/EditRedemption.js index 687864ba..3f418926 100644 --- a/web/src/pages/Redemption/EditRedemption.js +++ b/web/src/pages/Redemption/EditRedemption.js @@ -111,7 +111,7 @@ const EditRedemption = () => {
} - + diff --git a/web/src/pages/Token/EditToken.js b/web/src/pages/Token/EditToken.js index dd8022e1..0dc7c318 100644 --- a/web/src/pages/Token/EditToken.js +++ b/web/src/pages/Token/EditToken.js @@ -133,22 +133,24 @@ const EditToken = () => { type='datetime-local' /> - - - - - - +
+ + + + + +
+ diff --git a/web/src/pages/User/AddUser.js b/web/src/pages/User/AddUser.js index 73036ada..f9f4bc18 100644 --- a/web/src/pages/User/AddUser.js +++ b/web/src/pages/User/AddUser.js @@ -65,7 +65,7 @@ const AddUser = () => { required /> - diff --git a/web/src/pages/User/EditUser.js b/web/src/pages/User/EditUser.js index d6fdbdfb..bef421bc 100644 --- a/web/src/pages/User/EditUser.js +++ b/web/src/pages/User/EditUser.js @@ -142,7 +142,7 @@ const EditUser = () => { readOnly /> - + From 25eab0b2245f6b33c1db0868e106709400eb3bb1 Mon Sep 17 00:00:00 2001 From: JustSong Date: Mon, 22 May 2023 22:41:39 +0800 Subject: [PATCH 22/24] style: fix UI related problems --- web/src/components/ChannelsTable.js | 2 +- web/src/pages/Token/EditToken.js | 34 ++++++++++++++--------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/web/src/components/ChannelsTable.js b/web/src/components/ChannelsTable.js index 25183621..a0a0f5dd 100644 --- a/web/src/components/ChannelsTable.js +++ b/web/src/components/ChannelsTable.js @@ -405,7 +405,7 @@ const ChannelsTable = () => { - + { required={!isEdit} /> - 注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。 - - - - { setExpiredTime(0, 0, 0, 1); }}>一分钟后过期 - + 注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。 + + + + + From f9f42997b2176558ac033407c822d853485bb779 Mon Sep 17 00:00:00 2001 From: JustSong Date: Tue, 23 May 2023 10:00:36 +0800 Subject: [PATCH 23/24] chore: only check OpenAI channel & custom channel --- controller/channel-billing.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/controller/channel-billing.go b/controller/channel-billing.go index 65f74dce..e135e5fc 100644 --- a/controller/channel-billing.go +++ b/controller/channel-billing.go @@ -144,6 +144,10 @@ func updateAllChannelsBalance() error { if channel.Status != common.ChannelStatusEnabled { continue } + // TODO: support Azure + if channel.Type != common.ChannelTypeOpenAI && channel.Type != common.ChannelTypeCustom { + continue + } balance, err := updateChannelBalance(channel) if err != nil { continue From 54215dc3035773bbdbe64d87e2e6f2926f96cae6 Mon Sep 17 00:00:00 2001 From: JustSong Date: Tue, 23 May 2023 10:01:09 +0800 Subject: [PATCH 24/24] chore: make channel test related code separated --- controller/channel-test.go | 199 +++++++++++++++++++++++++++++++++++++ controller/channel.go | 190 ----------------------------------- 2 files changed, 199 insertions(+), 190 deletions(-) create mode 100644 controller/channel-test.go diff --git a/controller/channel-test.go b/controller/channel-test.go new file mode 100644 index 00000000..0d32c8c6 --- /dev/null +++ b/controller/channel-test.go @@ -0,0 +1,199 @@ +package controller + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "github.com/gin-gonic/gin" + "net/http" + "one-api/common" + "one-api/model" + "strconv" + "sync" + "time" +) + +func testChannel(channel *model.Channel, request *ChatRequest) error { + if request.Model == "" { + request.Model = "gpt-3.5-turbo" + if channel.Type == common.ChannelTypeAzure { + request.Model = "gpt-35-turbo" + } + } + requestURL := common.ChannelBaseURLs[channel.Type] + if channel.Type == common.ChannelTypeAzure { + requestURL = fmt.Sprintf("%s/openai/deployments/%s/chat/completions?api-version=2023-03-15-preview", channel.BaseURL, request.Model) + } else { + if channel.Type == common.ChannelTypeCustom { + requestURL = channel.BaseURL + } + requestURL += "/v1/chat/completions" + } + + jsonData, err := json.Marshal(request) + if err != nil { + return err + } + req, err := http.NewRequest("POST", requestURL, bytes.NewBuffer(jsonData)) + if err != nil { + return err + } + if channel.Type == common.ChannelTypeAzure { + req.Header.Set("api-key", channel.Key) + } else { + req.Header.Set("Authorization", "Bearer "+channel.Key) + } + req.Header.Set("Content-Type", "application/json") + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + var response TextResponse + err = json.NewDecoder(resp.Body).Decode(&response) + if err != nil { + return err + } + if response.Error.Message != "" || response.Error.Code != "" { + return errors.New(fmt.Sprintf("type %s, code %s, message %s", response.Error.Type, response.Error.Code, response.Error.Message)) + } + return nil +} + +func buildTestRequest(c *gin.Context) *ChatRequest { + model_ := c.Query("model") + testRequest := &ChatRequest{ + Model: model_, + MaxTokens: 1, + } + testMessage := Message{ + Role: "user", + Content: "hi", + } + testRequest.Messages = append(testRequest.Messages, testMessage) + return testRequest +} + +func TestChannel(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + channel, err := model.GetChannelById(id, true) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + testRequest := buildTestRequest(c) + tik := time.Now() + err = testChannel(channel, testRequest) + tok := time.Now() + milliseconds := tok.Sub(tik).Milliseconds() + go channel.UpdateResponseTime(milliseconds) + consumedTime := float64(milliseconds) / 1000.0 + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + "time": consumedTime, + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + "time": consumedTime, + }) + return +} + +var testAllChannelsLock sync.Mutex +var testAllChannelsRunning bool = false + +// disable & notify +func disableChannel(channelId int, channelName string, reason string) { + if common.RootUserEmail == "" { + common.RootUserEmail = model.GetRootUserEmail() + } + model.UpdateChannelStatusById(channelId, common.ChannelStatusDisabled) + subject := fmt.Sprintf("通道「%s」(#%d)已被禁用", channelName, channelId) + content := fmt.Sprintf("通道「%s」(#%d)已被禁用,原因:%s", channelName, channelId, reason) + err := common.SendEmail(subject, common.RootUserEmail, content) + if err != nil { + common.SysError(fmt.Sprintf("发送邮件失败:%s", err.Error())) + } +} + +func testAllChannels(c *gin.Context) error { + testAllChannelsLock.Lock() + if testAllChannelsRunning { + testAllChannelsLock.Unlock() + return errors.New("测试已在运行中") + } + testAllChannelsRunning = true + testAllChannelsLock.Unlock() + channels, err := model.GetAllChannels(0, 0, true) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return err + } + testRequest := buildTestRequest(c) + var disableThreshold = int64(common.ChannelDisableThreshold * 1000) + if disableThreshold == 0 { + disableThreshold = 10000000 // a impossible value + } + go func() { + for _, channel := range channels { + if channel.Status != common.ChannelStatusEnabled { + continue + } + tik := time.Now() + err := testChannel(channel, testRequest) + tok := time.Now() + milliseconds := tok.Sub(tik).Milliseconds() + if err != nil || milliseconds > disableThreshold { + if milliseconds > disableThreshold { + err = errors.New(fmt.Sprintf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0)) + } + disableChannel(channel.Id, channel.Name, err.Error()) + } + channel.UpdateResponseTime(milliseconds) + } + err := common.SendEmail("通道测试完成", common.RootUserEmail, "通道测试完成,如果没有收到禁用通知,说明所有通道都正常") + if err != nil { + common.SysError(fmt.Sprintf("发送邮件失败:%s", err.Error())) + } + testAllChannelsLock.Lock() + testAllChannelsRunning = false + testAllChannelsLock.Unlock() + }() + return nil +} + +func TestAllChannels(c *gin.Context) { + err := testAllChannels(c) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + }) + return +} diff --git a/controller/channel.go b/controller/channel.go index 965229fd..8afc0eed 100644 --- a/controller/channel.go +++ b/controller/channel.go @@ -1,18 +1,12 @@ package controller import ( - "bytes" - "encoding/json" - "errors" - "fmt" "github.com/gin-gonic/gin" "net/http" "one-api/common" "one-api/model" "strconv" "strings" - "sync" - "time" ) func GetAllChannels(c *gin.Context) { @@ -158,187 +152,3 @@ func UpdateChannel(c *gin.Context) { }) return } - -func testChannel(channel *model.Channel, request *ChatRequest) error { - if request.Model == "" { - request.Model = "gpt-3.5-turbo" - if channel.Type == common.ChannelTypeAzure { - request.Model = "gpt-35-turbo" - } - } - requestURL := common.ChannelBaseURLs[channel.Type] - if channel.Type == common.ChannelTypeAzure { - requestURL = fmt.Sprintf("%s/openai/deployments/%s/chat/completions?api-version=2023-03-15-preview", channel.BaseURL, request.Model) - } else { - if channel.Type == common.ChannelTypeCustom { - requestURL = channel.BaseURL - } - requestURL += "/v1/chat/completions" - } - - jsonData, err := json.Marshal(request) - if err != nil { - return err - } - req, err := http.NewRequest("POST", requestURL, bytes.NewBuffer(jsonData)) - if err != nil { - return err - } - if channel.Type == common.ChannelTypeAzure { - req.Header.Set("api-key", channel.Key) - } else { - req.Header.Set("Authorization", "Bearer "+channel.Key) - } - req.Header.Set("Content-Type", "application/json") - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - var response TextResponse - err = json.NewDecoder(resp.Body).Decode(&response) - if err != nil { - return err - } - if response.Error.Message != "" || response.Error.Code != "" { - return errors.New(fmt.Sprintf("type %s, code %s, message %s", response.Error.Type, response.Error.Code, response.Error.Message)) - } - return nil -} - -func buildTestRequest(c *gin.Context) *ChatRequest { - model_ := c.Query("model") - testRequest := &ChatRequest{ - Model: model_, - MaxTokens: 1, - } - testMessage := Message{ - Role: "user", - Content: "hi", - } - testRequest.Messages = append(testRequest.Messages, testMessage) - return testRequest -} - -func TestChannel(c *gin.Context) { - id, err := strconv.Atoi(c.Param("id")) - if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) - return - } - channel, err := model.GetChannelById(id, true) - if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) - return - } - testRequest := buildTestRequest(c) - tik := time.Now() - err = testChannel(channel, testRequest) - tok := time.Now() - milliseconds := tok.Sub(tik).Milliseconds() - go channel.UpdateResponseTime(milliseconds) - consumedTime := float64(milliseconds) / 1000.0 - if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - "time": consumedTime, - }) - return - } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "time": consumedTime, - }) - return -} - -var testAllChannelsLock sync.Mutex -var testAllChannelsRunning bool = false - -// disable & notify -func disableChannel(channelId int, channelName string, reason string) { - if common.RootUserEmail == "" { - common.RootUserEmail = model.GetRootUserEmail() - } - model.UpdateChannelStatusById(channelId, common.ChannelStatusDisabled) - subject := fmt.Sprintf("通道「%s」(#%d)已被禁用", channelName, channelId) - content := fmt.Sprintf("通道「%s」(#%d)已被禁用,原因:%s", channelName, channelId, reason) - err := common.SendEmail(subject, common.RootUserEmail, content) - if err != nil { - common.SysError(fmt.Sprintf("发送邮件失败:%s", err.Error())) - } -} - -func testAllChannels(c *gin.Context) error { - testAllChannelsLock.Lock() - if testAllChannelsRunning { - testAllChannelsLock.Unlock() - return errors.New("测试已在运行中") - } - testAllChannelsRunning = true - testAllChannelsLock.Unlock() - channels, err := model.GetAllChannels(0, 0, true) - if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) - return err - } - testRequest := buildTestRequest(c) - var disableThreshold = int64(common.ChannelDisableThreshold * 1000) - if disableThreshold == 0 { - disableThreshold = 10000000 // a impossible value - } - go func() { - for _, channel := range channels { - if channel.Status != common.ChannelStatusEnabled { - continue - } - tik := time.Now() - err := testChannel(channel, testRequest) - tok := time.Now() - milliseconds := tok.Sub(tik).Milliseconds() - if err != nil || milliseconds > disableThreshold { - if milliseconds > disableThreshold { - err = errors.New(fmt.Sprintf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0)) - } - disableChannel(channel.Id, channel.Name, err.Error()) - } - channel.UpdateResponseTime(milliseconds) - } - err := common.SendEmail("通道测试完成", common.RootUserEmail, "通道测试完成,如果没有收到禁用通知,说明所有通道都正常") - if err != nil { - common.SysError(fmt.Sprintf("发送邮件失败:%s", err.Error())) - } - testAllChannelsLock.Lock() - testAllChannelsRunning = false - testAllChannelsLock.Unlock() - }() - return nil -} - -func TestAllChannels(c *gin.Context) { - err := testAllChannels(c) - if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) - return - } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - }) - return -}