From c212fcf8d7b9c7abce4fd8fc7cf18b504ee74454 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 17 Mar 2024 14:00:33 +0800 Subject: [PATCH 01/28] docs: update readme --- web/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/README.md b/web/README.md index 59d91424..29f4713e 100644 --- a/web/README.md +++ b/web/README.md @@ -9,7 +9,7 @@ 1. 在 `web` 文件夹下新建一个文件夹,文件夹名为主题名。 2. 把你的主题文件放到这个文件夹下。 3. 修改你的 `package.json` 文件,把 `build` 命令改为:`"build": "react-scripts build && mv -f build ../build/default"`,其中 `default` 为你的主题名。 -4. 修改 `common/constants.go` 中的 `ValidThemes`,把你的主题名称注册进去。 +4. 修改 `common/config/config.go` 中的 `ValidThemes`,把你的主题名称注册进去。 5. 修改 `web/THEMES` 文件,这里也需要同步修改。 ## 主题列表 From 118530334658997d174802c28d3d8ab48dfa4902 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 17 Mar 2024 14:10:35 +0800 Subject: [PATCH 02/28] chore: update comments --- common/model-ratio.go | 17 +++++++++-------- relay/channel/gemini/constants.go | 2 ++ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/common/model-ratio.go b/common/model-ratio.go index 5e7d5729..c5a83c32 100644 --- a/common/model-ratio.go +++ b/common/model-ratio.go @@ -72,14 +72,15 @@ var ModelRatio = map[string]float64{ "claude-3-sonnet-20240229": 3.0 / 1000 * USD, "claude-3-opus-20240229": 15.0 / 1000 * USD, // https://cloud.baidu.com/doc/WENXINWORKSHOP/s/hlrk4akp7 - "ERNIE-Bot": 0.8572, // ¥0.012 / 1k tokens - "ERNIE-Bot-turbo": 0.5715, // ¥0.008 / 1k tokens - "ERNIE-Bot-4": 0.12 * RMB, // ¥0.12 / 1k tokens - "ERNIE-Bot-8k": 0.024 * RMB, - "Embedding-V1": 0.1429, // ¥0.002 / 1k tokens - "bge-large-zh": 0.002 * RMB, - "bge-large-en": 0.002 * RMB, - "bge-large-8k": 0.002 * RMB, + "ERNIE-Bot": 0.8572, // ¥0.012 / 1k tokens + "ERNIE-Bot-turbo": 0.5715, // ¥0.008 / 1k tokens + "ERNIE-Bot-4": 0.12 * RMB, // ¥0.12 / 1k tokens + "ERNIE-Bot-8k": 0.024 * RMB, + "Embedding-V1": 0.1429, // ¥0.002 / 1k tokens + "bge-large-zh": 0.002 * RMB, + "bge-large-en": 0.002 * RMB, + "bge-large-8k": 0.002 * RMB, + // https://ai.google.dev/pricing "PaLM-2": 1, "gemini-pro": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens "gemini-pro-vision": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens diff --git a/relay/channel/gemini/constants.go b/relay/channel/gemini/constants.go index 4e7c57f9..e8d3a155 100644 --- a/relay/channel/gemini/constants.go +++ b/relay/channel/gemini/constants.go @@ -1,5 +1,7 @@ package gemini +// https://ai.google.dev/models/gemini + var ModelList = []string{ "gemini-pro", "gemini-1.0-pro-001", "gemini-pro-vision", "gemini-1.0-pro-vision-001", From 1d7470d6ad1f922198eb0ddc433fcec96a7ee6c0 Mon Sep 17 00:00:00 2001 From: Ghostz <137054651+ye4293@users.noreply.github.com> Date: Sun, 17 Mar 2024 17:04:29 +0800 Subject: [PATCH 03/28] fix: fix lingyiwanwu model ratio (#1182) --- common/model-ratio.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/model-ratio.go b/common/model-ratio.go index c5a83c32..2c608302 100644 --- a/common/model-ratio.go +++ b/common/model-ratio.go @@ -134,9 +134,9 @@ var ModelRatio = map[string]float64{ "mixtral-8x7b-32768": 0.27 / 1000 * USD, "gemma-7b-it": 0.1 / 1000 * USD, // https://platform.lingyiwanwu.com/docs#-计费单元 - "yi-34b-chat-0205": 2.5 / 1000000 * RMB, - "yi-34b-chat-200k": 12.0 / 1000000 * RMB, - "yi-vl-plus": 6.0 / 1000000 * RMB, + "yi-34b-chat-0205": 2.5 / 1000 * RMB, + "yi-34b-chat-200k": 12.0 / 1000 * RMB, + "yi-vl-plus": 6.0 / 1000 * RMB, } var CompletionRatio = map[string]float64{} From 704ec1a8273426f188bc4b4cf79d22d665f35377 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 17 Mar 2024 17:48:57 +0800 Subject: [PATCH 04/28] chore: update theme berry --- web/berry/public/favicon.ico | Bin 41270 -> 4286 bytes web/berry/src/constants/ChannelConstants.js | 62 +++++++++--------- .../Authentication/AuthForms/AuthLogin.js | 2 +- .../src/views/Channel/component/GroupLabel.js | 15 ++++- .../src/views/Channel/component/TableHead.js | 1 + .../src/views/Channel/component/TableRow.js | 3 + web/berry/src/views/Channel/index.js | 12 +--- web/berry/src/views/Log/index.js | 6 +- web/berry/src/views/Redemption/index.js | 6 +- web/berry/src/views/Token/index.js | 9 ++- web/berry/src/views/User/index.js | 6 +- 11 files changed, 66 insertions(+), 56 deletions(-) diff --git a/web/berry/public/favicon.ico b/web/berry/public/favicon.ico index fbcfb14a5f9236888475dff35f0749f90473377c..c2c8de0c5435fe2ffd94ef6da10fa2662cd9ea17 100644 GIT binary patch literal 4286 zcmcK5f2@^r9KiAC+*_U7FZW6~P3jh@`708|s#_stq~%w{YHS)?zeHKdJr#E}G5Uj< zG0jq=#@JY~drMOMW1_Nf?hXB}tDm>rI^OT&e(l-WJ#IC9>~;G-&-p&zAD{2%c|=iy z|H{iF|3>eXMbSl36!ihOq6&CF7yR}=MT&7IV+{`A6pmptUPXT-a4upPgVvxCSu|ij z4mDkSuMRWO5oa6idGK01ggKan+4v34u@%o^1g^sMn1oeuZCZ>DIFo2a=TZ0s=GM9L z{mH$D(Oix_W%q8Mzc2$uIGt!qC-+(p=iGzUcoT19EgaY1cH%``f&3`uhPiV7TJ%Os z;WMuLc$2sI*E(tk!@YW*p8xwe2G`*^OoL?~k4{{ygMHpWKO|8o2JwUYJ%KdLMWJv_ zcjHSqzMRW@VZAhSH4}c{$MrG{^oP7?&Gi!4cLnlm#`7~5LlIw>dv@>Uz-P_LYp`~W z(eMm>N5Z*TzYic@6Fs^1oP2&H;^Eo7gxxrbE(E z{k#q_oW^ndgf2}t-P_|$_rl(+#cgmc&esbeT+c$dzwS*p!~8#ReLk#-kmqk{hjosj z?Hn7B!aev3-jnBi7d#i|Tm-*4=H2{X2xB%1%^&)d(`OSJ@f3`k;r@TeXNcGKRIbCb zG;=lVT0Advdj#t-7F96+&Fw!v#$5WiuZ=J~zdsQ6yNh;n`Sly;`5%OHj)OJ74&L** z3|gB1Ci+;DK4Vy?esfOZV>nNKeVKE|cYoG}^%0^ojVZ{W4lT{!pFX}1&Swo+3+{a> z;(2n8>9FPw!85Bup_tDP@$>gwJ)<8HA9n=T;hnSw9CtG=gOR`}Sa+^-8SaJkF%N~} zX?}42u4soR;l4a)|GyBzF}%M4{$@Kqu64e)>Atyci%Vec-D9Dc!w=Tc{rCW`{R^0f zA{dur3*7TeSOeDnNQ5Y*;hEUSXN^lyhZnpk5U$N<4#PPfLnnl|n8tF1oH(X6;2H)X zg!6v`$MPFu*f#_7YhF_b;kf3>_3gm(=!p=;GzQ~SoPxD}I}CHw-2R)J|5y4tzR&yn zc-)8eaGa&M4%SbIJ~UpzUbt`Vg!c_=z=D z*Nw363UonxG?#zZZZ2CPL=PHO*nte3s{xL;1(VSkhBF;@;jK9 zkf*M+YZ2com$^vr_Zlq0VeCTw_nTX}ckCGdI!?Ski?|+-?Qji8;Tk>LJ7BcM%W!=A z`~uI;e1%BT7=(Us%y@LnS}3QA9M8)JgYLechAIm-IM3y+zpt4Vzd;!=wObm zfi&VZ$7L?c`Rl&IxgDbIdf$aPuEsvN#{HOu5}Z!B{wZ*8p7~r{fc&uEMEL!2o_x;l zd+&GrboNcRf1@3(MKE{1$7?VR!!Qt|VV(aB=dFd`oayiz{UEM}wR^VV8m7Xt2z&D# zGS|Kz-gAA{K%DzRZ%Icz2Iz2DdCRvv%NoG>1WTq@NA~PX1G@VXW z6em&@9TFusQv1WB9+g8YYbz@z)?_AhpI<*9DsJo+B^yhlQVw!RHY&|#tGj3a29D#m A;s5{u literal 41270 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 diff --git a/web/berry/src/constants/ChannelConstants.js b/web/berry/src/constants/ChannelConstants.js index 2c506881..ec049f7d 100644 --- a/web/berry/src/constants/ChannelConstants.js +++ b/web/berry/src/constants/ChannelConstants.js @@ -3,186 +3,186 @@ export const CHANNEL_OPTIONS = { key: 1, text: 'OpenAI', value: 1, - color: 'primary' + color: 'success' }, 14: { key: 14, text: 'Anthropic Claude', value: 14, - color: 'info' + color: 'primary' }, 3: { key: 3, text: 'Azure OpenAI', value: 3, - color: 'secondary' + color: 'success' }, 11: { key: 11, text: 'Google PaLM2', value: 11, - color: 'orange' + color: 'warning' }, 24: { key: 24, text: 'Google Gemini', value: 24, - color: 'orange' + color: 'warning' }, 28: { key: 28, text: 'Mistral AI', value: 28, - color: 'orange' + color: 'warning' }, 15: { key: 15, text: '百度文心千帆', value: 15, - color: 'default' + color: 'primary' }, 17: { key: 17, text: '阿里通义千问', value: 17, - color: 'default' + color: 'primary' }, 18: { key: 18, text: '讯飞星火认知', value: 18, - color: 'default' + color: 'primary' }, 16: { key: 16, text: '智谱 ChatGLM', value: 16, - color: 'default' + color: 'primary' }, 19: { key: 19, text: '360 智脑', value: 19, - color: 'default' + color: 'primary' }, 25: { key: 25, text: 'Moonshot AI', value: 25, - color: 'default' + color: 'primary' }, 23: { key: 23, text: '腾讯混元', value: 23, - color: 'default' + color: 'primary' }, 26: { key: 26, text: '百川大模型', value: 26, - color: 'default' + color: 'primary' }, 27: { key: 27, text: 'MiniMax', value: 27, - color: 'default' + color: 'primary' }, 29: { key: 29, text: 'Groq', value: 29, - color: 'default' + color: 'primary' }, 30: { key: 30, text: 'Ollama', value: 30, - color: 'default' + color: 'primary' }, 31: { key: 31, text: '零一万物', value: 31, - color: 'default' + color: 'primary' }, 8: { key: 8, text: '自定义渠道', value: 8, - color: 'primary' + color: 'error' }, 22: { key: 22, text: '知识库:FastGPT', value: 22, - color: 'default' + color: 'success' }, 21: { key: 21, text: '知识库:AI Proxy', value: 21, - color: 'purple' + color: 'success' }, 20: { key: 20, text: '代理:OpenRouter', value: 20, - color: 'primary' + color: 'success' }, 2: { key: 2, text: '代理:API2D', value: 2, - color: 'primary' + color: 'success' }, 5: { key: 5, text: '代理:OpenAI-SB', value: 5, - color: 'primary' + color: 'success' }, 7: { key: 7, text: '代理:OhMyGPT', value: 7, - color: 'primary' + color: 'success' }, 10: { key: 10, text: '代理:AI Proxy', value: 10, - color: 'primary' + color: 'success' }, 4: { key: 4, text: '代理:CloseAI', value: 4, - color: 'primary' + color: 'success' }, 6: { key: 6, text: '代理:OpenAI Max', value: 6, - color: 'primary' + color: 'success' }, 9: { key: 9, text: '代理:AI.LS', value: 9, - color: 'primary' + color: 'success' }, 12: { key: 12, text: '代理:API2GPT', value: 12, - color: 'primary' + color: 'success' }, 13: { key: 13, text: '代理:AIGC2D', value: 13, - color: 'primary' + color: 'success' } }; diff --git a/web/berry/src/views/Authentication/AuthForms/AuthLogin.js b/web/berry/src/views/Authentication/AuthForms/AuthLogin.js index 70aa2230..9420b098 100644 --- a/web/berry/src/views/Authentication/AuthForms/AuthLogin.js +++ b/web/berry/src/views/Authentication/AuthForms/AuthLogin.js @@ -180,7 +180,7 @@ const LoginForm = ({ ...others }) => { {({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, values }) => (
- 用户名 + 用户名 / 邮箱 { let groups = []; if (group === "") { @@ -14,7 +27,7 @@ const GroupLabel = ({ group }) => { return ( } spacing={0.5}> {groups.map((group, index) => { - return ; + return ; })} ); diff --git a/web/berry/src/views/Channel/component/TableHead.js b/web/berry/src/views/Channel/component/TableHead.js index 736dd8aa..8c47e440 100644 --- a/web/berry/src/views/Channel/component/TableHead.js +++ b/web/berry/src/views/Channel/component/TableHead.js @@ -10,6 +10,7 @@ const ChannelTableHead = () => { 类型 状态 响应时间 + 已消耗 余额 优先级 操作 diff --git a/web/berry/src/views/Channel/component/TableRow.js b/web/berry/src/views/Channel/component/TableRow.js index baca42cd..f7acb92e 100644 --- a/web/berry/src/views/Channel/component/TableRow.js +++ b/web/berry/src/views/Channel/component/TableRow.js @@ -170,6 +170,9 @@ export default function ChannelTableRow({ handle_action={handleResponseTime} /> + + {renderNumber(item.used_quota)} + - + 渠道 - - - - OpenAI 渠道已经不再支持通过 key 获取余额,因此余额显示为 0。对于支持的渠道类型,请点击余额进行刷新。 - - - + {matchUpMd ? ( - + diff --git a/web/berry/src/views/Log/index.js b/web/berry/src/views/Log/index.js index da24b4fd..f8cef0e8 100644 --- a/web/berry/src/views/Log/index.js +++ b/web/berry/src/views/Log/index.js @@ -102,11 +102,11 @@ export default function Log() { return ( <> - + 日志 - + - + diff --git a/web/berry/src/views/Redemption/index.js b/web/berry/src/views/Redemption/index.js index fc2d02f1..f617faaf 100644 --- a/web/berry/src/views/Redemption/index.js +++ b/web/berry/src/views/Redemption/index.js @@ -141,7 +141,7 @@ export default function Redemption() { return ( <> - + 兑换 - + - + diff --git a/web/berry/src/views/Token/index.js b/web/berry/src/views/Token/index.js index 97ece35f..b3315eb9 100644 --- a/web/berry/src/views/Token/index.js +++ b/web/berry/src/views/Token/index.js @@ -141,9 +141,8 @@ export default function Token() { return ( <> - + 令牌 - diff --git a/web/berry/src/views/User/index.js b/web/berry/src/views/User/index.js index 463f525a..e53e5bbb 100644 --- a/web/berry/src/views/User/index.js +++ b/web/berry/src/views/User/index.js @@ -139,7 +139,7 @@ export default function Users() { return ( <> - + 用户 - + - + From 0eb2272bb7f6668a7908ce946df27b28a5fe08db Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 17 Mar 2024 18:12:49 +0800 Subject: [PATCH 05/28] chore: update copy --- web/default/src/components/ChannelsTable.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/default/src/components/ChannelsTable.js b/web/default/src/components/ChannelsTable.js index 5f837d03..5fd6129a 100644 --- a/web/default/src/components/ChannelsTable.js +++ b/web/default/src/components/ChannelsTable.js @@ -333,6 +333,8 @@ const ChannelsTable = () => { setPromptShown("channel-test"); }}> OpenAI 渠道已经不再支持通过 key 获取余额,因此余额显示为 0。对于支持的渠道类型,请点击余额进行刷新。 +
+ 渠道测试仅支持 chat 模型,优先使用 gpt-3.5-turbo,如果该模型不可用则使用你所配置的模型列表中的第一个模型。 ) } From 08831881f1394d95dcb7f609bc2b7857f2a1caf7 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 17 Mar 2024 19:09:44 +0800 Subject: [PATCH 06/28] feat: increase initial root user quota and support INITIAL_ROOT_TOKEN now (#1105) --- README.md | 1 + common/config/config.go | 2 ++ model/main.go | 20 ++++++++++++++++++-- model/redemption.go | 2 +- model/token.go | 4 ++-- model/user.go | 6 +++--- 6 files changed, 27 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 0ba659c4..1bc190c4 100644 --- a/README.md +++ b/README.md @@ -381,6 +381,7 @@ graph LR 19. `ENABLE_METRIC`:是否根据请求成功率禁用渠道,默认不开启,可选值为 `true` 和 `false`。 20. `METRIC_QUEUE_SIZE`:请求成功率统计队列大小,默认为 `10`。 21. `METRIC_SUCCESS_RATE_THRESHOLD`:请求成功率阈值,默认为 `0.8`。 +22. `INITIAL_ROOT_TOKEN`:如果设置了该值,则在系统首次启动时会自动创建一个值为该环境变量值的 root 用户令牌。 ### 命令行参数 1. `--port `: 指定服务器监听的端口号,默认为 `3000`。 diff --git a/common/config/config.go b/common/config/config.go index a261523d..3524183a 100644 --- a/common/config/config.go +++ b/common/config/config.go @@ -136,3 +136,5 @@ var MetricQueueSize = env.Int("METRIC_QUEUE_SIZE", 10) var MetricSuccessRateThreshold = env.Float64("METRIC_SUCCESS_RATE_THRESHOLD", 0.8) var MetricSuccessChanSize = env.Int("METRIC_SUCCESS_CHAN_SIZE", 1024) var MetricFailChanSize = env.Int("METRIC_FAIL_CHAN_SIZE", 128) + +var InitialRootToken = os.Getenv("INITIAL_ROOT_TOKEN") diff --git a/model/main.go b/model/main.go index ca7a35b2..ff542faf 100644 --- a/model/main.go +++ b/model/main.go @@ -23,7 +23,7 @@ func CreateRootAccountIfNeed() error { var user User //if user.Status != util.UserStatusEnabled { if err := DB.First(&user).Error; err != nil { - logger.SysLog("no user exists, create a root user for you: username is root, password is 123456") + logger.SysLog("no user exists, creating a root user for you: username is root, password is 123456") hashedPassword, err := common.Password2Hash("123456") if err != nil { return err @@ -35,9 +35,25 @@ func CreateRootAccountIfNeed() error { Status: common.UserStatusEnabled, DisplayName: "Root User", AccessToken: helper.GetUUID(), - Quota: 100000000, + Quota: 500000000000000, } DB.Create(&rootUser) + if config.InitialRootToken != "" { + logger.SysLog("creating initial root token as requested") + token := Token{ + Id: 1, + UserId: rootUser.Id, + Key: config.InitialRootToken, + Status: common.TokenStatusEnabled, + Name: "Initial Root Token", + CreatedTime: helper.GetTimestamp(), + AccessedTime: helper.GetTimestamp(), + ExpiredTime: -1, + RemainQuota: 500000000000000, + UnlimitedQuota: true, + } + DB.Create(&token) + } } return nil } diff --git a/model/redemption.go b/model/redemption.go index e0ae68e2..79a5b8a9 100644 --- a/model/redemption.go +++ b/model/redemption.go @@ -14,7 +14,7 @@ type Redemption struct { Key string `json:"key" gorm:"type:char(32);uniqueIndex"` Status int `json:"status" gorm:"default:1"` Name string `json:"name" gorm:"index"` - Quota int64 `json:"quota" gorm:"default:100"` + Quota int64 `json:"quota" gorm:"bigint;default:100"` CreatedTime int64 `json:"created_time" gorm:"bigint"` RedeemedTime int64 `json:"redeemed_time" gorm:"bigint"` Count int `json:"count" gorm:"-:all"` // only for api request diff --git a/model/token.go b/model/token.go index 40d0eb8f..98214cf9 100644 --- a/model/token.go +++ b/model/token.go @@ -20,9 +20,9 @@ type Token struct { CreatedTime int64 `json:"created_time" gorm:"bigint"` AccessedTime int64 `json:"accessed_time" gorm:"bigint"` ExpiredTime int64 `json:"expired_time" gorm:"bigint;default:-1"` // -1 means never expired - RemainQuota int64 `json:"remain_quota" gorm:"default:0"` + RemainQuota int64 `json:"remain_quota" gorm:"bigint;default:0"` UnlimitedQuota bool `json:"unlimited_quota" gorm:"default:false"` - UsedQuota int64 `json:"used_quota" gorm:"default:0"` // used quota + UsedQuota int64 `json:"used_quota" gorm:"bigint;default:0"` // used quota } func GetAllUserTokens(userId int, startIdx int, num int) ([]*Token, error) { diff --git a/model/user.go b/model/user.go index e325394b..1c5833d9 100644 --- a/model/user.go +++ b/model/user.go @@ -26,9 +26,9 @@ type User struct { 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! AccessToken string `json:"access_token" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management - Quota int64 `json:"quota" gorm:"type:int;default:0"` - UsedQuota int64 `json:"used_quota" gorm:"type:int;default:0;column:used_quota"` // used quota - RequestCount int `json:"request_count" gorm:"type:int;default:0;"` // request number + Quota int64 `json:"quota" gorm:"bigint;default:0"` + UsedQuota int64 `json:"used_quota" gorm:"bigint;default:0;column:used_quota"` // used quota + RequestCount int `json:"request_count" gorm:"type:int;default:0;"` // request number Group string `json:"group" gorm:"type:varchar(32);default:'default'"` AffCode string `json:"aff_code" gorm:"type:varchar(32);column:aff_code;uniqueIndex"` InviterId int `json:"inviter_id" gorm:"type:int;column:inviter_id;index"` From 9821bc72815179e74172578dab12ef55512e4d08 Mon Sep 17 00:00:00 2001 From: Benny Date: Sun, 17 Mar 2024 19:25:36 +0800 Subject: [PATCH 07/28] feat: add user list sorting and pagination enhancements (#1178) * feat: add user list sorting and pagination enhancements * feat: add user list sorting for THEME=air * feat: add token list sorting and pagination enhancements * feat: add token list sorting for THEME=air --- controller/token.go | 5 ++- controller/user.go | 39 +++++++++--------- model/token.go | 15 ++++++- model/user.go | 19 +++++++-- web/air/src/components/TokensTable.js | 43 ++++++++++++++++++-- web/air/src/components/UsersTable.js | 48 ++++++++++++++++++++--- web/default/src/components/TokensTable.js | 27 +++++++++++-- web/default/src/components/UsersTable.js | 30 +++++++++++--- 8 files changed, 184 insertions(+), 42 deletions(-) diff --git a/controller/token.go b/controller/token.go index de0e65eb..7f6b4505 100644 --- a/controller/token.go +++ b/controller/token.go @@ -16,7 +16,10 @@ func GetAllTokens(c *gin.Context) { if p < 0 { p = 0 } - tokens, err := model.GetAllUserTokens(userId, p*config.ItemsPerPage, config.ItemsPerPage) + + order := c.Query("order") + tokens, err := model.GetAllUserTokens(userId, p*config.ItemsPerPage, config.ItemsPerPage, order) + if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, diff --git a/controller/user.go b/controller/user.go index c11b940e..8b614e5d 100644 --- a/controller/user.go +++ b/controller/user.go @@ -180,24 +180,27 @@ func Register(c *gin.Context) { } func GetAllUsers(c *gin.Context) { - p, _ := strconv.Atoi(c.Query("p")) - if p < 0 { - p = 0 - } - users, err := model.GetAllUsers(p*config.ItemsPerPage, config.ItemsPerPage) - if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) - return - } - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "", - "data": users, - }) - return + p, _ := strconv.Atoi(c.Query("p")) + if p < 0 { + p = 0 + } + + order := c.DefaultQuery("order", "") + users, err := model.GetAllUsers(p*config.ItemsPerPage, config.ItemsPerPage, order) + + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + "data": users, + }) } func SearchUsers(c *gin.Context) { diff --git a/model/token.go b/model/token.go index 98214cf9..493e27c9 100644 --- a/model/token.go +++ b/model/token.go @@ -25,10 +25,21 @@ type Token struct { UsedQuota int64 `json:"used_quota" gorm:"bigint;default:0"` // used quota } -func GetAllUserTokens(userId int, startIdx int, num int) ([]*Token, error) { +func GetAllUserTokens(userId int, startIdx int, num int, order string) ([]*Token, error) { var tokens []*Token var err error - err = DB.Where("user_id = ?", userId).Order("id desc").Limit(num).Offset(startIdx).Find(&tokens).Error + query := DB.Where("user_id = ?", userId) + + switch order { + case "remain_quota": + query = query.Order("unlimited_quota desc, remain_quota desc") + case "used_quota": + query = query.Order("used_quota desc") + default: + query = query.Order("id desc") + } + + err = query.Limit(num).Offset(startIdx).Find(&tokens).Error return tokens, err } diff --git a/model/user.go b/model/user.go index 1c5833d9..5e729b5e 100644 --- a/model/user.go +++ b/model/user.go @@ -40,9 +40,22 @@ func GetMaxUserId() int { return user.Id } -func GetAllUsers(startIdx int, num int) (users []*User, err error) { - err = DB.Order("id desc").Limit(num).Offset(startIdx).Omit("password").Where("status != ?", common.UserStatusDeleted).Find(&users).Error - return users, err +func GetAllUsers(startIdx int, num int, order string) (users []*User, err error) { + query := DB.Limit(num).Offset(startIdx).Omit("password").Where("status != ?", common.UserStatusDeleted) + + switch order { + case "quota": + query = query.Order("quota desc") + case "used_quota": + query = query.Order("used_quota desc") + case "request_count": + query = query.Order("request_count desc") + default: + query = query.Order("id desc") + } + + err = query.Find(&users).Error + return users, err } func SearchUsers(keyword string) (users []*User, err error) { diff --git a/web/air/src/components/TokensTable.js b/web/air/src/components/TokensTable.js index 9c4deb6e..c106b388 100644 --- a/web/air/src/components/TokensTable.js +++ b/web/air/src/components/TokensTable.js @@ -247,6 +247,8 @@ const TokensTable = () => { const [editingToken, setEditingToken] = useState({ id: undefined }); + const [orderBy, setOrderBy] = useState(''); + const [dropdownVisible, setDropdownVisible] = useState(false); const closeEdit = () => { setShowEdit(false); @@ -269,7 +271,7 @@ const TokensTable = () => { let pageData = tokens.slice((activePage - 1) * pageSize, activePage * pageSize); const loadTokens = async (startIdx) => { setLoading(true); - const res = await API.get(`/api/token/?p=${startIdx}&size=${pageSize}`); + const res = await API.get(`/api/token/?p=${startIdx}&size=${pageSize}&order=${orderBy}`); const { success, message, data } = res.data; if (success) { if (startIdx === 0) { @@ -289,7 +291,7 @@ const TokensTable = () => { (async () => { if (activePage === Math.ceil(tokens.length / pageSize) + 1) { // In this case we have to load more data and then append them. - await loadTokens(activePage - 1); + await loadTokens(activePage - 1, orderBy); } setActivePage(activePage); })(); @@ -392,12 +394,12 @@ const TokensTable = () => { }; useEffect(() => { - loadTokens(0) + loadTokens(0, orderBy) .then() .catch((reason) => { showError(reason); }); - }, [pageSize]); + }, [pageSize, orderBy]); const removeRecord = key => { let newDataSource = [...tokens]; @@ -452,6 +454,7 @@ const TokensTable = () => { // if keyword is blank, load files instead. await loadTokens(0); setActivePage(1); + setOrderBy(''); return; } setSearching(true); @@ -520,6 +523,23 @@ const TokensTable = () => { } }; + const handleOrderByChange = (e, { value }) => { + setOrderBy(value); + setActivePage(1); + setDropdownVisible(false); + }; + + const renderSelectedOption = (orderBy) => { + switch (orderBy) { + case 'remain_quota': + return '按剩余额度排序'; + case 'used_quota': + return '按已用额度排序'; + default: + return '默认排序'; + } + }; + return ( <> @@ -579,6 +599,21 @@ const TokensTable = () => { await copyText(keys); } }>复制所选令牌到剪贴板 + setDropdownVisible(visible)} + render={ + + handleOrderByChange('', { value: '' })}>默认排序 + handleOrderByChange('', { value: 'remain_quota' })}>按剩余额度排序 + handleOrderByChange('', { value: 'used_quota' })}>按已用额度排序 + + } + > + + ); }; diff --git a/web/air/src/components/UsersTable.js b/web/air/src/components/UsersTable.js index f3de46d6..4fc16ba5 100644 --- a/web/air/src/components/UsersTable.js +++ b/web/air/src/components/UsersTable.js @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { API, showError, showSuccess } from '../helpers'; -import { Button, Form, Popconfirm, Space, Table, Tag, Tooltip } from '@douyinfe/semi-ui'; +import { Button, Form, Popconfirm, Space, Table, Tag, Tooltip, Dropdown } from '@douyinfe/semi-ui'; import { ITEMS_PER_PAGE } from '../constants'; import { renderGroup, renderNumber, renderQuota } from '../helpers/render'; import AddUser from '../pages/User/AddUser'; @@ -139,6 +139,8 @@ const UsersTable = () => { const [editingUser, setEditingUser] = useState({ id: undefined }); + const [orderBy, setOrderBy] = useState(''); + const [dropdownVisible, setDropdownVisible] = useState(false); const setCount = (data) => { if (data.length >= (activePage) * ITEMS_PER_PAGE) { @@ -162,7 +164,7 @@ const UsersTable = () => { }; const loadUsers = async (startIdx) => { - const res = await API.get(`/api/user/?p=${startIdx}`); + const res = await API.get(`/api/user/?p=${startIdx}&order=${orderBy}`); const { success, message, data } = res.data; if (success) { if (startIdx === 0) { @@ -184,19 +186,19 @@ const UsersTable = () => { (async () => { if (activePage === Math.ceil(users.length / ITEMS_PER_PAGE) + 1) { // In this case we have to load more data and then append them. - await loadUsers(activePage - 1); + await loadUsers(activePage - 1, orderBy); } setActivePage(activePage); })(); }; useEffect(() => { - loadUsers(0) + loadUsers(0, orderBy) .then() .catch((reason) => { showError(reason); }); - }, []); + }, [orderBy]); const manageUser = async (username, action, record) => { const res = await API.post('/api/user/manage', { @@ -239,6 +241,7 @@ const UsersTable = () => { // if keyword is blank, load files instead. await loadUsers(0); setActivePage(1); + setOrderBy(''); return; } setSearching(true); @@ -301,6 +304,25 @@ const UsersTable = () => { } }; + const handleOrderByChange = (e, { value }) => { + setOrderBy(value); + setActivePage(1); + setDropdownVisible(false); + }; + + const renderSelectedOption = (orderBy) => { + switch (orderBy) { + case 'quota': + return '按剩余额度排序'; + case 'used_quota': + return '按已用额度排序'; + case 'request_count': + return '按请求次数排序'; + default: + return '默认排序'; + } + }; + return ( <> @@ -331,6 +353,22 @@ const UsersTable = () => { setShowAddUser(true); } }>添加用户 + setDropdownVisible(visible)} + render={ + + handleOrderByChange('', { value: '' })}>默认排序 + handleOrderByChange('', { value: 'quota' })}>按剩余额度排序 + handleOrderByChange('', { value: 'used_quota' })}>按已用额度排序 + handleOrderByChange('', { value: 'request_count' })}>按请求次数排序 + + } + > + + ); }; diff --git a/web/default/src/components/TokensTable.js b/web/default/src/components/TokensTable.js index d6ad2a21..19a688bb 100644 --- a/web/default/src/components/TokensTable.js +++ b/web/default/src/components/TokensTable.js @@ -48,9 +48,10 @@ const TokensTable = () => { const [searching, setSearching] = useState(false); const [showTopUpModal, setShowTopUpModal] = useState(false); const [targetTokenIdx, setTargetTokenIdx] = useState(0); + const [orderBy, setOrderBy] = useState(''); const loadTokens = async (startIdx) => { - const res = await API.get(`/api/token/?p=${startIdx}`); + const res = await API.get(`/api/token/?p=${startIdx}&order=${orderBy}`); const { success, message, data } = res.data; if (success) { if (startIdx === 0) { @@ -70,7 +71,7 @@ const TokensTable = () => { (async () => { if (activePage === Math.ceil(tokens.length / ITEMS_PER_PAGE) + 1) { // In this case we have to load more data and then append them. - await loadTokens(activePage - 1); + await loadTokens(activePage - 1, orderBy); } setActivePage(activePage); })(); @@ -160,12 +161,12 @@ const TokensTable = () => { } useEffect(() => { - loadTokens(0) + loadTokens(0, orderBy) .then() .catch((reason) => { showError(reason); }); - }, []); + }, [orderBy]); const manageToken = async (id, action, idx) => { let data = { id }; @@ -205,6 +206,7 @@ const TokensTable = () => { // if keyword is blank, load files instead. await loadTokens(0); setActivePage(1); + setOrderBy(''); return; } setSearching(true); @@ -243,6 +245,11 @@ const TokensTable = () => { setLoading(false); }; + const handleOrderByChange = (e, { value }) => { + setOrderBy(value); + setActivePage(1); + }; + return ( <> @@ -427,6 +434,18 @@ const TokensTable = () => { 添加新的令牌 + { const [activePage, setActivePage] = useState(1); const [searchKeyword, setSearchKeyword] = useState(''); const [searching, setSearching] = useState(false); + const [orderBy, setOrderBy] = useState(''); const loadUsers = async (startIdx) => { - const res = await API.get(`/api/user/?p=${startIdx}`); + const res = await API.get(`/api/user/?p=${startIdx}&order=${orderBy}`); const { success, message, data } = res.data; if (success) { if (startIdx === 0) { @@ -47,19 +48,19 @@ const UsersTable = () => { (async () => { if (activePage === Math.ceil(users.length / ITEMS_PER_PAGE) + 1) { // In this case we have to load more data and then append them. - await loadUsers(activePage - 1); + await loadUsers(activePage - 1, orderBy); } setActivePage(activePage); })(); }; useEffect(() => { - loadUsers(0) + loadUsers(0, orderBy) .then() .catch((reason) => { showError(reason); }); - }, []); + }, [orderBy]); const manageUser = (username, action, idx) => { (async () => { @@ -110,6 +111,7 @@ const UsersTable = () => { // if keyword is blank, load files instead. await loadUsers(0); setActivePage(1); + setOrderBy(''); return; } setSearching(true); @@ -148,6 +150,11 @@ const UsersTable = () => { setLoading(false); }; + const handleOrderByChange = (e, { value }) => { + setOrderBy(value); + setActivePage(1); + }; + return ( <> @@ -322,6 +329,19 @@ const UsersTable = () => { + Date: Sun, 17 Mar 2024 20:26:12 +0900 Subject: [PATCH 08/28] fix: fix panel cards style (#1171) --- .../src/views/Dashboard/component/StatisticalLineChartCard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/berry/src/views/Dashboard/component/StatisticalLineChartCard.js b/web/berry/src/views/Dashboard/component/StatisticalLineChartCard.js index 53cd46b0..9daa9519 100644 --- a/web/berry/src/views/Dashboard/component/StatisticalLineChartCard.js +++ b/web/berry/src/views/Dashboard/component/StatisticalLineChartCard.js @@ -65,7 +65,7 @@ const StatisticalLineChartCard = ({ isLoading, title, chartData, todayValue }) = ) : ( - + From 4d86d021c4df76083a3e3b8e9dc413321db1c2c0 Mon Sep 17 00:00:00 2001 From: Ian Li Date: Sun, 17 Mar 2024 19:30:50 +0800 Subject: [PATCH 09/28] feat: support Azure OpenAI TTS. (#1177) --- relay/controller/audio.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/relay/controller/audio.go b/relay/controller/audio.go index 155954d2..e69c47b3 100644 --- a/relay/controller/audio.go +++ b/relay/controller/audio.go @@ -104,10 +104,15 @@ func RelayAudioHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus } fullRequestURL := util.GetFullRequestURL(baseURL, requestURL, channelType) - if relayMode == constant.RelayModeAudioTranscription && channelType == common.ChannelTypeAzure { - // https://learn.microsoft.com/en-us/azure/ai-services/openai/whisper-quickstart?tabs=command-line#rest-api + if channelType == common.ChannelTypeAzure { apiVersion := util.GetAzureAPIVersion(c) - fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/audio/transcriptions?api-version=%s", baseURL, audioModel, apiVersion) + if relayMode == constant.RelayModeAudioTranscription { + // https://learn.microsoft.com/en-us/azure/ai-services/openai/whisper-quickstart?tabs=command-line#rest-api + fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/audio/transcriptions?api-version=%s", baseURL, audioModel, apiVersion) + } else if relayMode == constant.RelayModeAudioSpeech { + // https://learn.microsoft.com/en-us/azure/ai-services/openai/text-to-speech-quickstart?tabs=command-line#rest-api + fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/audio/speech?api-version=%s", baseURL, audioModel, apiVersion) + } } requestBody := &bytes.Buffer{} @@ -123,7 +128,7 @@ func RelayAudioHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus return openai.ErrorWrapper(err, "new_request_failed", http.StatusInternalServerError) } - if relayMode == constant.RelayModeAudioTranscription && channelType == common.ChannelTypeAzure { + if (relayMode == constant.RelayModeAudioTranscription || relayMode == constant.RelayModeAudioSpeech) && channelType == common.ChannelTypeAzure { // https://learn.microsoft.com/en-us/azure/ai-services/openai/whisper-quickstart?tabs=command-line#rest-api apiKey := c.Request.Header.Get("Authorization") apiKey = strings.TrimPrefix(apiKey, "Bearer ") From ade19ba4a22bfcdd02051168fae7f2225d1c9b0d Mon Sep 17 00:00:00 2001 From: Ian Li Date: Sun, 17 Mar 2024 19:34:21 +0800 Subject: [PATCH 10/28] feat: update default API version for Azure OpenAI (#994) * feat: Update default API version for Azure OpenAI. * chore: update other theme --------- Co-authored-by: JustSong --- relay/controller/image.go | 2 +- web/air/src/pages/Channel/EditChannel.js | 4 ++-- web/berry/README.md | 2 +- web/berry/src/views/Channel/type/Config.js | 2 +- web/default/src/pages/Channel/EditChannel.js | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/relay/controller/image.go b/relay/controller/image.go index 20ea0a4c..d81dadf6 100644 --- a/relay/controller/image.go +++ b/relay/controller/image.go @@ -61,7 +61,7 @@ func RelayImageHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus if meta.ChannelType == common.ChannelTypeAzure { // https://learn.microsoft.com/en-us/azure/ai-services/openai/dall-e-quickstart?tabs=dalle3%2Ccommand-line&pivots=rest-api apiVersion := util.GetAzureAPIVersion(c) - // https://{resource_name}.openai.azure.com/openai/deployments/dall-e-3/images/generations?api-version=2023-06-01-preview + // https://{resource_name}.openai.azure.com/openai/deployments/dall-e-3/images/generations?api-version=2024-03-01-preview fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/images/generations?api-version=%s", meta.BaseURL, imageRequest.Model, apiVersion) } diff --git a/web/air/src/pages/Channel/EditChannel.js b/web/air/src/pages/Channel/EditChannel.js index 2b84011b..efb2cee8 100644 --- a/web/air/src/pages/Channel/EditChannel.js +++ b/web/air/src/pages/Channel/EditChannel.js @@ -230,7 +230,7 @@ const EditChannel = (props) => { localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1); } if (localInputs.type === 3 && localInputs.other === '') { - localInputs.other = '2023-06-01-preview'; + localInputs.other = '2024-03-01-preview'; } if (localInputs.type === 18 && localInputs.other === '') { localInputs.other = 'v2.1'; @@ -348,7 +348,7 @@ const EditChannel = (props) => { { handleInputChange('other', value) }} diff --git a/web/berry/README.md b/web/berry/README.md index 170feedc..84b2bc2c 100644 --- a/web/berry/README.md +++ b/web/berry/README.md @@ -49,7 +49,7 @@ const typeConfig = { base_url: "请填写AZURE_OPENAI_ENDPOINT", // 注意:通过判断 `other` 是否有值来判断是否需要显示 `other` 输入框, 默认是没有值的 - other: "请输入默认API版本,例如:2023-06-01-preview", + other: "请输入默认API版本,例如:2024-03-01-preview", }, modelGroup: "openai", // 模型组名称,这个值是给 填入渠道支持模型 按钮使用的。 填入渠道支持模型 按钮会根据这个值来获取模型组,如果填写默认是 openai }, diff --git a/web/berry/src/views/Channel/type/Config.js b/web/berry/src/views/Channel/type/Config.js index 8dfe77a4..7e42ca8d 100644 --- a/web/berry/src/views/Channel/type/Config.js +++ b/web/berry/src/views/Channel/type/Config.js @@ -41,7 +41,7 @@ const typeConfig = { }, prompt: { base_url: "请填写AZURE_OPENAI_ENDPOINT", - other: "请输入默认API版本,例如:2023-06-01-preview", + other: "请输入默认API版本,例如:2024-03-01-preview", }, }, 11: { diff --git a/web/default/src/pages/Channel/EditChannel.js b/web/default/src/pages/Channel/EditChannel.js index 4de8e87a..330b8c8e 100644 --- a/web/default/src/pages/Channel/EditChannel.js +++ b/web/default/src/pages/Channel/EditChannel.js @@ -160,7 +160,7 @@ const EditChannel = () => { localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1); } if (localInputs.type === 3 && localInputs.other === '') { - localInputs.other = '2023-06-01-preview'; + localInputs.other = '2024-03-01-preview'; } if (localInputs.type === 18 && localInputs.other === '') { localInputs.other = 'v2.1'; @@ -242,7 +242,7 @@ const EditChannel = () => { Date: Sun, 17 Mar 2024 19:39:00 +0800 Subject: [PATCH 11/28] chore: update copy --- web/default/src/components/LoginForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/default/src/components/LoginForm.js b/web/default/src/components/LoginForm.js index a3913220..b48f64c4 100644 --- a/web/default/src/components/LoginForm.js +++ b/web/default/src/components/LoginForm.js @@ -94,7 +94,7 @@ const LoginForm = () => { fluid icon='user' iconPosition='left' - placeholder='用户名' + placeholder='用户名 / 邮箱地址' name='username' value={username} onChange={handleChange} From 4ae311e964ba9a3a838b789ca7d21b9504cfff9e Mon Sep 17 00:00:00 2001 From: Benny Date: Sun, 17 Mar 2024 21:06:36 +0800 Subject: [PATCH 12/28] docs: update README (#1186) --- README.en.md | 14 ++++++++------ README.ja.md | 13 +++++++------ README.md | 39 ++++++++++++++++++++------------------- 3 files changed, 35 insertions(+), 31 deletions(-) diff --git a/README.en.md b/README.en.md index eec0047b..bce47353 100644 --- a/README.en.md +++ b/README.en.md @@ -241,17 +241,19 @@ If the channel ID is not provided, load balancing will be used to distribute the + Example: `SESSION_SECRET=random_string` 3. `SQL_DSN`: When set, the specified database will be used instead of SQLite. Please use MySQL version 8.0. + Example: `SQL_DSN=root:123456@tcp(localhost:3306)/oneapi` -4. `FRONTEND_BASE_URL`: When set, the specified frontend address will be used instead of the backend address. +4. `LOG_SQL_DSN`: When set, a separate database will be used for the `logs` table; please use MySQL or PostgreSQL. + + Example: `LOG_SQL_DSN=root:123456@tcp(localhost:3306)/oneapi-logs` +5. `FRONTEND_BASE_URL`: When set, the specified frontend address will be used instead of the backend address. + Example: `FRONTEND_BASE_URL=https://openai.justsong.cn` -5. `SYNC_FREQUENCY`: When set, the system will periodically sync configurations from the database, with the unit in seconds. If not set, no sync will happen. +6. `SYNC_FREQUENCY`: When set, the system will periodically sync configurations from the database, with the unit in seconds. If not set, no sync will happen. + Example: `SYNC_FREQUENCY=60` -6. `NODE_TYPE`: When set, specifies the node type. Valid values are `master` and `slave`. If not set, it defaults to `master`. +7. `NODE_TYPE`: When set, specifies the node type. Valid values are `master` and `slave`. If not set, it defaults to `master`. + Example: `NODE_TYPE=slave` -7. `CHANNEL_UPDATE_FREQUENCY`: When set, it periodically updates the channel balances, with the unit in minutes. If not set, no update will happen. +8. `CHANNEL_UPDATE_FREQUENCY`: When set, it periodically updates the channel balances, with the unit in minutes. If not set, no update will happen. + Example: `CHANNEL_UPDATE_FREQUENCY=1440` -8. `CHANNEL_TEST_FREQUENCY`: When set, it periodically tests the channels, with the unit in minutes. If not set, no test will happen. +9. `CHANNEL_TEST_FREQUENCY`: When set, it periodically tests the channels, with the unit in minutes. If not set, no test will happen. + Example: `CHANNEL_TEST_FREQUENCY=1440` -9. `POLLING_INTERVAL`: The time interval (in seconds) between requests when updating channel balances and testing channel availability. Default is no interval. +10. `POLLING_INTERVAL`: The time interval (in seconds) between requests when updating channel balances and testing channel availability. Default is no interval. + Example: `POLLING_INTERVAL=5` ### Command Line Parameters diff --git a/README.ja.md b/README.ja.md index e9149d71..c15915ec 100644 --- a/README.ja.md +++ b/README.ja.md @@ -242,17 +242,18 @@ graph LR + 例: `SESSION_SECRET=random_string` 3. `SQL_DSN`: 設定すると、SQLite の代わりに指定したデータベースが使用されます。MySQL バージョン 8.0 を使用してください。 + 例: `SQL_DSN=root:123456@tcp(localhost:3306)/oneapi` -4. `FRONTEND_BASE_URL`: 設定されると、バックエンドアドレスではなく、指定されたフロントエンドアドレスが使われる。 +4. `LOG_SQL_DSN`: を設定すると、`logs`テーブルには独立したデータベースが使用されます。MySQLまたはPostgreSQLを使用してください。 +5. `FRONTEND_BASE_URL`: 設定されると、バックエンドアドレスではなく、指定されたフロントエンドアドレスが使われる。 + 例: `FRONTEND_BASE_URL=https://openai.justsong.cn` -5. `SYNC_FREQUENCY`: 設定された場合、システムは定期的にデータベースからコンフィグを秒単位で同期する。設定されていない場合、同期は行われません。 +6. `SYNC_FREQUENCY`: 設定された場合、システムは定期的にデータベースからコンフィグを秒単位で同期する。設定されていない場合、同期は行われません。 + 例: `SYNC_FREQUENCY=60` -6. `NODE_TYPE`: 設定すると、ノードのタイプを指定する。有効な値は `master` と `slave` である。設定されていない場合、デフォルトは `master`。 +7. `NODE_TYPE`: 設定すると、ノードのタイプを指定する。有効な値は `master` と `slave` である。設定されていない場合、デフォルトは `master`。 + 例: `NODE_TYPE=slave` -7. `CHANNEL_UPDATE_FREQUENCY`: 設定すると、チャンネル残高を分単位で定期的に更新する。設定されていない場合、更新は行われません。 +8. `CHANNEL_UPDATE_FREQUENCY`: 設定すると、チャンネル残高を分単位で定期的に更新する。設定されていない場合、更新は行われません。 + 例: `CHANNEL_UPDATE_FREQUENCY=1440` -8. `CHANNEL_TEST_FREQUENCY`: 設定すると、チャンネルを定期的にテストする。設定されていない場合、テストは行われません。 +9. `CHANNEL_TEST_FREQUENCY`: 設定すると、チャンネルを定期的にテストする。設定されていない場合、テストは行われません。 + 例: `CHANNEL_TEST_FREQUENCY=1440` -9. `POLLING_INTERVAL`: チャネル残高の更新とチャネルの可用性をテストするときのリクエスト間の時間間隔 (秒)。デフォルトは間隔なし。 +10. `POLLING_INTERVAL`: チャネル残高の更新とチャネルの可用性をテストするときのリクエスト間の時間間隔 (秒)。デフォルトは間隔なし。 + 例: `POLLING_INTERVAL=5` ### コマンドラインパラメータ diff --git a/README.md b/README.md index 1bc190c4..efb04b6c 100644 --- a/README.md +++ b/README.md @@ -349,39 +349,40 @@ graph LR + `SQL_MAX_OPEN_CONNS`:最大打开连接数,默认为 `1000`。 + 如果报错 `Error 1040: Too many connections`,请适当减小该值。 + `SQL_CONN_MAX_LIFETIME`:连接的最大生命周期,默认为 `60`,单位分钟。 -4. `FRONTEND_BASE_URL`:设置之后将重定向页面请求到指定的地址,仅限从服务器设置。 +4. `LOG_SQL_DSN`:设置之后将为 `logs` 表使用独立的数据库,请使用 MySQL 或 PostgreSQL。 +5. `FRONTEND_BASE_URL`:设置之后将重定向页面请求到指定的地址,仅限从服务器设置。 + 例子:`FRONTEND_BASE_URL=https://openai.justsong.cn` -5. `MEMORY_CACHE_ENABLED`:启用内存缓存,会导致用户额度的更新存在一定的延迟,可选值为 `true` 和 `false`,未设置则默认为 `false`。 +6. `MEMORY_CACHE_ENABLED`:启用内存缓存,会导致用户额度的更新存在一定的延迟,可选值为 `true` 和 `false`,未设置则默认为 `false`。 + 例子:`MEMORY_CACHE_ENABLED=true` -6. `SYNC_FREQUENCY`:在启用缓存的情况下与数据库同步配置的频率,单位为秒,默认为 `600` 秒。 +7. `SYNC_FREQUENCY`:在启用缓存的情况下与数据库同步配置的频率,单位为秒,默认为 `600` 秒。 + 例子:`SYNC_FREQUENCY=60` -7. `NODE_TYPE`:设置之后将指定节点类型,可选值为 `master` 和 `slave`,未设置则默认为 `master`。 +8. `NODE_TYPE`:设置之后将指定节点类型,可选值为 `master` 和 `slave`,未设置则默认为 `master`。 + 例子:`NODE_TYPE=slave` -8. `CHANNEL_UPDATE_FREQUENCY`:设置之后将定期更新渠道余额,单位为分钟,未设置则不进行更新。 +9. `CHANNEL_UPDATE_FREQUENCY`:设置之后将定期更新渠道余额,单位为分钟,未设置则不进行更新。 + 例子:`CHANNEL_UPDATE_FREQUENCY=1440` -9. `CHANNEL_TEST_FREQUENCY`:设置之后将定期检查渠道,单位为分钟,未设置则不进行检查。 +10. `CHANNEL_TEST_FREQUENCY`:设置之后将定期检查渠道,单位为分钟,未设置则不进行检查。 + 例子:`CHANNEL_TEST_FREQUENCY=1440` -10. `POLLING_INTERVAL`:批量更新渠道余额以及测试可用性时的请求间隔,单位为秒,默认无间隔。 +11. `POLLING_INTERVAL`:批量更新渠道余额以及测试可用性时的请求间隔,单位为秒,默认无间隔。 + 例子:`POLLING_INTERVAL=5` -11. `BATCH_UPDATE_ENABLED`:启用数据库批量更新聚合,会导致用户额度的更新存在一定的延迟可选值为 `true` 和 `false`,未设置则默认为 `false`。 +12. `BATCH_UPDATE_ENABLED`:启用数据库批量更新聚合,会导致用户额度的更新存在一定的延迟可选值为 `true` 和 `false`,未设置则默认为 `false`。 + 例子:`BATCH_UPDATE_ENABLED=true` + 如果你遇到了数据库连接数过多的问题,可以尝试启用该选项。 -12. `BATCH_UPDATE_INTERVAL=5`:批量更新聚合的时间间隔,单位为秒,默认为 `5`。 +13. `BATCH_UPDATE_INTERVAL=5`:批量更新聚合的时间间隔,单位为秒,默认为 `5`。 + 例子:`BATCH_UPDATE_INTERVAL=5` -13. 请求频率限制: +14. 请求频率限制: + `GLOBAL_API_RATE_LIMIT`:全局 API 速率限制(除中继请求外),单 ip 三分钟内的最大请求数,默认为 `180`。 + `GLOBAL_WEB_RATE_LIMIT`:全局 Web 速率限制,单 ip 三分钟内的最大请求数,默认为 `60`。 -14. 编码器缓存设置: +15. 编码器缓存设置: + `TIKTOKEN_CACHE_DIR`:默认程序启动时会联网下载一些通用的词元的编码,如:`gpt-3.5-turbo`,在一些网络环境不稳定,或者离线情况,可能会导致启动有问题,可以配置此目录缓存数据,可迁移到离线环境。 + `DATA_GYM_CACHE_DIR`:目前该配置作用与 `TIKTOKEN_CACHE_DIR` 一致,但是优先级没有它高。 -15. `RELAY_TIMEOUT`:中继超时设置,单位为秒,默认不设置超时时间。 -16. `SQLITE_BUSY_TIMEOUT`:SQLite 锁等待超时设置,单位为毫秒,默认 `3000`。 -17. `GEMINI_SAFETY_SETTING`:Gemini 的安全设置,默认 `BLOCK_NONE`。 -18. `THEME`:系统的主题设置,默认为 `default`,具体可选值参考[此处](./web/README.md)。 -19. `ENABLE_METRIC`:是否根据请求成功率禁用渠道,默认不开启,可选值为 `true` 和 `false`。 -20. `METRIC_QUEUE_SIZE`:请求成功率统计队列大小,默认为 `10`。 -21. `METRIC_SUCCESS_RATE_THRESHOLD`:请求成功率阈值,默认为 `0.8`。 -22. `INITIAL_ROOT_TOKEN`:如果设置了该值,则在系统首次启动时会自动创建一个值为该环境变量值的 root 用户令牌。 +16. `RELAY_TIMEOUT`:中继超时设置,单位为秒,默认不设置超时时间。 +17. `SQLITE_BUSY_TIMEOUT`:SQLite 锁等待超时设置,单位为毫秒,默认 `3000`。 +18. `GEMINI_SAFETY_SETTING`:Gemini 的安全设置,默认 `BLOCK_NONE`。 +19. `THEME`:系统的主题设置,默认为 `default`,具体可选值参考[此处](./web/README.md)。 +20. `ENABLE_METRIC`:是否根据请求成功率禁用渠道,默认不开启,可选值为 `true` 和 `false`。 +21. `METRIC_QUEUE_SIZE`:请求成功率统计队列大小,默认为 `10`。 +22. `METRIC_SUCCESS_RATE_THRESHOLD`:请求成功率阈值,默认为 `0.8`。 +23. `INITIAL_ROOT_TOKEN`:如果设置了该值,则在系统首次启动时会自动创建一个值为该环境变量值的 root 用户令牌。 ### 命令行参数 1. `--port `: 指定服务器监听的端口号,默认为 `3000`。 From e96b173abecef61c02448fb43e2cc07129b5c38a Mon Sep 17 00:00:00 2001 From: GuangxiaoLong <33345078+lgxisbb@users.noreply.github.com> Date: Sun, 24 Mar 2024 21:47:46 +0800 Subject: [PATCH 13/28] =?UTF-8?q?feat:=20=E7=A7=BB=E9=99=A4=20azure=20mode?= =?UTF-8?q?l=20=E7=9A=84=20TrimSuffix=20(#1193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- relay/channel/openai/adaptor.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/relay/channel/openai/adaptor.go b/relay/channel/openai/adaptor.go index 47594030..1f153c3e 100644 --- a/relay/channel/openai/adaptor.go +++ b/relay/channel/openai/adaptor.go @@ -31,11 +31,8 @@ func (a *Adaptor) GetRequestURL(meta *util.RelayMeta) (string, error) { task := strings.TrimPrefix(requestURL, "/v1/") model_ := meta.ActualModelName model_ = strings.Replace(model_, ".", "", -1) - // https://github.com/songquanpeng/one-api/issues/67 - model_ = strings.TrimSuffix(model_, "-0301") - model_ = strings.TrimSuffix(model_, "-0314") - model_ = strings.TrimSuffix(model_, "-0613") - + //https://github.com/songquanpeng/one-api/issues/1191 + // {your endpoint}/openai/deployments/{your azure_model}/chat/completions?api-version={api_version} requestURL = fmt.Sprintf("/openai/deployments/%s/%s", model_, task) return util.GetFullRequestURL(meta.BaseURL, requestURL, meta.ChannelType), nil case common.ChannelTypeMinimax: From c243cd553537bf0a544242a52708aaab48e34997 Mon Sep 17 00:00:00 2001 From: xietong Date: Sun, 24 Mar 2024 21:51:31 +0800 Subject: [PATCH 14/28] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=20ollama=20?= =?UTF-8?q?=E7=9A=84=20embedding=20=E6=8E=A5=E5=8F=A3=20(#1221)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 增加ollama的embedding接口 * chore: fix function name --------- Co-authored-by: JustSong --- relay/channel/ollama/adaptor.go | 18 +++++++-- relay/channel/ollama/main.go | 65 +++++++++++++++++++++++++++++++-- relay/channel/ollama/model.go | 10 +++++ 3 files changed, 86 insertions(+), 7 deletions(-) diff --git a/relay/channel/ollama/adaptor.go b/relay/channel/ollama/adaptor.go index 06c66101..e2ae7d2b 100644 --- a/relay/channel/ollama/adaptor.go +++ b/relay/channel/ollama/adaptor.go @@ -3,13 +3,14 @@ package ollama import ( "errors" "fmt" + "io" + "net/http" + "github.com/gin-gonic/gin" "github.com/songquanpeng/one-api/relay/channel" "github.com/songquanpeng/one-api/relay/constant" "github.com/songquanpeng/one-api/relay/model" "github.com/songquanpeng/one-api/relay/util" - "io" - "net/http" ) type Adaptor struct { @@ -22,6 +23,9 @@ func (a *Adaptor) Init(meta *util.RelayMeta) { func (a *Adaptor) GetRequestURL(meta *util.RelayMeta) (string, error) { // https://github.com/ollama/ollama/blob/main/docs/api.md fullRequestURL := fmt.Sprintf("%s/api/chat", meta.BaseURL) + if meta.Mode == constant.RelayModeEmbeddings { + fullRequestURL = fmt.Sprintf("%s/api/embeddings", meta.BaseURL) + } return fullRequestURL, nil } @@ -37,7 +41,8 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.G } switch relayMode { case constant.RelayModeEmbeddings: - return nil, errors.New("not supported") + ollamaEmbeddingRequest := ConvertEmbeddingRequest(*request) + return ollamaEmbeddingRequest, nil default: return ConvertRequest(*request), nil } @@ -51,7 +56,12 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *util.Rel if meta.IsStream { err, usage = StreamHandler(c, resp) } else { - err, usage = Handler(c, resp) + switch meta.Mode { + case constant.RelayModeEmbeddings: + err, usage = EmbeddingHandler(c, resp) + default: + err, usage = Handler(c, resp) + } } return } diff --git a/relay/channel/ollama/main.go b/relay/channel/ollama/main.go index 7ec646a3..821a335b 100644 --- a/relay/channel/ollama/main.go +++ b/relay/channel/ollama/main.go @@ -5,6 +5,10 @@ import ( "context" "encoding/json" "fmt" + "io" + "net/http" + "strings" + "github.com/gin-gonic/gin" "github.com/songquanpeng/one-api/common" "github.com/songquanpeng/one-api/common/helper" @@ -12,9 +16,6 @@ import ( "github.com/songquanpeng/one-api/relay/channel/openai" "github.com/songquanpeng/one-api/relay/constant" "github.com/songquanpeng/one-api/relay/model" - "io" - "net/http" - "strings" ) func ConvertRequest(request model.GeneralOpenAIRequest) *ChatRequest { @@ -139,6 +140,64 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC return nil, &usage } +func ConvertEmbeddingRequest(request model.GeneralOpenAIRequest) *EmbeddingRequest { + return &EmbeddingRequest{ + Model: request.Model, + Prompt: strings.Join(request.ParseInput(), " "), + } +} + +func EmbeddingHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { + var ollamaResponse EmbeddingResponse + err := json.NewDecoder(resp.Body).Decode(&ollamaResponse) + if err != nil { + return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil + } + + err = resp.Body.Close() + if err != nil { + return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil + } + + if ollamaResponse.Error != "" { + return &model.ErrorWithStatusCode{ + Error: model.Error{ + Message: ollamaResponse.Error, + Type: "ollama_error", + Param: "", + Code: "ollama_error", + }, + StatusCode: resp.StatusCode, + }, nil + } + + fullTextResponse := embeddingResponseOllama2OpenAI(&ollamaResponse) + jsonResponse, err := json.Marshal(fullTextResponse) + if err != nil { + return openai.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 +} + +func embeddingResponseOllama2OpenAI(response *EmbeddingResponse) *openai.EmbeddingResponse { + openAIEmbeddingResponse := openai.EmbeddingResponse{ + Object: "list", + Data: make([]openai.EmbeddingResponseItem, 0, 1), + Model: "text-embedding-v1", + Usage: model.Usage{TotalTokens: 0}, + } + + openAIEmbeddingResponse.Data = append(openAIEmbeddingResponse.Data, openai.EmbeddingResponseItem{ + Object: `embedding`, + Index: 0, + Embedding: response.Embedding, + }) + return &openAIEmbeddingResponse +} + func Handler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { ctx := context.TODO() var ollamaResponse ChatResponse diff --git a/relay/channel/ollama/model.go b/relay/channel/ollama/model.go index a8ef1ffc..8baf56a0 100644 --- a/relay/channel/ollama/model.go +++ b/relay/channel/ollama/model.go @@ -35,3 +35,13 @@ type ChatResponse struct { EvalDuration int `json:"eval_duration,omitempty"` Error string `json:"error,omitempty"` } + +type EmbeddingRequest struct { + Model string `json:"model"` + Prompt string `json:"prompt"` +} + +type EmbeddingResponse struct { + Error string `json:"error,omitempty"` + Embedding []float64 `json:"embedding,omitempty"` +} From 99f81a267c6d65c6b426cdc5ea7b001b5f4eff53 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 24 Mar 2024 22:14:45 +0800 Subject: [PATCH 15/28] fix: fix xunfei error handling (close #1218) --- relay/channel/xunfei/main.go | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/relay/channel/xunfei/main.go b/relay/channel/xunfei/main.go index f89aea2b..5e7014cb 100644 --- a/relay/channel/xunfei/main.go +++ b/relay/channel/xunfei/main.go @@ -121,7 +121,7 @@ func StreamHandler(c *gin.Context, textRequest model.GeneralOpenAIRequest, appId domain, authUrl := getXunfeiAuthUrl(c, apiKey, apiSecret, textRequest.Model) dataChan, stopChan, err := xunfeiMakeRequest(textRequest, domain, authUrl, appId) if err != nil { - return openai.ErrorWrapper(err, "make xunfei request err", http.StatusInternalServerError), nil + return openai.ErrorWrapper(err, "xunfei_request_failed", http.StatusInternalServerError), nil } common.SetEventStreamHeaders(c) var usage model.Usage @@ -151,7 +151,7 @@ func Handler(c *gin.Context, textRequest model.GeneralOpenAIRequest, appId strin domain, authUrl := getXunfeiAuthUrl(c, apiKey, apiSecret, textRequest.Model) dataChan, stopChan, err := xunfeiMakeRequest(textRequest, domain, authUrl, appId) if err != nil { - return openai.ErrorWrapper(err, "make xunfei request err", http.StatusInternalServerError), nil + return openai.ErrorWrapper(err, "xunfei_request_failed", http.StatusInternalServerError), nil } var usage model.Usage var content string @@ -171,11 +171,7 @@ func Handler(c *gin.Context, textRequest model.GeneralOpenAIRequest, appId strin } } if len(xunfeiResponse.Payload.Choices.Text) == 0 { - xunfeiResponse.Payload.Choices.Text = []ChatResponseTextItem{ - { - Content: "", - }, - } + return openai.ErrorWrapper(err, "xunfei_empty_response_detected", http.StatusInternalServerError), nil } xunfeiResponse.Payload.Choices.Text[0].Content = content @@ -202,15 +198,21 @@ func xunfeiMakeRequest(textRequest model.GeneralOpenAIRequest, domain, authUrl, if err != nil { return nil, nil, err } + _, msg, err := conn.ReadMessage() + if err != nil { + return nil, nil, err + } dataChan := make(chan ChatResponse) stopChan := make(chan bool) go func() { for { - _, msg, err := conn.ReadMessage() - if err != nil { - logger.SysError("error reading stream response: " + err.Error()) - break + if msg == nil { + _, msg, err = conn.ReadMessage() + if err != nil { + logger.SysError("error reading stream response: " + err.Error()) + break + } } var response ChatResponse err = json.Unmarshal(msg, &response) @@ -218,6 +220,7 @@ func xunfeiMakeRequest(textRequest model.GeneralOpenAIRequest, domain, authUrl, logger.SysError("error unmarshalling stream response: " + err.Error()) break } + msg = nil dataChan <- response if response.Payload.Choices.Status == 2 { err := conn.Close() From 56ddbb842a9d68a30a0228436d39a888f0d8d87b Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 24 Mar 2024 22:20:41 +0800 Subject: [PATCH 16/28] fix: return pre-consumed quota when error happened for audio (close #1217) --- relay/controller/audio.go | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/relay/controller/audio.go b/relay/controller/audio.go index e69c47b3..cd118985 100644 --- a/relay/controller/audio.go +++ b/relay/controller/audio.go @@ -83,6 +83,24 @@ func RelayAudioHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus return openai.ErrorWrapper(err, "pre_consume_token_quota_failed", http.StatusForbidden) } } + succeed := false + defer func() { + if succeed { + return + } + if preConsumedQuota > 0 { + // we need to roll back the pre-consumed quota + defer func(ctx context.Context) { + go func() { + // negative means add quota back for token & user + err := model.PostConsumeTokenQuota(tokenId, -preConsumedQuota) + if err != nil { + logger.Error(ctx, fmt.Sprintf("error rollback pre-consumed quota: %s", err.Error())) + } + }() + }(c.Request.Context()) + } + }() // map model name modelMapping := c.GetString("model_mapping") @@ -193,20 +211,9 @@ func RelayAudioHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus resp.Body = io.NopCloser(bytes.NewBuffer(responseBody)) } if resp.StatusCode != http.StatusOK { - if preConsumedQuota > 0 { - // we need to roll back the pre-consumed quota - defer func(ctx context.Context) { - go func() { - // negative means add quota back for token & user - err := model.PostConsumeTokenQuota(tokenId, -preConsumedQuota) - if err != nil { - logger.Error(ctx, fmt.Sprintf("error rollback pre-consumed quota: %s", err.Error())) - } - }() - }(c.Request.Context()) - } return util.RelayErrorHandler(resp) } + succeed = true quotaDelta := quota - preConsumedQuota defer func(ctx context.Context) { go util.PostConsumeQuota(ctx, tokenId, quotaDelta, quota, userId, channelId, modelRatio, groupRatio, audioModel, tokenName) From cdfdeea3b49bd57fb16eb1dc3586d22b5da67320 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 24 Mar 2024 22:24:41 +0800 Subject: [PATCH 17/28] feat: return token when calling post /api/token (close #1208) --- controller/token.go | 1 + 1 file changed, 1 insertion(+) diff --git a/controller/token.go b/controller/token.go index 7f6b4505..949931da 100644 --- a/controller/token.go +++ b/controller/token.go @@ -142,6 +142,7 @@ func AddToken(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "success": true, "message": "", + "data": cleanToken, }) return } From f76c46d6489f0be68a1813f1c4b2d1e066c22cb3 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 24 Mar 2024 22:50:09 +0800 Subject: [PATCH 18/28] feat: add gemini-1.5-pro (#1211) --- common/model-ratio.go | 12 +++++++++--- relay/channel/gemini/constants.go | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/common/model-ratio.go b/common/model-ratio.go index 2c608302..460d4843 100644 --- a/common/model-ratio.go +++ b/common/model-ratio.go @@ -81,9 +81,12 @@ var ModelRatio = map[string]float64{ "bge-large-en": 0.002 * RMB, "bge-large-8k": 0.002 * RMB, // https://ai.google.dev/pricing - "PaLM-2": 1, - "gemini-pro": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens - "gemini-pro-vision": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens + "PaLM-2": 1, + "gemini-pro": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens + "gemini-pro-vision": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens + "gemini-1.0-pro-vision-001": 1, + "gemini-1.0-pro-001": 1, + "gemini-1.5-pro": 1, // https://open.bigmodel.cn/pricing "glm-4": 0.1 * RMB, "glm-4v": 0.1 * RMB, @@ -249,6 +252,9 @@ func GetCompletionRatio(name string) float64 { if strings.HasPrefix(name, "mistral-") { return 3 } + if strings.HasPrefix(name, "gemini-") { + return 3 + } switch name { case "llama2-70b-4096": return 0.8 / 0.7 diff --git a/relay/channel/gemini/constants.go b/relay/channel/gemini/constants.go index e8d3a155..32e7c240 100644 --- a/relay/channel/gemini/constants.go +++ b/relay/channel/gemini/constants.go @@ -3,6 +3,6 @@ package gemini // https://ai.google.dev/models/gemini var ModelList = []string{ - "gemini-pro", "gemini-1.0-pro-001", + "gemini-pro", "gemini-1.0-pro-001", "gemini-1.5-pro", "gemini-pro-vision", "gemini-1.0-pro-vision-001", } From 5b349efff9906d6db9f31644008959250b2c30f9 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 24 Mar 2024 22:57:24 +0800 Subject: [PATCH 19/28] chore: fix berry copy --- web/berry/src/views/Authentication/Auth/Register.js | 2 +- web/berry/src/views/Authentication/AuthForms/AuthRegister.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/berry/src/views/Authentication/Auth/Register.js b/web/berry/src/views/Authentication/Auth/Register.js index 4489e560..8027649d 100644 --- a/web/berry/src/views/Authentication/Auth/Register.js +++ b/web/berry/src/views/Authentication/Auth/Register.js @@ -51,7 +51,7 @@ const Register = () => { - 已经有帐号了?点击登录 + 已经有帐号了?点击登录 diff --git a/web/berry/src/views/Authentication/AuthForms/AuthRegister.js b/web/berry/src/views/Authentication/AuthForms/AuthRegister.js index c286faad..8d588696 100644 --- a/web/berry/src/views/Authentication/AuthForms/AuthRegister.js +++ b/web/berry/src/views/Authentication/AuthForms/AuthRegister.js @@ -296,7 +296,7 @@ const RegisterForm = ({ ...others }) => { From 24be9de09845331bc384123a650779f5dd8d93e4 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 24 Mar 2024 23:01:03 +0800 Subject: [PATCH 20/28] chore: update copy --- README.md | 8 ++--- controller/channel-test.go | 2 +- i18n/en.json | 34 +++++++++---------- monitor/channel.go | 14 ++++---- web/air/src/components/ChannelsTable.js | 26 +++++++------- web/air/src/components/OperationSetting.js | 6 ++-- .../src/views/Channel/component/TableRow.js | 6 ++-- web/berry/src/views/Channel/index.js | 4 +-- .../Setting/component/OperationSetting.js | 6 ++-- web/default/src/components/ChannelsTable.js | 8 ++--- .../src/components/OperationSetting.js | 6 ++-- 11 files changed, 60 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index efb04b6c..2dcdbd4f 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 5. 支持**多机部署**,[详见此处](#多机部署)。 6. 支持**令牌管理**,设置令牌的过期时间和额度。 7. 支持**兑换码管理**,支持批量生成和导出兑换码,可使用兑换码为账户进行充值。 -8. 支持**通道管理**,批量创建通道。 +8. 支持**渠道管理**,批量创建渠道。 9. 支持**用户分组**以及**渠道分组**,支持为不同分组设置不同的倍率。 10. 支持渠道**设置模型列表**。 11. 支持**查看额度明细**。 @@ -421,7 +421,7 @@ https://openai.justsong.cn + 检查你的接口地址和 API Key 有没有填对。 + 检查是否启用了 HTTPS,浏览器会拦截 HTTPS 域名下的 HTTP 请求。 6. 报错:`当前分组负载已饱和,请稍后再试` - + 上游通道 429 了。 + + 上游渠道 429 了。 7. 升级之后我的数据会丢失吗? + 如果使用 MySQL,不会。 + 如果使用 SQLite,需要按照我所给的部署命令挂载 volume 持久化 one-api.db 数据库文件,否则容器重启后数据会丢失。 @@ -429,8 +429,8 @@ https://openai.justsong.cn + 一般情况下不需要,系统将在初始化的时候自动调整。 + 如果需要的话,我会在更新日志中说明,并给出脚本。 9. 手动修改数据库后报错:`数据库一致性已被破坏,请联系管理员`? - + 这是检测到 ability 表里有些记录的通道 id 是不存在的,这大概率是因为你删了 channel 表里的记录但是没有同步在 ability 表里清理无效的通道。 - + 对于每一个通道,其所支持的模型都需要有一个专门的 ability 表的记录,表示该通道支持该模型。 + + 这是检测到 ability 表里有些记录的渠道 id 是不存在的,这大概率是因为你删了 channel 表里的记录但是没有同步在 ability 表里清理无效的渠道。 + + 对于每一个渠道,其所支持的模型都需要有一个专门的 ability 表的记录,表示该渠道支持该模型。 ## 相关项目 * [FastGPT](https://github.com/labring/FastGPT): 基于 LLM 大语言模型的知识库问答系统 diff --git a/controller/channel-test.go b/controller/channel-test.go index 67ac91d0..95f4d769 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -197,7 +197,7 @@ func testChannels(notify bool, scope string) error { testAllChannelsRunning = false testAllChannelsLock.Unlock() if notify { - err := message.Notify(message.ByAll, "通道测试完成", "", "通道测试完成,如果没有收到禁用通知,说明所有通道都正常") + err := message.Notify(message.ByAll, "渠道测试完成", "", "渠道测试完成,如果没有收到禁用通知,说明所有渠道都正常") if err != nil { logger.SysError(fmt.Sprintf("failed to send email: %s", err.Error())) } diff --git a/i18n/en.json b/i18n/en.json index 54728e2f..b7f1bd3e 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -8,12 +8,12 @@ "确认删除": "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", + "\"渠道「%s」(#%d)已被禁用\"": "\"Channel %s (#%d) has been disabled\"", + "渠道「%s」(#%d)已被禁用,原因:%s": "Channel %s (#%d) has been disabled, reason: %s", "测试已在运行中": "Test is already running", "响应时间 %.2fs 超过阈值 %.2fs": "Response time %.2fs exceeds threshold %.2fs", - "通道测试完成": "Channel test completed", - "通道测试完成,如果没有收到禁用通知,说明所有通道都正常": "Channel test completed, if you have not received the disable notification, it means that all channels are normal", + "渠道测试完成": "Channel test completed", + "渠道测试完成,如果没有收到禁用通知,说明所有渠道都正常": "Channel test completed, if you have not received the disable notification, it means that all channels are normal", "无法连接至 GitHub 服务器,请稍后重试!": "Unable to connect to GitHub server, please try again later!", "返回值非法,用户字段为空,请稍后重试!": "The return value is illegal, the user field is empty, please try again later!", "管理员未开启通过 GitHub 登录以及注册": "The administrator did not turn on login and registration via GitHub", @@ -119,11 +119,11 @@ " 个月 ": " M ", " 年 ": " y ", "未测试": "Not tested", - "通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。": "Channel ${name} test succeeded, time consumed ${time.toFixed(2)} s.", - "已成功开始测试所有通道,请刷新页面查看结果。": "All channels have been successfully tested, please refresh the page to view the results.", - "已成功开始测试所有已启用通道,请刷新页面查看结果。": "All enabled channels have been successfully tested, please refresh the page to view the results.", - "通道 ${name} 余额更新成功!": "Channel ${name} balance updated successfully!", - "已更新完毕所有已启用通道余额!": "The balance of all enabled channels has been updated!", + "渠道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。": "Channel ${name} test succeeded, time consumed ${time.toFixed(2)} s.", + "已成功开始测试所有渠道,请刷新页面查看结果。": "All channels have been successfully tested, please refresh the page to view the results.", + "已成功开始测试所有已启用渠道,请刷新页面查看结果。": "All enabled channels have been successfully tested, please refresh the page to view the results.", + "渠道 ${name} 余额更新成功!": "Channel ${name} balance updated successfully!", + "已更新完毕所有已启用渠道余额!": "The balance of all enabled channels has been updated!", "搜索渠道的 ID,名称和密钥 ...": "Search for channel ID, name and key ...", "名称": "Name", "分组": "Group", @@ -141,9 +141,9 @@ "启用": "Enable", "编辑": "Edit", "添加新的渠道": "Add a new channel", - "测试所有通道": "Test all channels", - "测试所有已启用通道": "Test all enabled channels", - "更新所有已启用通道余额": "Update the balance of all enabled channels", + "测试所有渠道": "Test all channels", + "测试所有已启用渠道": "Test all enabled channels", + "更新所有已启用渠道余额": "Update the balance of all enabled channels", "刷新": "Refresh", "处理中...": "Processing...", "绑定成功!": "Binding succeeded!", @@ -207,11 +207,11 @@ "监控设置": "Monitoring Settings", "最长响应时间": "Longest Response Time", "单位秒": "Unit in seconds", - "当运行通道全部测试时": "When all operating channels are tested", - "超过此时间将自动禁用通道": "Channels will be automatically disabled if this time is exceeded", + "当运行渠道全部测试时": "When all operating channels are tested", + "超过此时间将自动禁用渠道": "Channels will be automatically disabled if this time is exceeded", "额度提醒阈值": "Quota reminder threshold", "低于此额度时将发送邮件提醒用户": "Email will be sent to remind users when the quota is below this", - "失败时自动禁用通道": "Automatically disable the channel when it fails", + "失败时自动禁用渠道": "Automatically disable the channel when it fails", "保存监控设置": "Save Monitoring Settings", "额度设置": "Quota Settings", "新用户初始额度": "Initial quota for new users", @@ -405,7 +405,7 @@ "镜像": "Mirror", "请输入镜像站地址,格式为:https://domain.com,可不填,不填则使用渠道默认值": "Please enter the mirror site address, the format is: https://domain.com, it can be left blank, if left blank, the default value of the channel will be used", "模型": "Model", - "请选择该通道所支持的模型": "Please select the model supported by the channel", + "请选择该渠道所支持的模型": "Please select the model supported by the channel", "填入基础模型": "Fill in the basic model", "填入所有模型": "Fill in all models", "清除所有模型": "Clear all models", @@ -515,7 +515,7 @@ "请输入自定义渠道的 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", - "请为通道命名": "Please name the channel", + "请为渠道命名": "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", diff --git a/monitor/channel.go b/monitor/channel.go index 597ab11a..ad82d2f5 100644 --- a/monitor/channel.go +++ b/monitor/channel.go @@ -31,17 +31,17 @@ func notifyRootUser(subject string, content string) { func DisableChannel(channelId int, channelName string, reason string) { model.UpdateChannelStatusById(channelId, common.ChannelStatusAutoDisabled) logger.SysLog(fmt.Sprintf("channel #%d has been disabled: %s", channelId, reason)) - subject := fmt.Sprintf("通道「%s」(#%d)已被禁用", channelName, channelId) - content := fmt.Sprintf("通道「%s」(#%d)已被禁用,原因:%s", channelName, channelId, reason) + subject := fmt.Sprintf("渠道「%s」(#%d)已被禁用", channelName, channelId) + content := fmt.Sprintf("渠道「%s」(#%d)已被禁用,原因:%s", channelName, channelId, reason) notifyRootUser(subject, content) } func MetricDisableChannel(channelId int, successRate float64) { model.UpdateChannelStatusById(channelId, common.ChannelStatusAutoDisabled) logger.SysLog(fmt.Sprintf("channel #%d has been disabled due to low success rate: %.2f", channelId, successRate*100)) - subject := fmt.Sprintf("通道 #%d 已被禁用", channelId) - content := fmt.Sprintf("该渠道在最近 %d 次调用中成功率为 %.2f%%,低于阈值 %.2f%%,因此被系统自动禁用。", - config.MetricQueueSize, successRate*100, config.MetricSuccessRateThreshold*100) + subject := fmt.Sprintf("渠道 #%d 已被禁用", channelId) + content := fmt.Sprintf("该渠道(#%d)在最近 %d 次调用中成功率为 %.2f%%,低于阈值 %.2f%%,因此被系统自动禁用。", + channelId, config.MetricQueueSize, successRate*100, config.MetricSuccessRateThreshold*100) notifyRootUser(subject, content) } @@ -49,7 +49,7 @@ func MetricDisableChannel(channelId int, successRate float64) { func EnableChannel(channelId int, channelName string) { model.UpdateChannelStatusById(channelId, common.ChannelStatusEnabled) logger.SysLog(fmt.Sprintf("channel #%d has been enabled", channelId)) - subject := fmt.Sprintf("通道「%s」(#%d)已被启用", channelName, channelId) - content := fmt.Sprintf("通道「%s」(#%d)已被启用", channelName, channelId) + subject := fmt.Sprintf("渠道「%s」(#%d)已被启用", channelName, channelId) + content := fmt.Sprintf("渠道「%s」(#%d)已被启用", channelName, channelId) notifyRootUser(subject, content) } diff --git a/web/air/src/components/ChannelsTable.js b/web/air/src/components/ChannelsTable.js index dee21a01..c384d50c 100644 --- a/web/air/src/components/ChannelsTable.js +++ b/web/air/src/components/ChannelsTable.js @@ -437,7 +437,7 @@ const ChannelsTable = () => { if (success) { record.response_time = time * 1000; record.test_time = Date.now() / 1000; - showInfo(`通道 ${record.name} 测试成功,耗时 ${time.toFixed(2)} 秒。`); + showInfo(`渠道 ${record.name} 测试成功,耗时 ${time.toFixed(2)} 秒。`); } else { showError(message); } @@ -447,7 +447,7 @@ const ChannelsTable = () => { const res = await API.get(`/api/channel/test?scope=${scope}`); const { success, message } = res.data; if (success) { - showInfo('已成功开始测试通道,请刷新页面查看结果。'); + showInfo('已成功开始测试渠道,请刷新页面查看结果。'); } else { showError(message); } @@ -470,7 +470,7 @@ const ChannelsTable = () => { if (success) { record.balance = balance; record.balance_updated_time = Date.now() / 1000; - showInfo(`通道 ${record.name} 余额更新成功!`); + showInfo(`渠道 ${record.name} 余额更新成功!`); } else { showError(message); } @@ -481,7 +481,7 @@ const ChannelsTable = () => { const res = await API.get(`/api/channel/update_balance`); const { success, message } = res.data; if (success) { - showInfo('已更新完毕所有已启用通道余额!'); + showInfo('已更新完毕所有已启用渠道余额!'); } else { showError(message); } @@ -490,7 +490,7 @@ const ChannelsTable = () => { const batchDeleteChannels = async () => { if (selectedChannels.length === 0) { - showError('请先选择要删除的通道!'); + showError('请先选择要删除的渠道!'); return; } setLoading(true); @@ -501,7 +501,7 @@ const ChannelsTable = () => { const res = await API.post(`/api/channel/batch`, { ids: ids }); const { success, message, data } = res.data; if (success) { - showSuccess(`已删除 ${data} 个通道!`); + showSuccess(`已删除 ${data} 个渠道!`); await refresh(); } else { showError(message); @@ -513,7 +513,7 @@ const ChannelsTable = () => { const res = await API.post(`/api/channel/fix`); const { success, message, data } = res.data; if (success) { - showSuccess(`已修复 ${data} 个通道!`); + showSuccess(`已修复 ${data} 个渠道!`); await refresh(); } else { showError(message); @@ -633,7 +633,7 @@ const ChannelsTable = () => { onConfirm={() => { testChannels("all") }} position={isMobile() ? 'top' : 'left'} > - + { okType={'secondary'} onConfirm={updateAllChannelsBalance} > - + */} - + @@ -673,7 +673,7 @@ const ChannelsTable = () => { setEnableBatchDelete(v); }}> { position={'top'} > + style={{ marginRight: 8 }}>删除所选渠道 { value={inputs.ChannelDisableThreshold} type='number' min='0' - placeholder='单位秒,当运行通道全部测试时,超过此时间将自动禁用通道' + placeholder='单位秒,当运行渠道全部测试时,超过此时间将自动禁用渠道' /> { diff --git a/web/berry/src/views/Channel/component/TableRow.js b/web/berry/src/views/Channel/component/TableRow.js index f7acb92e..1e58b678 100644 --- a/web/berry/src/views/Channel/component/TableRow.js +++ b/web/berry/src/views/Channel/component/TableRow.js @@ -93,7 +93,7 @@ export default function ChannelTableRow({ test_time: Date.now() / 1000, response_time: time * 1000, }); - showInfo(`通道 ${item.name} 测试成功,耗时 ${time.toFixed(2)} 秒。`); + showInfo(`渠道 ${item.name} 测试成功,耗时 ${time.toFixed(2)} 秒。`); } }; @@ -243,9 +243,9 @@ export default function ChannelTableRow({ - 删除通道 + 删除渠道 - 是否删除通道 {item.name}? + 是否删除渠道 {item.name}? diff --git a/web/berry/src/views/Channel/index.js b/web/berry/src/views/Channel/index.js index e801a8c3..c12ff3ba 100644 --- a/web/berry/src/views/Channel/index.js +++ b/web/berry/src/views/Channel/index.js @@ -135,7 +135,7 @@ export default function ChannelPage() { const res = await API.get(`/api/channel/test`); const { success, message } = res.data; if (success) { - showInfo('已成功开始测试所有通道,请刷新页面查看结果。'); + showInfo('已成功开始测试所有渠道,请刷新页面查看结果。'); } else { showError(message); } @@ -159,7 +159,7 @@ export default function ChannelPage() { const res = await API.get(`/api/channel/update_balance`); const { success, message } = res.data; if (success) { - showInfo('已更新完毕所有已启用通道余额!'); + showInfo('已更新完毕所有已启用渠道余额!'); } else { showError(message); } diff --git a/web/berry/src/views/Setting/component/OperationSetting.js b/web/berry/src/views/Setting/component/OperationSetting.js index d91298b2..2bed715b 100644 --- a/web/berry/src/views/Setting/component/OperationSetting.js +++ b/web/berry/src/views/Setting/component/OperationSetting.js @@ -371,7 +371,7 @@ const OperationSetting = () => { value={inputs.ChannelDisableThreshold} onChange={handleInputChange} label="最长响应时间" - placeholder="单位秒,当运行通道全部测试时,超过此时间将自动禁用通道" + placeholder="单位秒,当运行渠道全部测试时,超过此时间将自动禁用渠道" disabled={loading} />
@@ -392,7 +392,7 @@ const OperationSetting = () => { { } /> { newChannels[realIdx].response_time = time * 1000; newChannels[realIdx].test_time = Date.now() / 1000; setChannels(newChannels); - showInfo(`通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。`); + showInfo(`渠道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。`); } else { showError(message); } @@ -244,7 +244,7 @@ const ChannelsTable = () => { const res = await API.get(`/api/channel/test?scope=${scope}`); const { success, message } = res.data; if (success) { - showInfo('已成功开始测试通道,请刷新页面查看结果。'); + showInfo('已成功开始测试渠道,请刷新页面查看结果。'); } else { showError(message); } @@ -270,7 +270,7 @@ const ChannelsTable = () => { newChannels[realIdx].balance = balance; newChannels[realIdx].balance_updated_time = Date.now() / 1000; setChannels(newChannels); - showInfo(`通道 ${name} 余额更新成功!`); + showInfo(`渠道 ${name} 余额更新成功!`); } else { showError(message); } @@ -281,7 +281,7 @@ const ChannelsTable = () => { const res = await API.get(`/api/channel/update_balance`); const { success, message } = res.data; if (success) { - showInfo('已更新完毕所有已启用通道余额!'); + showInfo('已更新完毕所有已启用渠道余额!'); } else { showError(message); } diff --git a/web/default/src/components/OperationSetting.js b/web/default/src/components/OperationSetting.js index b823bb28..6356ac66 100644 --- a/web/default/src/components/OperationSetting.js +++ b/web/default/src/components/OperationSetting.js @@ -261,7 +261,7 @@ const OperationSetting = () => { value={inputs.ChannelDisableThreshold} type='number' min='0' - placeholder='单位秒,当运行通道全部测试时,超过此时间将自动禁用通道' + placeholder='单位秒,当运行渠道全部测试时,超过此时间将自动禁用渠道' /> { From 96d7a993120d54f7d7c85528bbae6eacc6c82e10 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 24 Mar 2024 23:12:32 +0800 Subject: [PATCH 21/28] fix: fix autofilled models are not correct --- web/default/src/pages/Channel/EditChannel.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/web/default/src/pages/Channel/EditChannel.js b/web/default/src/pages/Channel/EditChannel.js index 330b8c8e..203cd714 100644 --- a/web/default/src/pages/Channel/EditChannel.js +++ b/web/default/src/pages/Channel/EditChannel.js @@ -83,6 +83,7 @@ const EditChannel = () => { data.model_mapping = JSON.stringify(JSON.parse(data.model_mapping), null, 2); } setInputs(data); + setBasicModels(getChannelModels(data.type)); } else { showError(message); } @@ -99,9 +100,6 @@ const EditChannel = () => { })); setOriginModelOptions(localModelOptions); setFullModels(res.data.data.map((model) => model.id)); - setBasicModels(res.data.data.filter((model) => { - return model.id.startsWith('gpt-3') || model.id.startsWith('text-'); - }).map((model) => model.id)); } catch (error) { showError(error.message); } @@ -137,6 +135,9 @@ const EditChannel = () => { useEffect(() => { if (isEdit) { loadChannel().then(); + } else { + let localModels = getChannelModels(inputs.type); + setBasicModels(localModels); } fetchModels().then(); fetchGroups().then(); @@ -355,7 +356,7 @@ const EditChannel = () => {
+ }}>填入相关模型 From 5e81e19bc81e88d5df15a04f6a6268886127e002 Mon Sep 17 00:00:00 2001 From: JustSong Date: Wed, 27 Mar 2024 19:09:27 +0800 Subject: [PATCH 22/28] fix: fix SQL channel selection algo (#1197) --- model/ability.go | 12 +++++++++--- model/cache.go | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/model/ability.go b/model/ability.go index 7127abc3..48b856a2 100644 --- a/model/ability.go +++ b/model/ability.go @@ -2,6 +2,7 @@ package model import ( "github.com/songquanpeng/one-api/common" + "gorm.io/gorm" "strings" ) @@ -13,7 +14,7 @@ type Ability struct { Priority *int64 `json:"priority" gorm:"bigint;default:0;index"` } -func GetRandomSatisfiedChannel(group string, model string) (*Channel, error) { +func GetRandomSatisfiedChannel(group string, model string, ignoreFirstPriority bool) (*Channel, error) { ability := Ability{} groupCol := "`group`" trueVal := "1" @@ -23,8 +24,13 @@ func GetRandomSatisfiedChannel(group string, model string) (*Channel, error) { } var err error = nil - maxPrioritySubQuery := DB.Model(&Ability{}).Select("MAX(priority)").Where(groupCol+" = ? and model = ? and enabled = "+trueVal, group, model) - channelQuery := DB.Where(groupCol+" = ? and model = ? and enabled = "+trueVal+" and priority = (?)", group, model, maxPrioritySubQuery) + var channelQuery *gorm.DB + if ignoreFirstPriority { + channelQuery = DB.Where(groupCol+" = ? and model = ? and enabled = "+trueVal, group, model) + } else { + maxPrioritySubQuery := DB.Model(&Ability{}).Select("MAX(priority)").Where(groupCol+" = ? and model = ? and enabled = "+trueVal, group, model) + channelQuery = DB.Where(groupCol+" = ? and model = ? and enabled = "+trueVal+" and priority = (?)", group, model, maxPrioritySubQuery) + } if common.UsingSQLite || common.UsingPostgreSQL { err = channelQuery.Order("RANDOM()").First(&ability).Error } else { diff --git a/model/cache.go b/model/cache.go index dd20d857..244fe6ac 100644 --- a/model/cache.go +++ b/model/cache.go @@ -205,7 +205,7 @@ func SyncChannelCache(frequency int) { func CacheGetRandomSatisfiedChannel(group string, model string, ignoreFirstPriority bool) (*Channel, error) { if !config.MemoryCacheEnabled { - return GetRandomSatisfiedChannel(group, model) + return GetRandomSatisfiedChannel(group, model, ignoreFirstPriority) } channelSyncLock.RLock() defer channelSyncLock.RUnlock() From 2ba28c72cbd41ed1020d71d3fd57367ea99be7fd Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 30 Mar 2024 10:43:26 +0800 Subject: [PATCH 23/28] feat: support function call for ali (close #1242) --- common/conv/any.go | 6 ++++++ relay/channel/ali/main.go | 28 ++++++++++++++++------------ relay/channel/ali/model.go | 18 +++++++++++++----- relay/channel/openai/main.go | 3 ++- relay/channel/openai/model.go | 9 +++------ relay/channel/tencent/main.go | 3 ++- relay/model/general.go | 26 ++++++++++++++------------ relay/model/message.go | 7 ++++--- relay/model/tool.go | 14 ++++++++++++++ 9 files changed, 74 insertions(+), 40 deletions(-) create mode 100644 common/conv/any.go create mode 100644 relay/model/tool.go diff --git a/common/conv/any.go b/common/conv/any.go new file mode 100644 index 00000000..467e8bb7 --- /dev/null +++ b/common/conv/any.go @@ -0,0 +1,6 @@ +package conv + +func AsString(v any) string { + str, _ := v.(string) + return str +} diff --git a/relay/channel/ali/main.go b/relay/channel/ali/main.go index 62115d58..6fdfa4d4 100644 --- a/relay/channel/ali/main.go +++ b/relay/channel/ali/main.go @@ -48,7 +48,10 @@ func ConvertRequest(request model.GeneralOpenAIRequest) *ChatRequest { MaxTokens: request.MaxTokens, Temperature: request.Temperature, TopP: request.TopP, + TopK: request.TopK, + ResultFormat: "message", }, + Tools: request.Tools, } } @@ -117,19 +120,11 @@ func embeddingResponseAli2OpenAI(response *EmbeddingResponse) *openai.EmbeddingR } func responseAli2OpenAI(response *ChatResponse) *openai.TextResponse { - choice := openai.TextResponseChoice{ - Index: 0, - Message: model.Message{ - Role: "assistant", - Content: response.Output.Text, - }, - FinishReason: response.Output.FinishReason, - } fullTextResponse := openai.TextResponse{ Id: response.RequestId, Object: "chat.completion", Created: helper.GetTimestamp(), - Choices: []openai.TextResponseChoice{choice}, + Choices: response.Output.Choices, Usage: model.Usage{ PromptTokens: response.Usage.InputTokens, CompletionTokens: response.Usage.OutputTokens, @@ -140,10 +135,14 @@ func responseAli2OpenAI(response *ChatResponse) *openai.TextResponse { } func streamResponseAli2OpenAI(aliResponse *ChatResponse) *openai.ChatCompletionsStreamResponse { + if len(aliResponse.Output.Choices) == 0 { + return nil + } + aliChoice := aliResponse.Output.Choices[0] var choice openai.ChatCompletionsStreamResponseChoice - choice.Delta.Content = aliResponse.Output.Text - if aliResponse.Output.FinishReason != "null" { - finishReason := aliResponse.Output.FinishReason + choice.Delta = aliChoice.Message + if aliChoice.FinishReason != "null" { + finishReason := aliChoice.FinishReason choice.FinishReason = &finishReason } response := openai.ChatCompletionsStreamResponse{ @@ -204,6 +203,9 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC usage.TotalTokens = aliResponse.Usage.InputTokens + aliResponse.Usage.OutputTokens } response := streamResponseAli2OpenAI(&aliResponse) + if response == nil { + return true + } //response.Choices[0].Delta.Content = strings.TrimPrefix(response.Choices[0].Delta.Content, lastResponseText) //lastResponseText = aliResponse.Output.Text jsonResponse, err := json.Marshal(response) @@ -226,6 +228,7 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC } func Handler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { + ctx := c.Request.Context() var aliResponse ChatResponse responseBody, err := io.ReadAll(resp.Body) if err != nil { @@ -235,6 +238,7 @@ func Handler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, * if err != nil { return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil } + logger.Debugf(ctx, "response body: %s\n", responseBody) err = json.Unmarshal(responseBody, &aliResponse) if err != nil { return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil diff --git a/relay/channel/ali/model.go b/relay/channel/ali/model.go index 76e814d1..e19d427a 100644 --- a/relay/channel/ali/model.go +++ b/relay/channel/ali/model.go @@ -1,5 +1,10 @@ package ali +import ( + "github.com/songquanpeng/one-api/relay/channel/openai" + "github.com/songquanpeng/one-api/relay/model" +) + type Message struct { Content string `json:"content"` Role string `json:"role"` @@ -18,12 +23,14 @@ type Parameters struct { IncrementalOutput bool `json:"incremental_output,omitempty"` MaxTokens int `json:"max_tokens,omitempty"` Temperature float64 `json:"temperature,omitempty"` + ResultFormat string `json:"result_format,omitempty"` } type ChatRequest struct { - Model string `json:"model"` - Input Input `json:"input"` - Parameters Parameters `json:"parameters,omitempty"` + Model string `json:"model"` + Input Input `json:"input"` + Parameters Parameters `json:"parameters,omitempty"` + Tools []model.Tool `json:"tools,omitempty"` } type EmbeddingRequest struct { @@ -62,8 +69,9 @@ type Usage struct { } type Output struct { - Text string `json:"text"` - FinishReason string `json:"finish_reason"` + //Text string `json:"text"` + //FinishReason string `json:"finish_reason"` + Choices []openai.TextResponseChoice `json:"choices"` } type ChatResponse struct { diff --git a/relay/channel/openai/main.go b/relay/channel/openai/main.go index d47cd164..63cb9ae8 100644 --- a/relay/channel/openai/main.go +++ b/relay/channel/openai/main.go @@ -6,6 +6,7 @@ import ( "encoding/json" "github.com/gin-gonic/gin" "github.com/songquanpeng/one-api/common" + "github.com/songquanpeng/one-api/common/conv" "github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/relay/constant" "github.com/songquanpeng/one-api/relay/model" @@ -53,7 +54,7 @@ func StreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*model.E continue // just ignore the error } for _, choice := range streamResponse.Choices { - responseText += choice.Delta.Content + responseText += conv.AsString(choice.Delta.Content) } if streamResponse.Usage != nil { usage = streamResponse.Usage diff --git a/relay/channel/openai/model.go b/relay/channel/openai/model.go index 6c0b2c53..30d77739 100644 --- a/relay/channel/openai/model.go +++ b/relay/channel/openai/model.go @@ -118,12 +118,9 @@ type ImageResponse struct { } type ChatCompletionsStreamResponseChoice struct { - Index int `json:"index"` - Delta struct { - Content string `json:"content"` - Role string `json:"role,omitempty"` - } `json:"delta"` - FinishReason *string `json:"finish_reason,omitempty"` + Index int `json:"index"` + Delta model.Message `json:"delta"` + FinishReason *string `json:"finish_reason,omitempty"` } type ChatCompletionsStreamResponse struct { diff --git a/relay/channel/tencent/main.go b/relay/channel/tencent/main.go index cfdc0bfd..b5a64cde 100644 --- a/relay/channel/tencent/main.go +++ b/relay/channel/tencent/main.go @@ -10,6 +10,7 @@ import ( "fmt" "github.com/gin-gonic/gin" "github.com/songquanpeng/one-api/common" + "github.com/songquanpeng/one-api/common/conv" "github.com/songquanpeng/one-api/common/helper" "github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/relay/channel/openai" @@ -129,7 +130,7 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC } response := streamResponseTencent2OpenAI(&TencentResponse) if len(response.Choices) != 0 { - responseText += response.Choices[0].Delta.Content + responseText += conv.AsString(response.Choices[0].Delta.Content) } jsonResponse, err := json.Marshal(response) if err != nil { diff --git a/relay/model/general.go b/relay/model/general.go index fbcc04e8..86facf04 100644 --- a/relay/model/general.go +++ b/relay/model/general.go @@ -5,25 +5,27 @@ type ResponseFormat struct { } type GeneralOpenAIRequest struct { - Model string `json:"model,omitempty"` Messages []Message `json:"messages,omitempty"` - Prompt any `json:"prompt,omitempty"` - Stream bool `json:"stream,omitempty"` - MaxTokens int `json:"max_tokens,omitempty"` - Temperature float64 `json:"temperature,omitempty"` - TopP float64 `json:"top_p,omitempty"` - N int `json:"n,omitempty"` - Input any `json:"input,omitempty"` - Instruction string `json:"instruction,omitempty"` - Size string `json:"size,omitempty"` - Functions any `json:"functions,omitempty"` + Model string `json:"model,omitempty"` FrequencyPenalty float64 `json:"frequency_penalty,omitempty"` + MaxTokens int `json:"max_tokens,omitempty"` + N int `json:"n,omitempty"` PresencePenalty float64 `json:"presence_penalty,omitempty"` ResponseFormat *ResponseFormat `json:"response_format,omitempty"` Seed float64 `json:"seed,omitempty"` - Tools any `json:"tools,omitempty"` + Stream bool `json:"stream,omitempty"` + Temperature float64 `json:"temperature,omitempty"` + TopP float64 `json:"top_p,omitempty"` + TopK int `json:"top_k,omitempty"` + Tools []Tool `json:"tools,omitempty"` ToolChoice any `json:"tool_choice,omitempty"` + FunctionCall any `json:"function_call,omitempty"` + Functions any `json:"functions,omitempty"` User string `json:"user,omitempty"` + Prompt any `json:"prompt,omitempty"` + Input any `json:"input,omitempty"` + Instruction string `json:"instruction,omitempty"` + Size string `json:"size,omitempty"` } func (r GeneralOpenAIRequest) ParseInput() []string { diff --git a/relay/model/message.go b/relay/model/message.go index c6c8a271..32a1055b 100644 --- a/relay/model/message.go +++ b/relay/model/message.go @@ -1,9 +1,10 @@ package model type Message struct { - Role string `json:"role"` - Content any `json:"content"` - Name *string `json:"name,omitempty"` + Role string `json:"role,omitempty"` + Content any `json:"content,omitempty"` + Name *string `json:"name,omitempty"` + ToolCalls []Tool `json:"tool_calls,omitempty"` } func (m Message) IsStringContent() bool { diff --git a/relay/model/tool.go b/relay/model/tool.go new file mode 100644 index 00000000..253dca35 --- /dev/null +++ b/relay/model/tool.go @@ -0,0 +1,14 @@ +package model + +type Tool struct { + Id string `json:"id,omitempty"` + Type string `json:"type"` + Function Function `json:"function"` +} + +type Function struct { + Description string `json:"description,omitempty"` + Name string `json:"name"` + Parameters any `json:"parameters,omitempty"` // request + Arguments any `json:"arguments,omitempty"` // response +} From 3f3c13c98c3ba10d5ca674c6c688f75fe9148c3e Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 30 Mar 2024 10:43:26 +0800 Subject: [PATCH 24/28] feat: support top_k for claude (close #1239) --- relay/channel/anthropic/main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/relay/channel/anthropic/main.go b/relay/channel/anthropic/main.go index 3eeb0b2c..04e65d99 100644 --- a/relay/channel/anthropic/main.go +++ b/relay/channel/anthropic/main.go @@ -38,6 +38,7 @@ func ConvertRequest(textRequest model.GeneralOpenAIRequest) *Request { MaxTokens: textRequest.MaxTokens, Temperature: textRequest.Temperature, TopP: textRequest.TopP, + TopK: textRequest.TopK, Stream: textRequest.Stream, } if claudeRequest.MaxTokens == 0 { From a9c464ec5ad6861d10cbd0f4bcd9859f7fd8abdd Mon Sep 17 00:00:00 2001 From: ManJieqi <40858189+ManJieqi@users.noreply.github.com> Date: Sat, 30 Mar 2024 11:06:31 +0800 Subject: [PATCH 25/28] =?UTF-8?q?fix:=20update=20model-ratio.go=20?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E6=96=87=E5=BF=83=E8=AE=A1=E8=B4=B9=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E5=90=8D=E7=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 统一文心计费模型名称 --- common/model-ratio.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/model-ratio.go b/common/model-ratio.go index 460d4843..aa75042e 100644 --- a/common/model-ratio.go +++ b/common/model-ratio.go @@ -75,7 +75,7 @@ var ModelRatio = map[string]float64{ "ERNIE-Bot": 0.8572, // ¥0.012 / 1k tokens "ERNIE-Bot-turbo": 0.5715, // ¥0.008 / 1k tokens "ERNIE-Bot-4": 0.12 * RMB, // ¥0.12 / 1k tokens - "ERNIE-Bot-8k": 0.024 * RMB, + "ERNIE-Bot-8K": 0.024 * RMB, "Embedding-V1": 0.1429, // ¥0.002 / 1k tokens "bge-large-zh": 0.002 * RMB, "bge-large-en": 0.002 * RMB, From 06a3fc54216cf1a8229193f749a9d316db5bfb9e Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 31 Mar 2024 22:23:42 +0800 Subject: [PATCH 26/28] chore: update GeneralOpenAIRequest --- relay/model/general.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/relay/model/general.go b/relay/model/general.go index 86facf04..30772894 100644 --- a/relay/model/general.go +++ b/relay/model/general.go @@ -24,6 +24,8 @@ type GeneralOpenAIRequest struct { User string `json:"user,omitempty"` Prompt any `json:"prompt,omitempty"` Input any `json:"input,omitempty"` + EncodingFormat string `json:"encoding_format,omitempty"` + Dimensions int `json:"dimensions,omitempty"` Instruction string `json:"instruction,omitempty"` Size string `json:"size,omitempty"` } From f89ae5ad5830ab5962b944b5becedecd60de6e3c Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 31 Mar 2024 23:12:29 +0800 Subject: [PATCH 27/28] feat: initial function call support for xunfei --- relay/channel/xunfei/main.go | 34 ++++++++++++++++++++++++++++++++-- relay/channel/xunfei/model.go | 11 ++++++++--- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/relay/channel/xunfei/main.go b/relay/channel/xunfei/main.go index 5e7014cb..67784a56 100644 --- a/relay/channel/xunfei/main.go +++ b/relay/channel/xunfei/main.go @@ -26,7 +26,11 @@ import ( func requestOpenAI2Xunfei(request model.GeneralOpenAIRequest, xunfeiAppId string, domain string) *ChatRequest { messages := make([]Message, 0, len(request.Messages)) + var lastToolCalls []model.Tool for _, message := range request.Messages { + if message.ToolCalls != nil { + lastToolCalls = message.ToolCalls + } messages = append(messages, Message{ Role: message.Role, Content: message.StringContent(), @@ -39,9 +43,33 @@ func requestOpenAI2Xunfei(request model.GeneralOpenAIRequest, xunfeiAppId string xunfeiRequest.Parameter.Chat.TopK = request.N xunfeiRequest.Parameter.Chat.MaxTokens = request.MaxTokens xunfeiRequest.Payload.Message.Text = messages + if len(lastToolCalls) != 0 { + for _, toolCall := range lastToolCalls { + xunfeiRequest.Payload.Functions.Text = append(xunfeiRequest.Payload.Functions.Text, toolCall.Function) + } + } + return &xunfeiRequest } +func getToolCalls(response *ChatResponse) []model.Tool { + var toolCalls []model.Tool + if len(response.Payload.Choices.Text) == 0 { + return toolCalls + } + item := response.Payload.Choices.Text[0] + if item.FunctionCall == nil { + return toolCalls + } + toolCall := model.Tool{ + Id: fmt.Sprintf("call_%s", helper.GetUUID()), + Type: "function", + Function: *item.FunctionCall, + } + toolCalls = append(toolCalls, toolCall) + return toolCalls +} + func responseXunfei2OpenAI(response *ChatResponse) *openai.TextResponse { if len(response.Payload.Choices.Text) == 0 { response.Payload.Choices.Text = []ChatResponseTextItem{ @@ -53,8 +81,9 @@ func responseXunfei2OpenAI(response *ChatResponse) *openai.TextResponse { choice := openai.TextResponseChoice{ Index: 0, Message: model.Message{ - Role: "assistant", - Content: response.Payload.Choices.Text[0].Content, + Role: "assistant", + Content: response.Payload.Choices.Text[0].Content, + ToolCalls: getToolCalls(response), }, FinishReason: constant.StopFinishReason, } @@ -78,6 +107,7 @@ func streamResponseXunfei2OpenAI(xunfeiResponse *ChatResponse) *openai.ChatCompl } var choice openai.ChatCompletionsStreamResponseChoice choice.Delta.Content = xunfeiResponse.Payload.Choices.Text[0].Content + choice.Delta.ToolCalls = getToolCalls(xunfeiResponse) if xunfeiResponse.Payload.Choices.Status == 2 { choice.FinishReason = &constant.StopFinishReason } diff --git a/relay/channel/xunfei/model.go b/relay/channel/xunfei/model.go index 1266739d..e9cc59a6 100644 --- a/relay/channel/xunfei/model.go +++ b/relay/channel/xunfei/model.go @@ -26,13 +26,18 @@ type ChatRequest struct { Message struct { Text []Message `json:"text"` } `json:"message"` + Functions struct { + Text []model.Function `json:"text,omitempty"` + } `json:"functions"` } `json:"payload"` } type ChatResponseTextItem struct { - Content string `json:"content"` - Role string `json:"role"` - Index int `json:"index"` + Content string `json:"content"` + Role string `json:"role"` + Index int `json:"index"` + ContentType string `json:"content_type"` + FunctionCall *model.Function `json:"function_call"` } type ChatResponse struct { From e3cfb1fa524107439e4b0caec0137e249bd06467 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 31 Mar 2024 23:41:52 +0800 Subject: [PATCH 28/28] feat: use given usage if available in stream mode --- relay/channel/openai/adaptor.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/relay/channel/openai/adaptor.go b/relay/channel/openai/adaptor.go index 1f153c3e..9be695f2 100644 --- a/relay/channel/openai/adaptor.go +++ b/relay/channel/openai/adaptor.go @@ -70,8 +70,10 @@ func (a *Adaptor) DoRequest(c *gin.Context, meta *util.RelayMeta, requestBody io func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *util.RelayMeta) (usage *model.Usage, err *model.ErrorWithStatusCode) { if meta.IsStream { var responseText string - err, responseText, _ = StreamHandler(c, resp, meta.Mode) - usage = ResponseText2Usage(responseText, meta.ActualModelName, meta.PromptTokens) + err, responseText, usage = StreamHandler(c, resp, meta.Mode) + if usage == nil { + usage = ResponseText2Usage(responseText, meta.ActualModelName, meta.PromptTokens) + } } else { err, usage = Handler(c, resp, meta.PromptTokens, meta.ActualModelName) }