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 001/121] 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 002/121] 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 003/121] 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 004/121] 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 005/121] 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 006/121] 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 007/121] 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 008/121] 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 009/121] 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 010/121] 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 011/121] =?UTF-8?q?feat:=20=E7=A7=BB=E9=99=A4=20azure=20mo?= =?UTF-8?q?del=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 012/121] =?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 013/121] 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 014/121] 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 015/121] 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 016/121] 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 017/121] 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 018/121] 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 019/121] 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 020/121] 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 021/121] 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 022/121] 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 023/121] =?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 024/121] 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 025/121] 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 026/121] 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) } From 065da8ef8c8bcbc0a7fc3ae22e39397ffe036b6a Mon Sep 17 00:00:00 2001 From: JustSong Date: Thu, 4 Apr 2024 00:46:30 +0800 Subject: [PATCH 027/121] fix: fix ali function call (#1242) --- relay/channel/ali/main.go | 2 +- relay/channel/ali/model.go | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/relay/channel/ali/main.go b/relay/channel/ali/main.go index 6fdfa4d4..dd1707ee 100644 --- a/relay/channel/ali/main.go +++ b/relay/channel/ali/main.go @@ -50,8 +50,8 @@ func ConvertRequest(request model.GeneralOpenAIRequest) *ChatRequest { TopP: request.TopP, TopK: request.TopK, ResultFormat: "message", + Tools: request.Tools, }, - Tools: request.Tools, } } diff --git a/relay/channel/ali/model.go b/relay/channel/ali/model.go index e19d427a..3b8a8372 100644 --- a/relay/channel/ali/model.go +++ b/relay/channel/ali/model.go @@ -16,21 +16,21 @@ type Input struct { } type Parameters struct { - TopP float64 `json:"top_p,omitempty"` - TopK int `json:"top_k,omitempty"` - Seed uint64 `json:"seed,omitempty"` - EnableSearch bool `json:"enable_search,omitempty"` - IncrementalOutput bool `json:"incremental_output,omitempty"` - MaxTokens int `json:"max_tokens,omitempty"` - Temperature float64 `json:"temperature,omitempty"` - ResultFormat string `json:"result_format,omitempty"` + TopP float64 `json:"top_p,omitempty"` + TopK int `json:"top_k,omitempty"` + Seed uint64 `json:"seed,omitempty"` + EnableSearch bool `json:"enable_search,omitempty"` + IncrementalOutput bool `json:"incremental_output,omitempty"` + MaxTokens int `json:"max_tokens,omitempty"` + Temperature float64 `json:"temperature,omitempty"` + ResultFormat string `json:"result_format,omitempty"` + Tools []model.Tool `json:"tools,omitempty"` } type ChatRequest struct { - Model string `json:"model"` - Input Input `json:"input"` - Parameters Parameters `json:"parameters,omitempty"` - Tools []model.Tool `json:"tools,omitempty"` + Model string `json:"model"` + Input Input `json:"input"` + Parameters Parameters `json:"parameters,omitempty"` } type EmbeddingRequest struct { From dc7aaf2de5aaf0000073a7466acbda6fe213c291 Mon Sep 17 00:00:00 2001 From: JustSong Date: Thu, 4 Apr 2024 02:08:18 +0800 Subject: [PATCH 028/121] feat: able to set model limitation for token (close #178) --- controller/model.go | 28 ++++++++++++ controller/token.go | 1 + middleware/auth.go | 13 ++++++ middleware/distributor.go | 38 +++------------- middleware/utils.go | 42 ++++++++++++++++++ model/cache.go | 20 +++++++++ model/channel.go | 30 ++++++++++++- model/token.go | 29 ++++++------ router/api-router.go | 1 + web/default/src/pages/Token/EditToken.js | 56 +++++++++++++++++++++--- 10 files changed, 204 insertions(+), 54 deletions(-) diff --git a/controller/model.go b/controller/model.go index 4c5476b4..bf4b83a7 100644 --- a/controller/model.go +++ b/controller/model.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/gin-gonic/gin" "github.com/songquanpeng/one-api/common" + "github.com/songquanpeng/one-api/model" "github.com/songquanpeng/one-api/relay/channel/openai" "github.com/songquanpeng/one-api/relay/constant" "github.com/songquanpeng/one-api/relay/helper" @@ -142,3 +143,30 @@ func RetrieveModel(c *gin.Context) { }) } } + +func GetUserAvailableModels(c *gin.Context) { + ctx := c.Request.Context() + id := c.GetInt("id") + userGroup, err := model.CacheGetUserGroup(id) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + models, err := model.CacheGetGroupModels(ctx, userGroup) + 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": models, + }) + return +} diff --git a/controller/token.go b/controller/token.go index 949931da..c6128534 100644 --- a/controller/token.go +++ b/controller/token.go @@ -216,6 +216,7 @@ func UpdateToken(c *gin.Context) { cleanToken.ExpiredTime = token.ExpiredTime cleanToken.RemainQuota = token.RemainQuota cleanToken.UnlimitedQuota = token.UnlimitedQuota + cleanToken.Models = token.Models } err = cleanToken.Update() if err != nil { diff --git a/middleware/auth.go b/middleware/auth.go index 30997efd..443199d0 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -1,6 +1,7 @@ package middleware import ( + "fmt" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" "github.com/songquanpeng/one-api/common" @@ -107,6 +108,18 @@ func TokenAuth() func(c *gin.Context) { abortWithMessage(c, http.StatusForbidden, "用户已被封禁") return } + requestModel, err := getRequestModel(c) + if err != nil { + abortWithMessage(c, http.StatusBadRequest, err.Error()) + return + } + c.Set("request_model", requestModel) + if token.Models != nil && *token.Models != "" { + if !isModelInList(requestModel, *token.Models) { + abortWithMessage(c, http.StatusForbidden, fmt.Sprintf("该令牌无权使用模型:%s", requestModel)) + return + } + } c.Set("id", token.UserId) c.Set("token_id", token.Id) c.Set("token_name", token.Name) diff --git a/middleware/distributor.go b/middleware/distributor.go index e845c2f8..04489a2b 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -2,14 +2,12 @@ package middleware import ( "fmt" + "github.com/gin-gonic/gin" "github.com/songquanpeng/one-api/common" "github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/model" "net/http" "strconv" - "strings" - - "github.com/gin-gonic/gin" ) type ModelRequest struct { @@ -40,37 +38,11 @@ func Distribute() func(c *gin.Context) { return } } else { - // Select a channel for the user - var modelRequest ModelRequest - err := common.UnmarshalBodyReusable(c, &modelRequest) + requestModel := c.GetString("request_model") + var err error + channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, requestModel, false) if err != nil { - abortWithMessage(c, http.StatusBadRequest, "无效的请求") - return - } - if strings.HasPrefix(c.Request.URL.Path, "/v1/moderations") { - if modelRequest.Model == "" { - modelRequest.Model = "text-moderation-stable" - } - } - if strings.HasSuffix(c.Request.URL.Path, "embeddings") { - if modelRequest.Model == "" { - modelRequest.Model = c.Param("model") - } - } - if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations") { - if modelRequest.Model == "" { - modelRequest.Model = "dall-e-2" - } - } - if strings.HasPrefix(c.Request.URL.Path, "/v1/audio/transcriptions") || strings.HasPrefix(c.Request.URL.Path, "/v1/audio/translations") { - if modelRequest.Model == "" { - modelRequest.Model = "whisper-1" - } - } - requestModel = modelRequest.Model - channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, modelRequest.Model, false) - if err != nil { - message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", userGroup, modelRequest.Model) + message := fmt.Sprintf("当前分组 %s 下对于模型 %s 无可用渠道", userGroup, requestModel) if channel != nil { logger.SysError(fmt.Sprintf("渠道不存在:%d", channel.Id)) message = "数据库一致性已被破坏,请联系管理员" diff --git a/middleware/utils.go b/middleware/utils.go index bc14c367..b65b018b 100644 --- a/middleware/utils.go +++ b/middleware/utils.go @@ -1,9 +1,12 @@ package middleware import ( + "fmt" "github.com/gin-gonic/gin" + "github.com/songquanpeng/one-api/common" "github.com/songquanpeng/one-api/common/helper" "github.com/songquanpeng/one-api/common/logger" + "strings" ) func abortWithMessage(c *gin.Context, statusCode int, message string) { @@ -16,3 +19,42 @@ func abortWithMessage(c *gin.Context, statusCode int, message string) { c.Abort() logger.Error(c.Request.Context(), message) } + +func getRequestModel(c *gin.Context) (string, error) { + var modelRequest ModelRequest + err := common.UnmarshalBodyReusable(c, &modelRequest) + if err != nil { + return "", fmt.Errorf("common.UnmarshalBodyReusable failed: %w", err) + } + if strings.HasPrefix(c.Request.URL.Path, "/v1/moderations") { + if modelRequest.Model == "" { + modelRequest.Model = "text-moderation-stable" + } + } + if strings.HasSuffix(c.Request.URL.Path, "embeddings") { + if modelRequest.Model == "" { + modelRequest.Model = c.Param("model") + } + } + if strings.HasPrefix(c.Request.URL.Path, "/v1/images/generations") { + if modelRequest.Model == "" { + modelRequest.Model = "dall-e-2" + } + } + if strings.HasPrefix(c.Request.URL.Path, "/v1/audio/transcriptions") || strings.HasPrefix(c.Request.URL.Path, "/v1/audio/translations") { + if modelRequest.Model == "" { + modelRequest.Model = "whisper-1" + } + } + return modelRequest.Model, nil +} + +func isModelInList(modelName string, models string) bool { + modelList := strings.Split(models, ",") + for _, model := range modelList { + if modelName == model { + return true + } + } + return false +} diff --git a/model/cache.go b/model/cache.go index 244fe6ac..cfc5445a 100644 --- a/model/cache.go +++ b/model/cache.go @@ -21,6 +21,7 @@ var ( UserId2GroupCacheSeconds = config.SyncFrequency UserId2QuotaCacheSeconds = config.SyncFrequency UserId2StatusCacheSeconds = config.SyncFrequency + GroupModelsCacheSeconds = config.SyncFrequency ) func CacheGetTokenByKey(key string) (*Token, error) { @@ -146,6 +147,25 @@ func CacheIsUserEnabled(userId int) (bool, error) { return userEnabled, err } +func CacheGetGroupModels(ctx context.Context, group string) ([]string, error) { + if !common.RedisEnabled { + return GetGroupModels(ctx, group) + } + modelsStr, err := common.RedisGet(fmt.Sprintf("group_models:%s", group)) + if err == nil { + return strings.Split(modelsStr, ","), nil + } + models, err := GetGroupModels(ctx, group) + if err != nil { + return nil, err + } + err = common.RedisSet(fmt.Sprintf("group_models:%s", group), strings.Join(models, ","), time.Duration(GroupModelsCacheSeconds)*time.Second) + if err != nil { + logger.SysError("Redis set group models error: " + err.Error()) + } + return models, nil +} + var group2model2channels map[string]map[string][]*Channel var channelSyncLock sync.RWMutex diff --git a/model/channel.go b/model/channel.go index fc4905b1..24829bc5 100644 --- a/model/channel.go +++ b/model/channel.go @@ -1,6 +1,7 @@ package model import ( + "context" "encoding/json" "fmt" "github.com/songquanpeng/one-api/common" @@ -8,6 +9,8 @@ import ( "github.com/songquanpeng/one-api/common/helper" "github.com/songquanpeng/one-api/common/logger" "gorm.io/gorm" + "sort" + "strings" ) type Channel struct { @@ -25,7 +28,7 @@ type Channel struct { Balance float64 `json:"balance"` // in USD BalanceUpdatedTime int64 `json:"balance_updated_time" gorm:"bigint"` Models string `json:"models"` - Group string `json:"group" gorm:"type:varchar(32);default:'default'"` + Group string `json:"group" gorm:"index;type:varchar(32);default:'default'"` UsedQuota int64 `json:"used_quota" gorm:"bigint;default:0"` ModelMapping *string `json:"model_mapping" gorm:"type:varchar(1024);default:''"` Priority *int64 `json:"priority" gorm:"bigint;default:0"` @@ -202,3 +205,28 @@ func DeleteDisabledChannel() (int64, error) { result := DB.Where("status = ? or status = ?", common.ChannelStatusAutoDisabled, common.ChannelStatusManuallyDisabled).Delete(&Channel{}) return result.RowsAffected, result.Error } + +func GetGroupModels(ctx context.Context, group string) ([]string, error) { + groupCol := "`group`" + if common.UsingPostgreSQL { + groupCol = `"group"` + } + var modelsList []string + err := DB.Model(&Channel{}).Distinct("models").Where(groupCol+" = ?", group).Pluck("models", &modelsList).Error + if err != nil { + return nil, err + } + set := make(map[string]bool) + for i := 0; i < len(modelsList); i++ { + modelList := strings.Split(modelsList[i], ",") + for _, model := range modelList { + set[model] = true + } + } + modelList := make([]string, 0, len(set)) + for model := range set { + modelList = append(modelList, model) + } + sort.Strings(modelList) + return modelList, err +} diff --git a/model/token.go b/model/token.go index 493e27c9..fef80fcf 100644 --- a/model/token.go +++ b/model/token.go @@ -12,24 +12,25 @@ import ( ) type Token struct { - Id int `json:"id"` - UserId int `json:"user_id"` - Key string `json:"key" gorm:"type:char(48);uniqueIndex"` - Status int `json:"status" gorm:"default:1"` - Name string `json:"name" gorm:"index" ` - 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:"bigint;default:0"` - UnlimitedQuota bool `json:"unlimited_quota" gorm:"default:false"` - UsedQuota int64 `json:"used_quota" gorm:"bigint;default:0"` // used quota + Id int `json:"id"` + UserId int `json:"user_id"` + Key string `json:"key" gorm:"type:char(48);uniqueIndex"` + Status int `json:"status" gorm:"default:1"` + Name string `json:"name" gorm:"index" ` + 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:"bigint;default:0"` + UnlimitedQuota bool `json:"unlimited_quota" gorm:"default:false"` + UsedQuota int64 `json:"used_quota" gorm:"bigint;default:0"` // used quota + Models *string `json:"models" gorm:"default:''"` } func GetAllUserTokens(userId int, startIdx int, num int, order string) ([]*Token, error) { var tokens []*Token var err error query := DB.Where("user_id = ?", userId) - + switch order { case "remain_quota": query = query.Order("unlimited_quota desc, remain_quota desc") @@ -38,7 +39,7 @@ func GetAllUserTokens(userId int, startIdx int, num int, order string) ([]*Token default: query = query.Order("id desc") } - + err = query.Limit(num).Offset(startIdx).Find(&tokens).Error return tokens, err } @@ -121,7 +122,7 @@ func (token *Token) Insert() error { // Update Make sure your token's fields is completed, because this will update non-zero values func (token *Token) Update() error { var err error - err = DB.Model(token).Select("name", "status", "expired_time", "remain_quota", "unlimited_quota").Updates(token).Error + err = DB.Model(token).Select("name", "status", "expired_time", "remain_quota", "unlimited_quota", "models").Updates(token).Error return err } diff --git a/router/api-router.go b/router/api-router.go index 5b755ede..4aa6d830 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -43,6 +43,7 @@ func SetApiRouter(router *gin.Engine) { selfRoute.GET("/token", controller.GenerateAccessToken) selfRoute.GET("/aff", controller.GetAffCode) selfRoute.POST("/topup", controller.TopUp) + selfRoute.GET("/available_models", controller.GetUserAvailableModels) } adminRoute := userRoute.Group("/") diff --git a/web/default/src/pages/Token/EditToken.js b/web/default/src/pages/Token/EditToken.js index 0ab37c29..6bc3ad23 100644 --- a/web/default/src/pages/Token/EditToken.js +++ b/web/default/src/pages/Token/EditToken.js @@ -1,19 +1,21 @@ import React, { useEffect, useState } from 'react'; import { Button, Form, Header, Message, Segment } from 'semantic-ui-react'; -import { useParams, useNavigate } from 'react-router-dom'; -import { API, showError, showSuccess, timestamp2string } from '../../helpers'; -import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render'; +import { useNavigate, useParams } from 'react-router-dom'; +import { API, copy, showError, showSuccess, timestamp2string } from '../../helpers'; +import { renderQuotaWithPrompt } from '../../helpers/render'; const EditToken = () => { const params = useParams(); const tokenId = params.id; const isEdit = tokenId !== undefined; const [loading, setLoading] = useState(isEdit); + const [modelOptions, setModelOptions] = useState([]); const originInputs = { name: '', remain_quota: isEdit ? 0 : 500000, expired_time: -1, - unlimited_quota: false + unlimited_quota: false, + models: [] }; const [inputs, setInputs] = useState(originInputs); const { name, remain_quota, expired_time, unlimited_quota } = inputs; @@ -22,8 +24,8 @@ const EditToken = () => { setInputs((inputs) => ({ ...inputs, [name]: value })); }; const handleCancel = () => { - navigate("/token"); - } + navigate('/token'); + }; const setExpiredTime = (month, day, hour, minute) => { let now = new Date(); let timestamp = now.getTime() / 1000; @@ -50,6 +52,11 @@ const EditToken = () => { if (data.expired_time !== -1) { data.expired_time = timestamp2string(data.expired_time); } + if (data.models === '') { + data.models = []; + } else { + data.models = data.models.split(','); + } setInputs(data); } else { showError(message); @@ -60,8 +67,26 @@ const EditToken = () => { if (isEdit) { loadToken().then(); } + loadAvailableModels().then(); }, []); + const loadAvailableModels = async () => { + let res = await API.get(`/api/user/available_models`); + const { success, message, data } = res.data; + if (success) { + let options = data.map((model) => { + return { + key: model, + text: model, + value: model + }; + }); + setModelOptions(options); + } else { + showError(message); + } + }; + const submit = async () => { if (!isEdit && inputs.name === '') return; let localInputs = inputs; @@ -74,6 +99,7 @@ const EditToken = () => { } localInputs.expired_time = Math.ceil(time / 1000); } + localInputs.models = localInputs.models.join(','); let res; if (isEdit) { res = await API.put(`/api/token/`, { ...localInputs, id: parseInt(tokenId) }); @@ -109,6 +135,24 @@ const EditToken = () => { required={!isEdit} /> + + { + copy(value).then(); + }} + selection + onChange={handleInputChange} + value={inputs.models} + autoComplete='new-password' + options={modelOptions} + /> + Date: Thu, 4 Apr 2024 02:44:59 +0800 Subject: [PATCH 029/121] feat: /v1/models now only return available models --- controller/model.go | 35 ++++++++++++++++++++++++++++++++++- middleware/auth.go | 3 ++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/controller/model.go b/controller/model.go index bf4b83a7..53649391 100644 --- a/controller/model.go +++ b/controller/model.go @@ -11,6 +11,7 @@ import ( relaymodel "github.com/songquanpeng/one-api/relay/model" "github.com/songquanpeng/one-api/relay/util" "net/http" + "strings" ) // https://platform.openai.com/docs/api-reference/models/list @@ -121,9 +122,41 @@ func DashboardListModels(c *gin.Context) { } func ListModels(c *gin.Context) { + ctx := c.Request.Context() + var availableModels []string + if c.GetString("available_models") != "" { + availableModels = strings.Split(c.GetString("available_models"), ",") + } else { + userId := c.GetInt("id") + userGroup, _ := model.CacheGetUserGroup(userId) + availableModels, _ = model.CacheGetGroupModels(ctx, userGroup) + } + modelSet := make(map[string]bool) + for _, availableModel := range availableModels { + modelSet[availableModel] = true + } + var availableOpenAIModels []OpenAIModels + for _, model := range openAIModels { + if _, ok := modelSet[model.Id]; ok { + modelSet[model.Id] = false + availableOpenAIModels = append(availableOpenAIModels, model) + } + } + for modelName, ok := range modelSet { + if ok { + availableOpenAIModels = append(availableOpenAIModels, OpenAIModels{ + Id: modelName, + Object: "model", + Created: 1626777600, + OwnedBy: "custom", + Root: modelName, + Parent: nil, + }) + } + } c.JSON(200, gin.H{ "object": "list", - "data": openAIModels, + "data": availableOpenAIModels, }) } diff --git a/middleware/auth.go b/middleware/auth.go index 443199d0..29701524 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -115,7 +115,8 @@ func TokenAuth() func(c *gin.Context) { } c.Set("request_model", requestModel) if token.Models != nil && *token.Models != "" { - if !isModelInList(requestModel, *token.Models) { + c.Set("available_models", *token.Models) + if requestModel != "" && !isModelInList(requestModel, *token.Models) { abortWithMessage(c, http.StatusForbidden, fmt.Sprintf("该令牌无权使用模型:%s", requestModel)) return } From 8b9fa3d6e452fbc95bfc37db836c69ed39f3f094 Mon Sep 17 00:00:00 2001 From: JustSong Date: Thu, 4 Apr 2024 02:58:21 +0800 Subject: [PATCH 030/121] fix: fix GetGroupModels --- model/ability.go | 18 ++++++++++++++++++ model/channel.go | 30 +----------------------------- 2 files changed, 19 insertions(+), 29 deletions(-) diff --git a/model/ability.go b/model/ability.go index 48b856a2..4a48bc51 100644 --- a/model/ability.go +++ b/model/ability.go @@ -1,8 +1,10 @@ package model import ( + "context" "github.com/songquanpeng/one-api/common" "gorm.io/gorm" + "sort" "strings" ) @@ -88,3 +90,19 @@ func (channel *Channel) UpdateAbilities() error { func UpdateAbilityStatus(channelId int, status bool) error { return DB.Model(&Ability{}).Where("channel_id = ?", channelId).Select("enabled").Update("enabled", status).Error } + +func GetGroupModels(ctx context.Context, group string) ([]string, error) { + groupCol := "`group`" + trueVal := "1" + if common.UsingPostgreSQL { + groupCol = `"group"` + trueVal = "true" + } + var models []string + err := DB.Model(&Ability{}).Distinct("model").Where(groupCol+" = ? and enabled = "+trueVal, group).Pluck("model", &models).Error + if err != nil { + return nil, err + } + sort.Strings(models) + return models, err +} diff --git a/model/channel.go b/model/channel.go index 24829bc5..fc4905b1 100644 --- a/model/channel.go +++ b/model/channel.go @@ -1,7 +1,6 @@ package model import ( - "context" "encoding/json" "fmt" "github.com/songquanpeng/one-api/common" @@ -9,8 +8,6 @@ import ( "github.com/songquanpeng/one-api/common/helper" "github.com/songquanpeng/one-api/common/logger" "gorm.io/gorm" - "sort" - "strings" ) type Channel struct { @@ -28,7 +25,7 @@ type Channel struct { Balance float64 `json:"balance"` // in USD BalanceUpdatedTime int64 `json:"balance_updated_time" gorm:"bigint"` Models string `json:"models"` - Group string `json:"group" gorm:"index;type:varchar(32);default:'default'"` + Group string `json:"group" gorm:"type:varchar(32);default:'default'"` UsedQuota int64 `json:"used_quota" gorm:"bigint;default:0"` ModelMapping *string `json:"model_mapping" gorm:"type:varchar(1024);default:''"` Priority *int64 `json:"priority" gorm:"bigint;default:0"` @@ -205,28 +202,3 @@ func DeleteDisabledChannel() (int64, error) { result := DB.Where("status = ? or status = ?", common.ChannelStatusAutoDisabled, common.ChannelStatusManuallyDisabled).Delete(&Channel{}) return result.RowsAffected, result.Error } - -func GetGroupModels(ctx context.Context, group string) ([]string, error) { - groupCol := "`group`" - if common.UsingPostgreSQL { - groupCol = `"group"` - } - var modelsList []string - err := DB.Model(&Channel{}).Distinct("models").Where(groupCol+" = ?", group).Pluck("models", &modelsList).Error - if err != nil { - return nil, err - } - set := make(map[string]bool) - for i := 0; i < len(modelsList); i++ { - modelList := strings.Split(modelsList[i], ",") - for _, model := range modelList { - set[model] = true - } - } - modelList := make([]string, 0, len(set)) - for model := range set { - modelList = append(modelList, model) - } - sort.Strings(modelList) - return modelList, err -} From ed70881a58bc77c9be86122d95612c2d225be633 Mon Sep 17 00:00:00 2001 From: JustSong Date: Thu, 4 Apr 2024 11:18:21 +0800 Subject: [PATCH 031/121] fix: fix token create --- controller/token.go | 1 + 1 file changed, 1 insertion(+) diff --git a/controller/token.go b/controller/token.go index c6128534..13b90de0 100644 --- a/controller/token.go +++ b/controller/token.go @@ -130,6 +130,7 @@ func AddToken(c *gin.Context) { ExpiredTime: token.ExpiredTime, RemainQuota: token.RemainQuota, UnlimitedQuota: token.UnlimitedQuota, + Models: token.Models, } err = cleanToken.Insert() if err != nil { From fb90747c23373bce14e92abb823f08387c005aea Mon Sep 17 00:00:00 2001 From: JustSong Date: Thu, 4 Apr 2024 18:53:42 +0800 Subject: [PATCH 032/121] fix: fix /v1/models return null data when no models available --- controller/model.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/model.go b/controller/model.go index 53649391..43e73c6c 100644 --- a/controller/model.go +++ b/controller/model.go @@ -135,7 +135,7 @@ func ListModels(c *gin.Context) { for _, availableModel := range availableModels { modelSet[availableModel] = true } - var availableOpenAIModels []OpenAIModels + availableOpenAIModels := make([]OpenAIModels, 0) for _, model := range openAIModels { if _, ok := modelSet[model.Id]; ok { modelSet[model.Id] = false From 6f036bd0c937afc9e477d421dd8c3113424f313b Mon Sep 17 00:00:00 2001 From: Yang Fei Date: Thu, 4 Apr 2024 23:32:59 +0800 Subject: [PATCH 033/121] feat: add embedding-2 support for zhipu (#1273) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 增加对智谱embedding-2模型的支持 * fix: fix usage & ratio --------- Co-authored-by: yangfei Co-authored-by: JustSong --- common/model-ratio.go | 1 + relay/channel/zhipu/adaptor.go | 44 ++++++++++++++++++++++-------- relay/channel/zhipu/constants.go | 2 +- relay/channel/zhipu/main.go | 47 ++++++++++++++++++++++++++++++++ relay/channel/zhipu/model.go | 18 ++++++++++++ 5 files changed, 100 insertions(+), 12 deletions(-) diff --git a/common/model-ratio.go b/common/model-ratio.go index aa75042e..d8356dc2 100644 --- a/common/model-ratio.go +++ b/common/model-ratio.go @@ -91,6 +91,7 @@ var ModelRatio = map[string]float64{ "glm-4": 0.1 * RMB, "glm-4v": 0.1 * RMB, "glm-3-turbo": 0.005 * RMB, + "embedding-2": 0.0005 * RMB, "chatglm_turbo": 0.3572, // ¥0.005 / 1k tokens "chatglm_pro": 0.7143, // ¥0.01 / 1k tokens "chatglm_std": 0.3572, // ¥0.005 / 1k tokens diff --git a/relay/channel/zhipu/adaptor.go b/relay/channel/zhipu/adaptor.go index 0ca23d59..7b570e71 100644 --- a/relay/channel/zhipu/adaptor.go +++ b/relay/channel/zhipu/adaptor.go @@ -6,6 +6,7 @@ import ( "github.com/gin-gonic/gin" "github.com/songquanpeng/one-api/relay/channel" "github.com/songquanpeng/one-api/relay/channel/openai" + "github.com/songquanpeng/one-api/relay/constant" "github.com/songquanpeng/one-api/relay/model" "github.com/songquanpeng/one-api/relay/util" "io" @@ -35,6 +36,9 @@ func (a *Adaptor) GetRequestURL(meta *util.RelayMeta) (string, error) { if a.APIVersion == "v4" { return fmt.Sprintf("%s/api/paas/v4/chat/completions", meta.BaseURL), nil } + if meta.Mode == constant.RelayModeEmbeddings { + return fmt.Sprintf("%s/api/paas/v4/embeddings", meta.BaseURL), nil + } method := "invoke" if meta.IsStream { method = "sse-invoke" @@ -53,18 +57,24 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.G if request == nil { return nil, errors.New("request is nil") } - // TopP (0.0, 1.0) - request.TopP = math.Min(0.99, request.TopP) - request.TopP = math.Max(0.01, request.TopP) + switch relayMode { + case constant.RelayModeEmbeddings: + baiduEmbeddingRequest := ConvertEmbeddingRequest(*request) + return baiduEmbeddingRequest, nil + default: + // TopP (0.0, 1.0) + request.TopP = math.Min(0.99, request.TopP) + request.TopP = math.Max(0.01, request.TopP) - // Temperature (0.0, 1.0) - request.Temperature = math.Min(0.99, request.Temperature) - request.Temperature = math.Max(0.01, request.Temperature) - a.SetVersionByModeName(request.Model) - if a.APIVersion == "v4" { - return request, nil + // Temperature (0.0, 1.0) + request.Temperature = math.Min(0.99, request.Temperature) + request.Temperature = math.Max(0.01, request.Temperature) + a.SetVersionByModeName(request.Model) + if a.APIVersion == "v4" { + return request, nil + } + return ConvertRequest(*request), nil } - return ConvertRequest(*request), nil } func (a *Adaptor) DoRequest(c *gin.Context, meta *util.RelayMeta, requestBody io.Reader) (*http.Response, error) { @@ -84,14 +94,26 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *util.Rel if a.APIVersion == "v4" { return a.DoResponseV4(c, resp, meta) } + if meta.IsStream { err, usage = StreamHandler(c, resp) } else { - err, usage = Handler(c, resp) + if meta.Mode == constant.RelayModeEmbeddings { + err, usage = EmbeddingsHandler(c, resp) + } else { + err, usage = Handler(c, resp) + } } return } +func ConvertEmbeddingRequest(request model.GeneralOpenAIRequest) *EmbeddingRequest { + return &EmbeddingRequest{ + Model: "embedding-2", + Input: request.Input.(string), + } +} + func (a *Adaptor) GetModelList() []string { return ModelList } diff --git a/relay/channel/zhipu/constants.go b/relay/channel/zhipu/constants.go index 1655a59d..2daeb19c 100644 --- a/relay/channel/zhipu/constants.go +++ b/relay/channel/zhipu/constants.go @@ -2,5 +2,5 @@ package zhipu var ModelList = []string{ "chatglm_turbo", "chatglm_pro", "chatglm_std", "chatglm_lite", - "glm-4", "glm-4v", "glm-3-turbo", + "glm-4", "glm-4v", "glm-3-turbo", "embedding-2", } diff --git a/relay/channel/zhipu/main.go b/relay/channel/zhipu/main.go index a46fd537..f54e0504 100644 --- a/relay/channel/zhipu/main.go +++ b/relay/channel/zhipu/main.go @@ -254,3 +254,50 @@ func Handler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, * _, err = c.Writer.Write(jsonResponse) return nil, &fullTextResponse.Usage } + +func EmbeddingsHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { + var zhipuResponse EmbeddingRespone + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return openai.ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil + } + err = resp.Body.Close() + if err != nil { + return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil + } + err = json.Unmarshal(responseBody, &zhipuResponse) + if err != nil { + return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil + } + fullTextResponse := embeddingResponseZhipu2OpenAI(&zhipuResponse) + 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 embeddingResponseZhipu2OpenAI(response *EmbeddingRespone) *openai.EmbeddingResponse { + openAIEmbeddingResponse := openai.EmbeddingResponse{ + Object: "list", + Data: make([]openai.EmbeddingResponseItem, 0, len(response.Embeddings)), + Model: response.Model, + Usage: model.Usage{ + PromptTokens: response.PromptTokens, + CompletionTokens: response.CompletionTokens, + TotalTokens: response.Usage.TotalTokens, + }, + } + + for _, item := range response.Embeddings { + openAIEmbeddingResponse.Data = append(openAIEmbeddingResponse.Data, openai.EmbeddingResponseItem{ + Object: `embedding`, + Index: item.Index, + Embedding: item.Embedding, + }) + } + return &openAIEmbeddingResponse +} diff --git a/relay/channel/zhipu/model.go b/relay/channel/zhipu/model.go index b63e1d6f..3c3a7443 100644 --- a/relay/channel/zhipu/model.go +++ b/relay/channel/zhipu/model.go @@ -44,3 +44,21 @@ type tokenData struct { Token string ExpiryTime time.Time } + +type EmbeddingRequest struct { + Model string `json:"model"` + Input string `json:"input"` +} + +type EmbeddingRespone struct { + Model string `json:"model"` + Object string `json:"object"` + Embeddings []EmbeddingData `json:"data"` + model.Usage `json:"usage"` +} + +type EmbeddingData struct { + Index int `json:"index"` + Object string `json:"object"` + Embedding []float64 `json:"embedding"` +} From f73f2e51dfcf6f15f3d26dd045ad9ae283f25760 Mon Sep 17 00:00:00 2001 From: manjieqi <40858189+manjieqi@users.noreply.github.com> Date: Fri, 5 Apr 2024 00:02:15 +0800 Subject: [PATCH 034/121] feat: update baidu model name & ratio (#1253) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 修正百度模型名称 * 更新百度模型名称,并保留旧版兼容以及修正单价 * chore: add more model and adjust order --------- Co-authored-by: JustSong --- common/model-ratio.go | 24 ++++++++++++++++-------- relay/channel/baidu/adaptor.go | 24 +++++++++++++++++------- relay/channel/baidu/constants.go | 17 ++++++++++++----- 3 files changed, 45 insertions(+), 20 deletions(-) diff --git a/common/model-ratio.go b/common/model-ratio.go index d8356dc2..94607c92 100644 --- a/common/model-ratio.go +++ b/common/model-ratio.go @@ -72,14 +72,22 @@ 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-4.0-8K": 0.120 * RMB, + "ERNIE-Bot-8K-0922": 0.024 * RMB, + "ERNIE-3.5-8K": 0.012 * RMB, + "ERNIE-Lite-8K-0922": 0.008 * RMB, + "ERNIE-Speed-8K": 0.004 * RMB, + "ERNIE-3.5-4K-0205": 0.012 * RMB, + "ERNIE-3.5-8K-0205": 0.024 * RMB, + "ERNIE-3.5-8K-1222": 0.012 * RMB, + "ERNIE-Lite-8K": 0.003 * RMB, + "ERNIE-Speed-128K": 0.004 * RMB, + "ERNIE-Tiny-8K": 0.001 * RMB, + "BLOOMZ-7B": 0.004 * RMB, + "Embedding-V1": 0.002 * RMB, + "bge-large-zh": 0.002 * RMB, + "bge-large-en": 0.002 * RMB, + "tao-8k": 0.002 * RMB, // https://ai.google.dev/pricing "PaLM-2": 1, "gemini-pro": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens diff --git a/relay/channel/baidu/adaptor.go b/relay/channel/baidu/adaptor.go index 2d2e24f6..72302fdf 100644 --- a/relay/channel/baidu/adaptor.go +++ b/relay/channel/baidu/adaptor.go @@ -38,16 +38,26 @@ func (a *Adaptor) GetRequestURL(meta *util.RelayMeta) (string, error) { suffix += "completions_pro" case "ERNIE-Bot-4": suffix += "completions_pro" - case "ERNIE-3.5-8K": - suffix += "completions" - case "ERNIE-Bot-8K": - suffix += "ernie_bot_8k" case "ERNIE-Bot": suffix += "completions" - case "ERNIE-Speed": - suffix += "ernie_speed" case "ERNIE-Bot-turbo": suffix += "eb-instant" + case "ERNIE-Speed": + suffix += "ernie_speed" + case "ERNIE-Bot-8K": + suffix += "ernie_bot_8k" + case "ERNIE-4.0-8K": + suffix += "completions_pro" + case "ERNIE-3.5-8K": + suffix += "completions" + case "ERNIE-Speed-8K": + suffix += "ernie_speed" + case "ERNIE-Speed-128K": + suffix += "ernie-speed-128k" + case "ERNIE-Lite-8K": + suffix += "ernie-lite-8k" + case "ERNIE-Tiny-8K": + suffix += "ernie-tiny-8k" case "BLOOMZ-7B": suffix += "bloomz_7b1" case "Embedding-V1": @@ -59,7 +69,7 @@ func (a *Adaptor) GetRequestURL(meta *util.RelayMeta) (string, error) { case "tao-8k": suffix += "tao_8k" default: - suffix += meta.ActualModelName + suffix += strings.ToLower(meta.ActualModelName) } fullRequestURL := fmt.Sprintf("%s/rpc/2.0/ai_custom/v1/wenxinworkshop/%s", meta.BaseURL, suffix) var accessToken string diff --git a/relay/channel/baidu/constants.go b/relay/channel/baidu/constants.go index 45a4e901..ccdc25c3 100644 --- a/relay/channel/baidu/constants.go +++ b/relay/channel/baidu/constants.go @@ -1,11 +1,18 @@ package baidu var ModelList = []string{ - "ERNIE-Bot-4", - "ERNIE-Bot-8K", - "ERNIE-Bot", - "ERNIE-Speed", - "ERNIE-Bot-turbo", + "ERNIE-4.0-8K", + "ERNIE-Bot-8K-0922", + "ERNIE-3.5-8K", + "ERNIE-Lite-8K-0922", + "ERNIE-Speed-8K", + "ERNIE-3.5-4K-0205", + "ERNIE-3.5-8K-0205", + "ERNIE-3.5-8K-1222", + "ERNIE-Lite-8K", + "ERNIE-Speed-128K", + "ERNIE-Tiny-8K", + "BLOOMZ-7B", "Embedding-V1", "bge-large-zh", "bge-large-en", From 1f80b0a39fb728776fdfa635d17fbc90de56baef Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 5 Apr 2024 00:13:37 +0800 Subject: [PATCH 035/121] chore: add omitempty for xunfei functions --- relay/channel/xunfei/model.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/relay/channel/xunfei/model.go b/relay/channel/xunfei/model.go index e9cc59a6..97a43154 100644 --- a/relay/channel/xunfei/model.go +++ b/relay/channel/xunfei/model.go @@ -28,7 +28,7 @@ type ChatRequest struct { } `json:"message"` Functions struct { Text []model.Function `json:"text,omitempty"` - } `json:"functions"` + } `json:"functions,omitempty"` } `json:"payload"` } From 1994256bac48dc7d55d22f9a43577651eb187693 Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 5 Apr 2024 00:18:26 +0800 Subject: [PATCH 036/121] chore: disable channel when error message contain quota --- relay/util/common.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/relay/util/common.go b/relay/util/common.go index 535ef680..0bb76909 100644 --- a/relay/util/common.go +++ b/relay/util/common.go @@ -46,6 +46,9 @@ func ShouldDisableChannel(err *relaymodel.Error, statusCode int) bool { } else if strings.HasPrefix(err.Message, "This organization has been disabled.") { return true } + if strings.Contains(err.Message, "quota") { + return true + } return false } From 76569bb0b64d470aee3e970fe8e82557c8931cde Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 5 Apr 2024 00:31:41 +0800 Subject: [PATCH 037/121] chore: disable channel when error message contain credit or balance --- relay/util/common.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/relay/util/common.go b/relay/util/common.go index 0bb76909..d1f79a26 100644 --- a/relay/util/common.go +++ b/relay/util/common.go @@ -49,6 +49,12 @@ func ShouldDisableChannel(err *relaymodel.Error, statusCode int) bool { if strings.Contains(err.Message, "quota") { return true } + if strings.Contains(err.Message, "credit") { + return true + } + if strings.Contains(err.Message, "balance") { + return true + } return false } From 054b00b7250853f9ba345dc5756ae345ec5666bf Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 5 Apr 2024 00:40:48 +0800 Subject: [PATCH 038/121] docs: add API docs --- README.md | 1 + docs/API.md | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 docs/API.md diff --git a/README.md b/README.md index 2dcdbd4f..53847b45 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 + 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。 23. 支持主题切换,设置环境变量 `THEME` 即可,默认为 `default`,欢迎 PR 更多主题,具体参考[此处](./web/README.md)。 24. 配合 [Message Pusher](https://github.com/songquanpeng/message-pusher) 可将报警信息推送到多种 App 上。 +25. 支持**扩展**,详情请参考此处 [API 文档](./docs/API.md)。 ## 部署 ### 基于 Docker 进行部署 diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 00000000..9fc350ef --- /dev/null +++ b/docs/API.md @@ -0,0 +1,17 @@ +# 使用 API 操控 & 扩展 One API +> 欢迎提交 PR 在此放上你的拓展项目。 + +## 鉴权 +One API 支持两种鉴权方式:Cookie 和 Token,对于 Token,参照下图获取: + +![image](https://github.com/songquanpeng/songquanpeng.github.io/assets/39998050/c15281a7-83ed-47cb-a1f6-913cb6bf4a7c) + +之后,将 Token 作为请求头的 Authorization 字段的值即可,例如下面使用 Token 调用测试渠道的 API: +![image](https://github.com/songquanpeng/songquanpeng.github.io/assets/39998050/1273b7ae-cb60-4c0d-93a6-b1cbc039c4f8) + +## API 列表 +> 当前 API 列表不全,请自行通过浏览器抓取前端请求 + +欢迎此处 PR 补充。 + +如果现有的 API 没有办法满足你的需求,欢迎提交 issue 讨论。 \ No newline at end of file From 0a37aa4cbd322e7ff44446ae5f130a43a90a630e Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 5 Apr 2024 01:10:30 +0800 Subject: [PATCH 039/121] docs: add API docs --- controller/user.go | 77 ++++++++++++++++++++++++++++++++------------ docs/API.md | 31 ++++++++++++++++-- model/log.go | 15 +++++++++ router/api-router.go | 1 + 4 files changed, 101 insertions(+), 23 deletions(-) diff --git a/controller/user.go b/controller/user.go index 8b614e5d..61055878 100644 --- a/controller/user.go +++ b/controller/user.go @@ -180,27 +180,27 @@ func Register(c *gin.Context) { } func GetAllUsers(c *gin.Context) { - 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, - }) + 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) { @@ -770,3 +770,38 @@ func TopUp(c *gin.Context) { }) return } + +type adminTopUpRequest struct { + UserId int `json:"user_id"` + Quota int `json:"quota"` + Remark string `json:"remark"` +} + +func AdminTopUp(c *gin.Context) { + req := adminTopUpRequest{} + err := c.ShouldBindJSON(&req) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + err = model.IncreaseUserQuota(req.UserId, int64(req.Quota)) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + if req.Remark == "" { + req.Remark = fmt.Sprintf("通过 API 充值 %s", common.LogQuota(int64(req.Quota))) + } + model.RecordTopupLog(req.UserId, req.Remark, req.Quota) + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + }) + return +} diff --git a/docs/API.md b/docs/API.md index 9fc350ef..72ae7d91 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,6 +1,10 @@ # 使用 API 操控 & 扩展 One API > 欢迎提交 PR 在此放上你的拓展项目。 +例如,虽然 One API 本身没有直接支持支付,但是你可以通过系统扩展的 API 来实现支付功能。 + +又或者你想自定义渠道管理策略,也可以通过 API 来实现渠道的禁用与启用。 + ## 鉴权 One API 支持两种鉴权方式:Cookie 和 Token,对于 Token,参照下图获取: @@ -9,9 +13,32 @@ One API 支持两种鉴权方式:Cookie 和 Token,对于 Token,参照下 之后,将 Token 作为请求头的 Authorization 字段的值即可,例如下面使用 Token 调用测试渠道的 API: ![image](https://github.com/songquanpeng/songquanpeng.github.io/assets/39998050/1273b7ae-cb60-4c0d-93a6-b1cbc039c4f8) +## 请求格式与响应格式 +One API 使用 JSON 格式进行请求和响应。 + +对于响应体,一般格式如下: +```json +{ + "message": "请求信息", + "success": true, + "data": {} +} +``` + ## API 列表 > 当前 API 列表不全,请自行通过浏览器抓取前端请求 -欢迎此处 PR 补充。 +如果现有的 API 没有办法满足你的需求,欢迎提交 issue 讨论。 -如果现有的 API 没有办法满足你的需求,欢迎提交 issue 讨论。 \ No newline at end of file +### 获取当前登录用户信息 +**GET** `/api/user/self` + +### 为给定用户充值额度 +**POST** `/api/topup` +```json +{ + "user_id": 1, + "quota": 100000, + "remark": "充值 100000 额度" +} +``` \ No newline at end of file diff --git a/model/log.go b/model/log.go index 4409f73e..6b679c36 100644 --- a/model/log.go +++ b/model/log.go @@ -51,6 +51,21 @@ func RecordLog(userId int, logType int, content string) { } } +func RecordTopupLog(userId int, content string, quota int) { + log := &Log{ + UserId: userId, + Username: GetUsernameById(userId), + CreatedAt: helper.GetTimestamp(), + Type: LogTypeTopup, + Content: content, + Quota: quota, + } + err := LOG_DB.Create(log).Error + if err != nil { + logger.SysError("failed to record log: " + err.Error()) + } +} + func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptTokens int, completionTokens int, modelName string, tokenName string, quota int64, content string) { logger.Info(ctx, fmt.Sprintf("record consume log: userId=%d, channelId=%d, promptTokens=%d, completionTokens=%d, modelName=%s, tokenName=%s, quota=%d, content=%s", userId, channelId, promptTokens, completionTokens, modelName, tokenName, quota, content)) if !config.LogConsumeEnabled { diff --git a/router/api-router.go b/router/api-router.go index 4aa6d830..1558640f 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -26,6 +26,7 @@ func SetApiRouter(router *gin.Engine) { apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth) apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind) apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.EmailBind) + apiRouter.POST("/topup", middleware.AdminAuth(), controller.AdminTopUp) userRoute := apiRouter.Group("/user") { From f8cc63f00b47a2279091e122f8815050262a31e2 Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 5 Apr 2024 01:23:11 +0800 Subject: [PATCH 040/121] feat: add user info to topup link --- web/default/src/pages/TopUp/index.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/web/default/src/pages/TopUp/index.js b/web/default/src/pages/TopUp/index.js index f52cb8d5..2fcf0eae 100644 --- a/web/default/src/pages/TopUp/index.js +++ b/web/default/src/pages/TopUp/index.js @@ -8,6 +8,7 @@ const TopUp = () => { const [topUpLink, setTopUpLink] = useState(''); const [userQuota, setUserQuota] = useState(0); const [isSubmitting, setIsSubmitting] = useState(false); + const [user, setUser] = useState({}); const topUp = async () => { if (redemptionCode === '') { @@ -41,7 +42,14 @@ const TopUp = () => { showError('超级管理员未设置充值链接!'); return; } - window.open(topUpLink, '_blank'); + let url = new URL(topUpLink); + let username = user.username; + let user_id = user.id; + // add username and user_id to the topup link + url.searchParams.append('username', username); + url.searchParams.append('user_id', user_id); + url.searchParams.append('transaction_id', crypto.randomUUID()); + window.open(url.toString(), '_blank'); }; const getUserQuota = async ()=>{ @@ -49,6 +57,7 @@ const TopUp = () => { const {success, message, data} = res.data; if (success) { setUserQuota(data.quota); + setUser(data); } else { showError(message); } @@ -80,7 +89,7 @@ const TopUp = () => { }} /> + ) : ( + <> + )} {status.wechat_login ? ( ) } + { + status.lark_client_id && ( + + ) + } - ) : ( - <> - )} - {status.wechat_login ? ( -
) : ( <> From 572fc9ffb8b2ccf47efacaa8f311e0810c45e736 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 6 Apr 2024 10:43:54 +0800 Subject: [PATCH 071/121] fix: fix stepfun model ratio & id --- relay/billing/ratio/model.go | 4 ++++ web/default/src/constants/channel.constants.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/relay/billing/ratio/model.go b/relay/billing/ratio/model.go index e98c5be8..108924a1 100644 --- a/relay/billing/ratio/model.go +++ b/relay/billing/ratio/model.go @@ -154,6 +154,10 @@ var ModelRatio = map[string]float64{ "yi-34b-chat-0205": 2.5 / 1000 * RMB, "yi-34b-chat-200k": 12.0 / 1000 * RMB, "yi-vl-plus": 6.0 / 1000 * RMB, + // stepfun todo + "step-1v-32k": 0.024 * RMB, + "step-1-32k": 0.024 * RMB, + "step-1-200k": 0.15 * RMB, } var CompletionRatio = map[string]float64{} diff --git a/web/default/src/constants/channel.constants.js b/web/default/src/constants/channel.constants.js index 2bf53c81..7535b666 100644 --- a/web/default/src/constants/channel.constants.js +++ b/web/default/src/constants/channel.constants.js @@ -17,7 +17,7 @@ export const CHANNEL_OPTIONS = [ { key: 29, text: 'Groq', value: 29, color: 'orange' }, { key: 30, text: 'Ollama', value: 30, color: 'black' }, { key: 31, text: '零一万物', value: 31, color: 'green' }, - { key: 31, text: '阶跃星辰', value: 32, color: 'blue' }, + { key: 32, text: '阶跃星辰', value: 32, color: 'blue' }, { key: 8, text: '自定义渠道', value: 8, color: 'pink' }, { key: 22, text: '知识库:FastGPT', value: 22, color: 'blue' }, { key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple' }, From acf8cb624889fc05bc5bf9db99c69d5624e73a88 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 6 Apr 2024 11:47:31 +0800 Subject: [PATCH 072/121] chore: update default nextweb link --- web/air/src/components/TokensTable.js | 2 +- web/berry/src/views/Token/component/TableRow.js | 2 +- web/default/src/components/TokensTable.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/web/air/src/components/TokensTable.js b/web/air/src/components/TokensTable.js index c106b388..0853ddfb 100644 --- a/web/air/src/components/TokensTable.js +++ b/web/air/src/components/TokensTable.js @@ -319,7 +319,7 @@ const TokensTable = () => { if (nextLink) { nextUrl = nextLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; } else { - nextUrl = `https://chat.oneapi.pro/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; + nextUrl = `https://app.nextchat.dev/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; } let url; diff --git a/web/berry/src/views/Token/component/TableRow.js b/web/berry/src/views/Token/component/TableRow.js index 2753764c..51ab0d4b 100644 --- a/web/berry/src/views/Token/component/TableRow.js +++ b/web/berry/src/views/Token/component/TableRow.js @@ -28,7 +28,7 @@ const COPY_OPTIONS = [ { key: 'next', text: 'ChatGPT Next', - url: 'https://chat.oneapi.pro/#/?settings={"key":"sk-{key}","url":"{serverAddress}"}', + url: 'https://app.nextchat.dev/#/?settings={"key":"sk-{key}","url":"{serverAddress}"}', encode: false }, { key: 'ama', text: 'BotGem', url: 'ama://set-api-key?server={serverAddress}&key=sk-{key}', encode: true }, diff --git a/web/default/src/components/TokensTable.js b/web/default/src/components/TokensTable.js index 19a688bb..4f6c118e 100644 --- a/web/default/src/components/TokensTable.js +++ b/web/default/src/components/TokensTable.js @@ -99,7 +99,7 @@ const TokensTable = () => { if (nextLink) { nextUrl = nextLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; } else { - nextUrl = `https://chat.oneapi.pro/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; + nextUrl = `https://app.nextchat.dev/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; } let url; @@ -141,7 +141,7 @@ const TokensTable = () => { if (chatLink) { defaultUrl = chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; } else { - defaultUrl = `https://chat.oneapi.pro/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; + defaultUrl = `https://app.nextchat.dev/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; } let url; switch (type) { From 3fe2863ff72ac5c87c7d5b6e1c94564afdaf4753 Mon Sep 17 00:00:00 2001 From: Buer <42402987+MartialBE@users.noreply.github.com> Date: Sat, 6 Apr 2024 19:44:23 +0800 Subject: [PATCH 073/121] feat: berry theme update & bug fix (#1282) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ⚡️ improve: delete google fonts * ⚡️ improve: Optimized priority input handling in TableRow component. * 🔖 chore: channel batch add * ✨ feat: add dark mod * ✨ feat: support token limit ip range and models * ✨ feat: add MessagePusher * ✨ feat: add lark login --- web/berry/.prettierrc | 8 + web/berry/public/index.html | 5 - web/berry/src/App.js | 13 +- web/berry/src/assets/fonts/roboto-500.woff2 | Bin 0 -> 11072 bytes web/berry/src/assets/fonts/roboto-700.woff2 | Bin 0 -> 11040 bytes .../src/assets/fonts/roboto-regular.woff2 | Bin 0 -> 11028 bytes web/berry/src/assets/images/icons/lark.svg | 1 + web/berry/src/assets/images/logo-white.svg | 13 + .../src/assets/scss/_themes-vars.module.scss | 10 + web/berry/src/assets/scss/fonts.scss | 32 +++ web/berry/src/assets/scss/style.scss | 1 + web/berry/src/hooks/useLogin.js | 24 +- .../MainLayout/Header/ProfileSection/index.js | 4 +- .../src/layout/MainLayout/Header/index.js | 6 +- .../MainLayout/Sidebar/MenuCard/index.js | 3 +- .../src/layout/MainLayout/Sidebar/index.js | 16 +- .../src/layout/MinimalLayout/Header/index.js | 174 +++++++++--- web/berry/src/layout/MinimalLayout/index.js | 10 +- web/berry/src/routes/OtherRoutes.js | 5 + web/berry/src/store/actions.js | 1 + web/berry/src/store/customizationReducer.js | 8 +- web/berry/src/themes/compStyleOverride.js | 39 ++- web/berry/src/themes/index.js | 59 +++- web/berry/src/themes/palette.js | 2 +- web/berry/src/themes/typography.js | 13 + web/berry/src/ui-component/Logo.js | 6 +- web/berry/src/ui-component/ThemeButton.js | 50 ++++ web/berry/src/ui-component/cards/MainCard.js | 2 +- web/berry/src/ui-component/cards/SubCard.js | 10 +- web/berry/src/utils/chart.js | 3 +- web/berry/src/utils/common.js | 7 + .../views/Authentication/Auth/LarkOAuth.js | 94 +++++++ .../Authentication/AuthForms/AuthLogin.js | 28 +- .../src/views/Authentication/AuthWrapper.js | 2 +- .../src/views/Channel/component/EditModal.js | 101 ++++--- .../src/views/Channel/component/TableRow.js | 61 ++--- .../component/StatisticalLineChartCard.js | 2 +- web/berry/src/views/Profile/index.js | 16 +- .../views/Setting/component/SystemSetting.js | 134 ++++++++- .../src/views/Token/component/EditModal.js | 254 ++++++++++-------- 40 files changed, 936 insertions(+), 281 deletions(-) create mode 100644 web/berry/.prettierrc create mode 100644 web/berry/src/assets/fonts/roboto-500.woff2 create mode 100644 web/berry/src/assets/fonts/roboto-700.woff2 create mode 100644 web/berry/src/assets/fonts/roboto-regular.woff2 create mode 100644 web/berry/src/assets/images/icons/lark.svg create mode 100644 web/berry/src/assets/images/logo-white.svg create mode 100644 web/berry/src/assets/scss/fonts.scss create mode 100644 web/berry/src/ui-component/ThemeButton.js create mode 100644 web/berry/src/views/Authentication/Auth/LarkOAuth.js diff --git a/web/berry/.prettierrc b/web/berry/.prettierrc new file mode 100644 index 00000000..d5fba07c --- /dev/null +++ b/web/berry/.prettierrc @@ -0,0 +1,8 @@ +{ + "bracketSpacing": true, + "printWidth": 140, + "singleQuote": true, + "trailingComma": "none", + "tabWidth": 2, + "useTabs": false +} diff --git a/web/berry/public/index.html b/web/berry/public/index.html index 6f232250..abd079e1 100644 --- a/web/berry/public/index.html +++ b/web/berry/public/index.html @@ -11,11 +11,6 @@ name="description" content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用" /> - - diff --git a/web/berry/src/App.js b/web/berry/src/App.js index fc54c632..d6422a0f 100644 --- a/web/berry/src/App.js +++ b/web/berry/src/App.js @@ -1,8 +1,9 @@ -import { useSelector } from 'react-redux'; +import { useEffect } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; import { ThemeProvider } from '@mui/material/styles'; import { CssBaseline, StyledEngineProvider } from '@mui/material'; - +import { SET_THEME } from 'store/actions'; // routing import Routes from 'routes'; @@ -20,8 +21,16 @@ import { SnackbarProvider } from 'notistack'; // ==============================|| APP ||============================== // const App = () => { + const dispatch = useDispatch(); const customization = useSelector((state) => state.customization); + useEffect(() => { + const storedTheme = localStorage.getItem('theme'); + if (storedTheme) { + dispatch({ type: SET_THEME, theme: storedTheme }); + } + }, [dispatch]); + return ( diff --git a/web/berry/src/assets/fonts/roboto-500.woff2 b/web/berry/src/assets/fonts/roboto-500.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..2360b721dc266e107240483333017cb941baf4e9 GIT binary patch literal 11072 zcmV-GE5FotPew8T0RR9104qQM4gdfE08}&p04nML0RR9100000000000000000000 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 ON3B$ub4ks;IsgFgYn-P5 literal 0 HcmV?d00001 diff --git a/web/berry/src/assets/images/icons/lark.svg b/web/berry/src/assets/images/icons/lark.svg new file mode 100644 index 00000000..239e1bef --- /dev/null +++ b/web/berry/src/assets/images/icons/lark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/berry/src/assets/images/logo-white.svg b/web/berry/src/assets/images/logo-white.svg new file mode 100644 index 00000000..d6289b9a --- /dev/null +++ b/web/berry/src/assets/images/logo-white.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/berry/src/assets/scss/_themes-vars.module.scss b/web/berry/src/assets/scss/_themes-vars.module.scss index a470b033..661bb6c6 100644 --- a/web/berry/src/assets/scss/_themes-vars.module.scss +++ b/web/berry/src/assets/scss/_themes-vars.module.scss @@ -46,11 +46,16 @@ $grey600: #4b5565; $grey700: #364152; $grey900: #121926; +$tableBackground: #f4f6f8; +$tableBorderBottom: #f1f3f4; + // ==============================|| DARK THEME VARIANTS ||============================== // // paper & background $darkBackground: #1a223f; // level 3 $darkPaper: #111936; // level 4 +$darkDivider: rgba(227, 232, 239, 0.2); +$darkSelectedBack : rgba(124, 77, 255, 0.15); // dark 800 & 900 $darkLevel1: #29314f; // level 1 @@ -154,4 +159,9 @@ $darkTextSecondary: #8492c4; darkSecondaryDark: $darkSecondaryDark; darkSecondary200: $darkSecondary200; darkSecondary800: $darkSecondary800; + + darkDivider: $darkDivider; + darkSelectedBack: $darkSelectedBack; + tableBackground: $tableBackground; + tableBorderBottom: $tableBorderBottom; } diff --git a/web/berry/src/assets/scss/fonts.scss b/web/berry/src/assets/scss/fonts.scss new file mode 100644 index 00000000..c792aab2 --- /dev/null +++ b/web/berry/src/assets/scss/fonts.scss @@ -0,0 +1,32 @@ + +/* roboto-regular */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: local('Roboto'), url('../fonts/roboto-regular.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + } + + /* roboto-500 */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: local('Roboto'), url('../fonts/roboto-500.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + + +/* roboto-700 */ +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: local('Roboto'), url('../fonts/roboto-700.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + \ No newline at end of file diff --git a/web/berry/src/assets/scss/style.scss b/web/berry/src/assets/scss/style.scss index 17d566e6..5d2d8975 100644 --- a/web/berry/src/assets/scss/style.scss +++ b/web/berry/src/assets/scss/style.scss @@ -1,3 +1,4 @@ +@import 'fonts.scss'; // color variants @import 'themes-vars.module.scss'; diff --git a/web/berry/src/hooks/useLogin.js b/web/berry/src/hooks/useLogin.js index 53626577..39d8b407 100644 --- a/web/berry/src/hooks/useLogin.js +++ b/web/berry/src/hooks/useLogin.js @@ -48,6 +48,28 @@ const useLogin = () => { } }; + const larkLogin = async (code, state) => { + try { + const res = await API.get(`/api/oauth/lark?code=${code}&state=${state}`); + const { success, message, data } = res.data; + if (success) { + if (message === 'bind') { + showSuccess('绑定成功!'); + navigate('/panel'); + } else { + dispatch({ type: LOGIN, payload: data }); + localStorage.setItem('user', JSON.stringify(data)); + showSuccess('登录成功!'); + navigate('/panel'); + } + } + return { success, message }; + } catch (err) { + // 请求失败,设置错误信息 + return { success: false, message: '' }; + } + }; + const wechatLogin = async (code) => { try { const res = await API.get(`/api/oauth/wechat?code=${code}`); @@ -72,7 +94,7 @@ const useLogin = () => { navigate('/'); }; - return { login, logout, githubLogin, wechatLogin }; + return { login, logout, githubLogin, wechatLogin, larkLogin }; }; export default useLogin; diff --git a/web/berry/src/layout/MainLayout/Header/ProfileSection/index.js b/web/berry/src/layout/MainLayout/Header/ProfileSection/index.js index 3e351254..e1392dc0 100644 --- a/web/berry/src/layout/MainLayout/Header/ProfileSection/index.js +++ b/web/berry/src/layout/MainLayout/Header/ProfileSection/index.js @@ -71,8 +71,8 @@ const ProfileSection = () => { alignItems: 'center', borderRadius: '27px', transition: 'all .2s ease-in-out', - borderColor: theme.palette.primary.light, - backgroundColor: theme.palette.primary.light, + borderColor: theme.typography.menuChip.background, + backgroundColor: theme.typography.menuChip.background, '&[aria-controls="menu-list-grow"], &:hover': { borderColor: theme.palette.primary.main, background: `${theme.palette.primary.main}!important`, diff --git a/web/berry/src/layout/MainLayout/Header/index.js b/web/berry/src/layout/MainLayout/Header/index.js index 51d40c75..8fd9c950 100644 --- a/web/berry/src/layout/MainLayout/Header/index.js +++ b/web/berry/src/layout/MainLayout/Header/index.js @@ -7,6 +7,7 @@ import { Avatar, Box, ButtonBase } from '@mui/material'; // project imports import LogoSection from '../LogoSection'; import ProfileSection from './ProfileSection'; +import ThemeButton from 'ui-component/ThemeButton'; // assets import { IconMenu2 } from '@tabler/icons-react'; @@ -37,9 +38,8 @@ const Header = ({ handleLeftDrawerToggle }) => { sx={{ ...theme.typography.commonAvatar, ...theme.typography.mediumAvatar, + ...theme.typography.menuButton, transition: 'all .2s ease-in-out', - background: theme.palette.secondary.light, - color: theme.palette.secondary.dark, '&:hover': { background: theme.palette.secondary.dark, color: theme.palette.secondary.light @@ -55,7 +55,7 @@ const Header = ({ handleLeftDrawerToggle }) => { - + ); diff --git a/web/berry/src/layout/MainLayout/Sidebar/MenuCard/index.js b/web/berry/src/layout/MainLayout/Sidebar/MenuCard/index.js index 16b13231..dadd3eca 100644 --- a/web/berry/src/layout/MainLayout/Sidebar/MenuCard/index.js +++ b/web/berry/src/layout/MainLayout/Sidebar/MenuCard/index.js @@ -36,7 +36,7 @@ import { useNavigate } from 'react-router-dom'; // })); const CardStyle = styled(Card)(({ theme }) => ({ - background: theme.palette.primary.light, + background: theme.typography.menuChip.background, marginBottom: '22px', overflow: 'hidden', position: 'relative', @@ -121,7 +121,6 @@ const MenuCard = () => { /> - {/* */} ); diff --git a/web/berry/src/layout/MainLayout/Sidebar/index.js b/web/berry/src/layout/MainLayout/Sidebar/index.js index e3c6d12d..10652ba6 100644 --- a/web/berry/src/layout/MainLayout/Sidebar/index.js +++ b/web/berry/src/layout/MainLayout/Sidebar/index.js @@ -39,7 +39,13 @@ const Sidebar = ({ drawerOpen, drawerToggle, window }) => { - + @@ -48,7 +54,13 @@ const Sidebar = ({ drawerOpen, drawerToggle, window }) => { - + diff --git a/web/berry/src/layout/MinimalLayout/Header/index.js b/web/berry/src/layout/MinimalLayout/Header/index.js index 4f61da60..feaeb603 100644 --- a/web/berry/src/layout/MinimalLayout/Header/index.js +++ b/web/berry/src/layout/MinimalLayout/Header/index.js @@ -1,10 +1,30 @@ // material-ui -import { useTheme } from "@mui/material/styles"; -import { Box, Button, Stack } from "@mui/material"; -import LogoSection from "layout/MainLayout/LogoSection"; -import { Link } from "react-router-dom"; -import { useLocation } from "react-router-dom"; -import { useSelector } from "react-redux"; +import { useState } from 'react'; +import { useTheme } from '@mui/material/styles'; +import { + Box, + Button, + Stack, + Popper, + IconButton, + List, + ListItemButton, + Paper, + ListItemText, + Typography, + Divider, + ClickAwayListener +} from '@mui/material'; +import LogoSection from 'layout/MainLayout/LogoSection'; +import { Link } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; +import { useSelector } from 'react-redux'; +import ThemeButton from 'ui-component/ThemeButton'; +import ProfileSection from 'layout/MainLayout/Header/ProfileSection'; +import { IconMenu2 } from '@tabler/icons-react'; +import Transitions from 'ui-component/extended/Transitions'; +import MainCard from 'ui-component/cards/MainCard'; +import { useMediaQuery } from '@mui/material'; // ==============================|| MAIN NAVBAR / HEADER ||============================== // @@ -12,16 +32,26 @@ const Header = () => { const theme = useTheme(); const { pathname } = useLocation(); const account = useSelector((state) => state.account); + const [open, setOpen] = useState(null); + const isMobile = useMediaQuery(theme.breakpoints.down('sm')); + + const handleOpenMenu = (event) => { + setOpen(open ? null : event.currentTarget); + }; + + const handleCloseMenu = () => { + setOpen(null); + }; return ( <> @@ -31,43 +61,99 @@ const Header = () => { - - - - {account.user ? ( - + + {isMobile ? ( + <> + + + + + ) : ( - + <> + + + + {account.user ? ( + <> + + + + ) : ( + + )} + )} + + + {({ TransitionProps }) => ( + + + + + + + 首页} /> + + + + 关于} /> + + + {account.user ? ( + + 控制台 + + ) : ( + + 登录 + + )} + + + + + + )} + ); }; diff --git a/web/berry/src/layout/MinimalLayout/index.js b/web/berry/src/layout/MinimalLayout/index.js index c2919c6d..81047fd1 100644 --- a/web/berry/src/layout/MinimalLayout/index.js +++ b/web/berry/src/layout/MinimalLayout/index.js @@ -1,6 +1,6 @@ import { Outlet } from 'react-router-dom'; import { useTheme } from '@mui/material/styles'; -import { AppBar, Box, CssBaseline, Toolbar } from '@mui/material'; +import { AppBar, Box, CssBaseline, Toolbar, Container } from '@mui/material'; import Header from './Header'; import Footer from 'ui-component/Footer'; @@ -22,9 +22,11 @@ const MinimalLayout = () => { flex: 'none' }} > - -
- + + +
+ + diff --git a/web/berry/src/routes/OtherRoutes.js b/web/berry/src/routes/OtherRoutes.js index 085c4add..58c0b660 100644 --- a/web/berry/src/routes/OtherRoutes.js +++ b/web/berry/src/routes/OtherRoutes.js @@ -8,6 +8,7 @@ import MinimalLayout from 'layout/MinimalLayout'; const AuthLogin = Loadable(lazy(() => import('views/Authentication/Auth/Login'))); const AuthRegister = Loadable(lazy(() => import('views/Authentication/Auth/Register'))); const GitHubOAuth = Loadable(lazy(() => import('views/Authentication/Auth/GitHubOAuth'))); +const LarkOAuth = Loadable(lazy(() => import('views/Authentication/Auth/LarkOAuth'))); const ForgetPassword = Loadable(lazy(() => import('views/Authentication/Auth/ForgetPassword'))); const ResetPassword = Loadable(lazy(() => import('views/Authentication/Auth/ResetPassword'))); const Home = Loadable(lazy(() => import('views/Home'))); @@ -48,6 +49,10 @@ const OtherRoutes = { path: '/oauth/github', element: }, + { + path: '/oauth/lark', + element: + }, { path: '/404', element: diff --git a/web/berry/src/store/actions.js b/web/berry/src/store/actions.js index 221e8578..f1592d17 100644 --- a/web/berry/src/store/actions.js +++ b/web/berry/src/store/actions.js @@ -7,3 +7,4 @@ export const SET_BORDER_RADIUS = '@customization/SET_BORDER_RADIUS'; export const SET_SITE_INFO = '@siteInfo/SET_SITE_INFO'; export const LOGIN = '@account/LOGIN'; export const LOGOUT = '@account/LOGOUT'; +export const SET_THEME = '@customization/SET_THEME'; diff --git a/web/berry/src/store/customizationReducer.js b/web/berry/src/store/customizationReducer.js index bd8e5f00..0c104025 100644 --- a/web/berry/src/store/customizationReducer.js +++ b/web/berry/src/store/customizationReducer.js @@ -9,7 +9,8 @@ export const initialState = { defaultId: 'default', fontFamily: config.fontFamily, borderRadius: config.borderRadius, - opened: true + opened: true, + theme: 'light' }; // ==============================|| CUSTOMIZATION REDUCER ||============================== // @@ -38,6 +39,11 @@ const customizationReducer = (state = initialState, action) => { ...state, borderRadius: action.borderRadius }; + case actionTypes.SET_THEME: + return { + ...state, + theme: action.theme + }; default: return state; } diff --git a/web/berry/src/themes/compStyleOverride.js b/web/berry/src/themes/compStyleOverride.js index b6e87e01..67a3dd14 100644 --- a/web/berry/src/themes/compStyleOverride.js +++ b/web/berry/src/themes/compStyleOverride.js @@ -1,5 +1,5 @@ export default function componentStyleOverrides(theme) { - const bgColor = theme.colors?.grey50; + const bgColor = theme.mode === 'dark' ? theme.backgroundDefault : theme.colors?.grey50; return { MuiButton: { styleOverrides: { @@ -12,15 +12,7 @@ export default function componentStyleOverrides(theme) { } } }, - MuiMenuItem: { - styleOverrides: { - root: { - '&:hover': { - backgroundColor: theme.colors?.grey100 - } - } - } - }, //MuiAutocomplete-popper MuiPopover-root + //MuiAutocomplete-popper MuiPopover-root MuiAutocomplete: { styleOverrides: { popper: { @@ -226,12 +218,12 @@ export default function componentStyleOverrides(theme) { MuiTableCell: { styleOverrides: { root: { - borderBottom: '1px solid rgb(241, 243, 244)', + borderBottom: '1px solid ' + theme.tableBorderBottom, textAlign: 'center' }, head: { color: theme.darkTextSecondary, - backgroundColor: 'rgb(244, 246, 248)' + backgroundColor: theme.headBackgroundColor } } }, @@ -239,7 +231,7 @@ export default function componentStyleOverrides(theme) { styleOverrides: { root: { '&:hover': { - backgroundColor: 'rgb(244, 246, 248)' + backgroundColor: theme.headBackgroundColor } } } @@ -247,10 +239,29 @@ export default function componentStyleOverrides(theme) { MuiTooltip: { styleOverrides: { tooltip: { - color: theme.paper, + color: theme.colors.paper, background: theme.colors?.grey700 } } + }, + MuiCssBaseline: { + styleOverrides: ` + .apexcharts-title-text { + fill: ${theme.textDark} !important + } + .apexcharts-text { + fill: ${theme.textDark} !important + } + .apexcharts-legend-text { + color: ${theme.textDark} !important + } + .apexcharts-menu { + background: ${theme.backgroundDefault} !important + } + .apexcharts-gridline, .apexcharts-xaxistooltip-background, .apexcharts-yaxistooltip-background { + stroke: ${theme.divider} !important; + } + ` } }; } diff --git a/web/berry/src/themes/index.js b/web/berry/src/themes/index.js index 6e694aa6..addd61f7 100644 --- a/web/berry/src/themes/index.js +++ b/web/berry/src/themes/index.js @@ -15,19 +15,10 @@ import themeTypography from './typography'; export const theme = (customization) => { const color = colors; - + const options = customization.theme === 'light' ? GetLightOption() : GetDarkOption(); const themeOption = { colors: color, - heading: color.grey900, - paper: color.paper, - backgroundDefault: color.paper, - background: color.primaryLight, - darkTextPrimary: color.grey700, - darkTextSecondary: color.grey500, - textDark: color.grey900, - menuSelected: color.secondaryDark, - menuSelectedBack: color.secondaryLight, - divider: color.grey200, + ...options, customization }; @@ -53,3 +44,49 @@ export const theme = (customization) => { }; export default theme; + +function GetDarkOption() { + const color = colors; + return { + mode: 'dark', + heading: color.darkTextTitle, + paper: color.darkLevel2, + backgroundDefault: color.darkPaper, + background: color.darkBackground, + darkTextPrimary: color.darkTextPrimary, + darkTextSecondary: color.darkTextSecondary, + textDark: color.darkTextTitle, + menuSelected: color.darkSecondaryMain, + menuSelectedBack: color.darkSelectedBack, + divider: color.darkDivider, + borderColor: color.darkBorderColor, + menuButton: color.darkLevel1, + menuButtonColor: color.darkSecondaryMain, + menuChip: color.darkLevel1, + headBackgroundColor: color.darkBackground, + tableBorderBottom: color.darkDivider + }; +} + +function GetLightOption() { + const color = colors; + return { + mode: 'light', + heading: color.grey900, + paper: color.paper, + backgroundDefault: color.paper, + background: color.primaryLight, + darkTextPrimary: color.grey700, + darkTextSecondary: color.grey500, + textDark: color.grey900, + menuSelected: color.secondaryDark, + menuSelectedBack: color.secondaryLight, + divider: color.grey200, + borderColor: color.grey300, + menuButton: color.secondaryLight, + menuButtonColor: color.secondaryDark, + menuChip: color.primaryLight, + headBackgroundColor: color.tableBackground, + tableBorderBottom: color.tableBorderBottom + }; +} diff --git a/web/berry/src/themes/palette.js b/web/berry/src/themes/palette.js index 09768555..70c78782 100644 --- a/web/berry/src/themes/palette.js +++ b/web/berry/src/themes/palette.js @@ -5,7 +5,7 @@ export default function themePalette(theme) { return { - mode: 'light', + mode: theme.mode, common: { black: theme.colors?.darkPaper }, diff --git a/web/berry/src/themes/typography.js b/web/berry/src/themes/typography.js index 24bfabb9..f20d87a5 100644 --- a/web/berry/src/themes/typography.js +++ b/web/berry/src/themes/typography.js @@ -132,6 +132,19 @@ export default function themeTypography(theme) { width: '44px', height: '44px', fontSize: '1.5rem' + }, + menuButton: { + color: theme.menuButtonColor, + background: theme.menuButton + }, + menuChip: { + background: theme.menuChip + }, + CardWrapper: { + backgroundColor: theme.mode === 'dark' ? theme.colors.darkLevel2 : theme.colors.primaryDark + }, + SubCard: { + border: theme.mode === 'dark' ? '1px solid rgba(227, 232, 239, 0.2)' : '1px solid rgb(227, 232, 239)' } }; } diff --git a/web/berry/src/ui-component/Logo.js b/web/berry/src/ui-component/Logo.js index a34fe895..52e61f4f 100644 --- a/web/berry/src/ui-component/Logo.js +++ b/web/berry/src/ui-component/Logo.js @@ -1,6 +1,8 @@ // material-ui -import logo from 'assets/images/logo.svg'; +import logoLight from 'assets/images/logo.svg'; +import logoDark from 'assets/images/logo-white.svg'; import { useSelector } from 'react-redux'; +import { useTheme } from '@mui/material/styles'; /** * if you want to use image instead of uncomment following. @@ -14,6 +16,8 @@ import { useSelector } from 'react-redux'; const Logo = () => { const siteInfo = useSelector((state) => state.siteInfo); + const theme = useTheme(); + const logo = theme.palette.mode === 'light' ? logoLight : logoDark; return {siteInfo.system_name}; }; diff --git a/web/berry/src/ui-component/ThemeButton.js b/web/berry/src/ui-component/ThemeButton.js new file mode 100644 index 00000000..c907c646 --- /dev/null +++ b/web/berry/src/ui-component/ThemeButton.js @@ -0,0 +1,50 @@ +import { useDispatch, useSelector } from 'react-redux'; +import { SET_THEME } from 'store/actions'; +import { useTheme } from '@mui/material/styles'; +import { Avatar, Box, ButtonBase } from '@mui/material'; +import { IconSun, IconMoon } from '@tabler/icons-react'; + +export default function ThemeButton() { + const dispatch = useDispatch(); + + const defaultTheme = useSelector((state) => state.customization.theme); + + const theme = useTheme(); + + return ( + + + { + let theme = defaultTheme === 'light' ? 'dark' : 'light'; + dispatch({ type: SET_THEME, theme: theme }); + localStorage.setItem('theme', theme); + }} + color="inherit" + > + {defaultTheme === 'light' ? : } + + + + ); +} diff --git a/web/berry/src/ui-component/cards/MainCard.js b/web/berry/src/ui-component/cards/MainCard.js index 8735282c..32353027 100644 --- a/web/berry/src/ui-component/cards/MainCard.js +++ b/web/berry/src/ui-component/cards/MainCard.js @@ -15,7 +15,7 @@ const headerSX = { const MainCard = forwardRef( ( { - border = true, + border = false, boxShadow, children, content = true, diff --git a/web/berry/src/ui-component/cards/SubCard.js b/web/berry/src/ui-component/cards/SubCard.js index 05f9abb7..a63819a8 100644 --- a/web/berry/src/ui-component/cards/SubCard.js +++ b/web/berry/src/ui-component/cards/SubCard.js @@ -15,8 +15,7 @@ const SubCard = forwardRef( )} @@ -62,7 +61,8 @@ SubCard.propTypes = { secondary: PropTypes.oneOfType([PropTypes.node, PropTypes.string, PropTypes.object]), sx: PropTypes.object, contentSX: PropTypes.object, - title: PropTypes.oneOfType([PropTypes.node, PropTypes.string, PropTypes.object]) + title: PropTypes.oneOfType([PropTypes.node, PropTypes.string, PropTypes.object]), + subTitle: PropTypes.oneOfType([PropTypes.node, PropTypes.string, PropTypes.object]) }; SubCard.defaultProps = { diff --git a/web/berry/src/utils/chart.js b/web/berry/src/utils/chart.js index 4633fe37..8cf6d847 100644 --- a/web/berry/src/utils/chart.js +++ b/web/berry/src/utils/chart.js @@ -40,7 +40,8 @@ export function generateChartOptions(data, unit) { chart: { sparkline: { enabled: true - } + }, + background: 'transparent' }, dataLabels: { enabled: false diff --git a/web/berry/src/utils/common.js b/web/berry/src/utils/common.js index d8dabac3..947df3bf 100644 --- a/web/berry/src/utils/common.js +++ b/web/berry/src/utils/common.js @@ -91,6 +91,13 @@ export async function onGitHubOAuthClicked(github_client_id, openInNewTab = fals } } +export async function onLarkOAuthClicked(lark_client_id) { + const state = await getOAuthState(); + if (!state) return; + let redirect_uri = `${window.location.origin}/oauth/lark`; + window.open(`https://open.feishu.cn/open-apis/authen/v1/index?redirect_uri=${redirect_uri}&app_id=${lark_client_id}&state=${state}`); +} + export function isAdmin() { let user = localStorage.getItem('user'); if (!user) return false; diff --git a/web/berry/src/views/Authentication/Auth/LarkOAuth.js b/web/berry/src/views/Authentication/Auth/LarkOAuth.js new file mode 100644 index 00000000..88ced5d8 --- /dev/null +++ b/web/berry/src/views/Authentication/Auth/LarkOAuth.js @@ -0,0 +1,94 @@ +import { Link, useNavigate, useSearchParams } from 'react-router-dom'; +import React, { useEffect, useState } from 'react'; +import { showError } from 'utils/common'; +import useLogin from 'hooks/useLogin'; + +// material-ui +import { useTheme } from '@mui/material/styles'; +import { Grid, Stack, Typography, useMediaQuery, CircularProgress } from '@mui/material'; + +// project imports +import AuthWrapper from '../AuthWrapper'; +import AuthCardWrapper from '../AuthCardWrapper'; +import Logo from 'ui-component/Logo'; + +// assets + +// ================================|| AUTH3 - LOGIN ||================================ // + +const LarkOAuth = () => { + const theme = useTheme(); + const matchDownSM = useMediaQuery(theme.breakpoints.down('md')); + + const [searchParams] = useSearchParams(); + const [prompt, setPrompt] = useState('处理中...'); + const { larkLogin } = useLogin(); + + let navigate = useNavigate(); + + const sendCode = async (code, state, count) => { + const { success, message } = await larkLogin(code, state); + if (!success) { + if (message) { + showError(message); + } + if (count === 0) { + setPrompt(`操作失败,重定向至登录界面中...`); + await new Promise((resolve) => setTimeout(resolve, 2000)); + navigate('/login'); + return; + } + count++; + setPrompt(`出现错误,第 ${count} 次重试中...`); + await new Promise((resolve) => setTimeout(resolve, 2000)); + await sendCode(code, state, count); + } + }; + + useEffect(() => { + let code = searchParams.get('code'); + let state = searchParams.get('state'); + sendCode(code, state, 0).then(); + }, []); + + return ( + + + + + + + + + + + + + + + + + + 飞书 登录 + + + + + + + + + {prompt} + + + + + + + + + + ); +}; + +export default LarkOAuth; diff --git a/web/berry/src/views/Authentication/AuthForms/AuthLogin.js b/web/berry/src/views/Authentication/AuthForms/AuthLogin.js index 9420b098..bc7a35c0 100644 --- a/web/berry/src/views/Authentication/AuthForms/AuthLogin.js +++ b/web/berry/src/views/Authentication/AuthForms/AuthLogin.js @@ -35,7 +35,8 @@ import VisibilityOff from '@mui/icons-material/VisibilityOff'; import Github from 'assets/images/icons/github.svg'; import Wechat from 'assets/images/icons/wechat.svg'; -import { onGitHubOAuthClicked } from 'utils/common'; +import Lark from 'assets/images/icons/lark.svg'; +import { onGitHubOAuthClicked, onLarkOAuthClicked } from 'utils/common'; // ============================|| FIREBASE - LOGIN ||============================ // @@ -49,7 +50,7 @@ const LoginForm = ({ ...others }) => { // const [checked, setChecked] = useState(true); let tripartiteLogin = false; - if (siteInfo.github_oauth || siteInfo.wechat_login) { + if (siteInfo.github_oauth || siteInfo.wechat_login || siteInfo.lark_client_id) { tripartiteLogin = true; } @@ -121,6 +122,29 @@ const LoginForm = ({ ...others }) => { )} + {siteInfo.lark_client_id && ( + + + + + + )} ({ - backgroundColor: theme.palette.primary.light + backgroundColor: theme.palette.background.default })); // eslint-disable-next-line diff --git a/web/berry/src/views/Channel/component/EditModal.js b/web/berry/src/views/Channel/component/EditModal.js index cbf411b9..03b4df57 100644 --- a/web/berry/src/views/Channel/component/EditModal.js +++ b/web/berry/src/views/Channel/component/EditModal.js @@ -21,15 +21,16 @@ import { Container, Autocomplete, FormHelperText, - Checkbox + Switch, + Checkbox, } from "@mui/material"; import { Formik } from "formik"; import * as Yup from "yup"; import { defaultConfig, typeConfig } from "../type/Config"; //typeConfig import { createFilterOptions } from "@mui/material/Autocomplete"; -import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank'; -import CheckBoxIcon from '@mui/icons-material/CheckBox'; +import CheckBoxOutlineBlankIcon from "@mui/icons-material/CheckBoxOutlineBlank"; +import CheckBoxIcon from "@mui/icons-material/CheckBox"; const icon = ; const checkedIcon = ; @@ -79,6 +80,7 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => { const [inputPrompt, setInputPrompt] = useState(defaultConfig.prompt); const [groupOptions, setGroupOptions] = useState([]); const [modelOptions, setModelOptions] = useState([]); + const [batchAdd, setBatchAdd] = useState(false); const initChannel = (typeValue) => { if (typeConfig[typeValue]?.inputLabel) { @@ -151,7 +153,7 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => { try { let res = await API.get(`/api/channel/models`); const { data } = res.data; - data.forEach(item => { + data.forEach((item) => { if (!item.owned_by) { item.owned_by = "未知"; } @@ -166,7 +168,7 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => { }); setModelOptions( - data.map((model) => { + data.map((model) => { return { id: model.id, group: model.owned_by, @@ -258,7 +260,7 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => { 2 ); } - data.base_url = data.base_url ?? ''; + data.base_url = data.base_url ?? ""; data.is_edit = true; initChannel(data.type); setInitialInput(data); @@ -273,6 +275,7 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => { }, []); useEffect(() => { + setBatchAdd(false); if (channelId) { loadChannel().then(); } else { @@ -340,15 +343,17 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => { }, }} > - {Object.values(CHANNEL_OPTIONS).sort((a, b) => { - return a.text.localeCompare(b.text) - }).map((option) => { - return ( - - {option.text} - - ); - })} + {Object.values(CHANNEL_OPTIONS) + .sort((a, b) => { + return a.text.localeCompare(b.text); + }) + .map((option) => { + return ( + + {option.text} + + ); + })} {touched.type && errors.type ? ( @@ -553,7 +558,12 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => { }} renderOption={(props, option, { selected }) => (
  • - + {option.id}
  • )} @@ -599,20 +609,38 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => { error={Boolean(touched.key && errors.key)} sx={{ ...theme.typography.otherInput }} > - - {inputLabel.key} - - + {!batchAdd ? ( + <> + + {inputLabel.key} + + + + ) : ( + + )} + {touched.key && errors.key ? ( {errors.key} @@ -624,6 +652,19 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => { )} + {channelId === 0 && ( + + setBatchAdd(e.target.checked)} + /> + 批量添加 + + )} { - if (priorityValve === "" || priorityValve === item.priority) { + const handlePriority = async (event) => { + const currentValue = parseInt(event.target.value); + if (isNaN(currentValue) || currentValue === priorityValve) { return; } - await manageChannel(item.id, "priority", priorityValve); + + if (currentValue < 0) { + showError("优先级不能小于 0"); + return; + } + + await manageChannel(item.id, "priority", currentValue); + setPriority(currentValue); }; const handleResponseTime = async () => { @@ -170,9 +170,7 @@ export default function ChannelTableRow({ handle_action={handleResponseTime} /> - - {renderNumber(item.used_quota)} - + {renderNumber(item.used_quota)} - - 优先级 - setPriority(e.target.value)} - sx={{ textAlign: "center" }} - endAdornment={ - - - - - - } - /> - + diff --git a/web/berry/src/views/Dashboard/component/StatisticalLineChartCard.js b/web/berry/src/views/Dashboard/component/StatisticalLineChartCard.js index 9daa9519..e6b46e25 100644 --- a/web/berry/src/views/Dashboard/component/StatisticalLineChartCard.js +++ b/web/berry/src/views/Dashboard/component/StatisticalLineChartCard.js @@ -12,7 +12,7 @@ import MainCard from 'ui-component/cards/MainCard'; import SkeletonTotalOrderCard from 'ui-component/cards/Skeleton/EarningCard'; const CardWrapper = styled(MainCard)(({ theme }) => ({ - backgroundColor: theme.palette.primary.dark, + ...theme.typography.CardWrapper, color: '#fff', overflow: 'hidden', position: 'relative', diff --git a/web/berry/src/views/Profile/index.js b/web/berry/src/views/Profile/index.js index e0683228..483e3141 100644 --- a/web/berry/src/views/Profile/index.js +++ b/web/berry/src/views/Profile/index.js @@ -12,7 +12,8 @@ import { DialogTitle, DialogContent, DialogActions, - Divider + Divider, + SvgIcon } from '@mui/material'; import Grid from '@mui/material/Unstable_Grid2'; import SubCard from 'ui-component/cards/SubCard'; @@ -20,12 +21,13 @@ import { IconBrandWechat, IconBrandGithub, IconMail } from '@tabler/icons-react' import Label from 'ui-component/Label'; import { API } from 'utils/api'; import { showError, showSuccess } from 'utils/common'; -import { onGitHubOAuthClicked } from 'utils/common'; +import { onGitHubOAuthClicked, onLarkOAuthClicked } from 'utils/common'; import * as Yup from 'yup'; import WechatModal from 'views/Authentication/AuthForms/WechatModal'; import { useSelector } from 'react-redux'; import EmailModal from './component/EmailModal'; import Turnstile from 'react-turnstile'; +import { ReactComponent as Lark } from 'assets/images/icons/lark.svg'; const validationSchema = Yup.object().shape({ username: Yup.string().required('用户名 不能为空').min(3, '用户名 不能小于 3 个字符'), @@ -137,6 +139,9 @@ export default function Profile() { + @@ -205,6 +210,13 @@ export default function Profile() { )} + {status.lark_client_id && !inputs.lark_id && ( + + + + )} + +
    + { + + 用以推送报警信息, + + 点击此处 + + 了解 Message Pusher + + } + > + + + + Message Pusher 推送地址 + + + + + + Message Pusher 访问凭证 + + + + + + + + ; +const checkedIcon = ; +const filter = createFilterOptions(); const validationSchema = Yup.object().shape({ is_edit: Yup.boolean(), - name: Yup.string().required("名称 不能为空"), - remain_quota: Yup.number().min(0, "必须大于等于0"), + name: Yup.string().required('名称 不能为空'), + remain_quota: Yup.number().min(0, '必须大于等于0'), expired_time: Yup.number(), - unlimited_quota: Yup.boolean(), + unlimited_quota: Yup.boolean() }); const originInputs = { is_edit: false, - name: "", + name: '', remain_quota: 0, expired_time: -1, unlimited_quota: false, + subnet: '', + models: [] }; const EditModal = ({ open, tokenId, onCancel, onOk }) => { const theme = useTheme(); const [inputs, setInputs] = useState(originInputs); + const [modelOptions, setModelOptions] = useState([]); const submit = async (values, { setErrors, setStatus, setSubmitting }) => { setSubmitting(true); values.remain_quota = parseInt(values.remain_quota); let res; + let models = values.models.join(','); if (values.is_edit) { - res = await API.put(`/api/token/`, { ...values, id: parseInt(tokenId) }); + res = await API.put(`/api/token/`, { ...values, id: parseInt(tokenId), models: models }); } else { - res = await API.post(`/api/token/`, values); + res = await API.post(`/api/token/`, { ...values, models: models }); } const { success, message } = res.data; if (success) { if (values.is_edit) { - showSuccess("令牌更新成功!"); + showSuccess('令牌更新成功!'); } else { - showSuccess("令牌创建成功,请在列表页面点击复制获取令牌!"); + showSuccess('令牌创建成功,请在列表页面点击复制获取令牌!'); } setSubmitting(false); setStatus({ success: true }); @@ -78,61 +91,55 @@ const EditModal = ({ open, tokenId, onCancel, onOk }) => { const { success, message, data } = res.data; if (success) { data.is_edit = true; + if (data.models === '') { + data.models = []; + } else { + data.models = data.models.split(','); + } setInputs(data); } else { showError(message); } }; + const loadAvailableModels = async () => { + let res = await API.get(`/api/user/available_models`); + const { success, message, data } = res.data; + if (success) { + setModelOptions(data); + } else { + showError(message); + } + }; useEffect(() => { if (tokenId) { loadToken().then(); } else { - setInputs({...originInputs}); + setInputs({ ...originInputs }); } + loadAvailableModels().then(); }, [tokenId]); return ( - + - {tokenId ? "编辑令牌" : "新建令牌"} + {tokenId ? '编辑令牌' : '新建令牌'} - - 注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。 - - - {({ - errors, - handleBlur, - handleChange, - handleSubmit, - touched, - values, - setFieldError, - setFieldValue, - isSubmitting, - }) => ( + 注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。 + + {({ errors, handleBlur, handleChange, handleSubmit, touched, values, setFieldError, setFieldValue, isSubmitting }) => ( - + 名称 { name="name" onBlur={handleBlur} onChange={handleChange} - inputProps={{ autoComplete: "name" }} + inputProps={{ autoComplete: 'name' }} aria-describedby="helper-text-channel-name-label" /> {touched.name && errors.name && ( @@ -151,42 +158,99 @@ const EditModal = ({ open, tokenId, onCancel, onOk }) => { )} + + { + const event = { + target: { + name: 'models', + value: value + } + }; + handleChange(event); + }} + onBlur={handleBlur} + // filterSelectedOptions + disableCloseOnSelect + renderInput={(params) => } + filterOptions={(options, params) => { + const filtered = filter(options, params); + const { inputValue } = params; + const isExisting = options.some((option) => inputValue === option); + if (inputValue !== '' && !isExisting) { + filtered.push(inputValue); + } + return filtered; + }} + renderOption={(props, option, { selected }) => ( +
  • + + {option} +
  • + )} + /> + {errors.models ? ( + + {errors.models} + + ) : ( + 请选择允许使用的模型,留空则不进行限制 + )} +
    + + IP 限制 + + {touched.subnet && errors.subnet ? ( + + {errors.subnet} + + ) : ( + + 请输入允许访问的网段,例如:192.168.0.0/24,请使用英文逗号分隔多个网段 + + )} + {values.expired_time !== -1 && ( - - + + { if (newError === null) { - setFieldError("expired_time", null); + setFieldError('expired_time', null); } else { - setFieldError("expired_time", "无效的日期"); + setFieldError('expired_time', '无效的日期'); } }} onChange={(newValue) => { - setFieldValue("expired_time", newValue.unix()); + setFieldValue('expired_time', newValue.unix()); }} slotProps={{ actionBar: { - actions: ["today", "accept"], - }, + actions: ['today', 'accept'] + } }} /> {errors.expired_time && ( - + {errors.expired_time} )} @@ -196,35 +260,22 @@ const EditModal = ({ open, tokenId, onCancel, onOk }) => { checked={values.expired_time === -1} onClick={() => { if (values.expired_time === -1) { - setFieldValue( - "expired_time", - Math.floor(Date.now() / 1000) - ); + setFieldValue('expired_time', Math.floor(Date.now() / 1000)); } else { - setFieldValue("expired_time", -1); + setFieldValue('expired_time', -1); } }} - />{" "} + />{' '} 永不过期 - - - 额度 - + + 额度 - {renderQuotaWithPrompt(values.remain_quota)} - - } + endAdornment={{renderQuotaWithPrompt(values.remain_quota)}} onBlur={handleBlur} onChange={handleChange} aria-describedby="helper-text-channel-remain_quota-label" @@ -232,10 +283,7 @@ const EditModal = ({ open, tokenId, onCancel, onOk }) => { /> {touched.remain_quota && errors.remain_quota && ( - + {errors.remain_quota} )} @@ -243,19 +291,13 @@ const EditModal = ({ open, tokenId, onCancel, onOk }) => { { - setFieldValue("unlimited_quota", !values.unlimited_quota); + setFieldValue('unlimited_quota', !values.unlimited_quota); }} - />{" "} + />{' '} 无限额度 - @@ -273,5 +315,5 @@ EditModal.propTypes = { open: PropTypes.bool, tokenId: PropTypes.number, onCancel: PropTypes.func, - onOk: PropTypes.func, + onOk: PropTypes.func }; From 52c32c0b4a4eacf595463ca699ad85fb2c4e07f9 Mon Sep 17 00:00:00 2001 From: GAI Group <133845290+AI-ASS@users.noreply.github.com> Date: Sat, 6 Apr 2024 20:08:05 +0800 Subject: [PATCH 074/121] chore: resolve the issue of onclick event scope for custom Lark button (#1281) chore: Resolve the issue of onclick event scope for custom Lark button --- web/default/src/components/LoginForm.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/default/src/components/LoginForm.js b/web/default/src/components/LoginForm.js index 1623b549..71566ef8 100644 --- a/web/default/src/components/LoginForm.js +++ b/web/default/src/components/LoginForm.js @@ -157,7 +157,9 @@ const LoginForm = () => { borderRadius: "10em", display: "flex", cursor: "pointer" - }}> + }} + onClick={() => onLarkOAuthClicked(status.lark_client_id)} + > Date: Sat, 6 Apr 2024 20:42:35 +0800 Subject: [PATCH 075/121] fix: only check model when request path in whitelist --- middleware/auth.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/middleware/auth.go b/middleware/auth.go index 01b2cce3..64ce6608 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -116,7 +116,7 @@ func TokenAuth() func(c *gin.Context) { return } requestModel, err := getRequestModel(c) - if err != nil && !strings.HasPrefix(c.Request.URL.Path, "/v1/models") { + if err != nil && shouldCheckModel(c) { abortWithMessage(c, http.StatusBadRequest, err.Error()) return } @@ -142,3 +142,19 @@ func TokenAuth() func(c *gin.Context) { c.Next() } } + +func shouldCheckModel(c *gin.Context) bool { + if strings.HasPrefix(c.Request.URL.Path, "/v1/completions") { + return true + } + if strings.HasPrefix(c.Request.URL.Path, "/v1/chat/completions") { + return true + } + if strings.HasPrefix(c.Request.URL.Path, "/v1/images") { + return true + } + if strings.HasPrefix(c.Request.URL.Path, "/v1/audio") { + return true + } + return false +} From e086da05b1b2193e5a5ddfc257df04a94206c91a Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 6 Apr 2024 20:48:22 +0800 Subject: [PATCH 076/121] feat: able to change gemini version (close #1211) --- common/config/config.go | 2 ++ relay/adaptor/gemini/adaptor.go | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/common/config/config.go b/common/config/config.go index 9fd7cba0..4d54f4e5 100644 --- a/common/config/config.go +++ b/common/config/config.go @@ -141,3 +141,5 @@ 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") + +var GeminiVersion = env.String("GEMINI_VERSION", "v1") diff --git a/relay/adaptor/gemini/adaptor.go b/relay/adaptor/gemini/adaptor.go index 45124752..6a2867e4 100644 --- a/relay/adaptor/gemini/adaptor.go +++ b/relay/adaptor/gemini/adaptor.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "github.com/gin-gonic/gin" + "github.com/songquanpeng/one-api/common/config" "github.com/songquanpeng/one-api/common/helper" channelhelper "github.com/songquanpeng/one-api/relay/adaptor" "github.com/songquanpeng/one-api/relay/adaptor/openai" @@ -21,7 +22,7 @@ func (a *Adaptor) Init(meta *meta.Meta) { } func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { - version := helper.AssignOrDefault(meta.APIVersion, "v1") + version := helper.AssignOrDefault(meta.APIVersion, config.GeminiVersion) action := "generateContent" if meta.IsStream { action = "streamGenerateContent" From af543ab8ecb6827cbbc151c2cff181cdc3286274 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 6 Apr 2024 20:50:43 +0800 Subject: [PATCH 077/121] docs: update readme --- README.md | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index d5c939be..d42c6b49 100644 --- a/README.md +++ b/README.md @@ -363,28 +363,29 @@ graph LR 9. `CHANNEL_UPDATE_FREQUENCY`:设置之后将定期更新渠道余额,单位为分钟,未设置则不进行更新。 + 例子:`CHANNEL_UPDATE_FREQUENCY=1440` 10. `CHANNEL_TEST_FREQUENCY`:设置之后将定期检查渠道,单位为分钟,未设置则不进行检查。 - + 例子:`CHANNEL_TEST_FREQUENCY=1440` -11. `POLLING_INTERVAL`:批量更新渠道余额以及测试可用性时的请求间隔,单位为秒,默认无间隔。 +11. 例子:`CHANNEL_TEST_FREQUENCY=1440` +12. `POLLING_INTERVAL`:批量更新渠道余额以及测试可用性时的请求间隔,单位为秒,默认无间隔。 + 例子:`POLLING_INTERVAL=5` -12. `BATCH_UPDATE_ENABLED`:启用数据库批量更新聚合,会导致用户额度的更新存在一定的延迟可选值为 `true` 和 `false`,未设置则默认为 `false`。 +13. `BATCH_UPDATE_ENABLED`:启用数据库批量更新聚合,会导致用户额度的更新存在一定的延迟可选值为 `true` 和 `false`,未设置则默认为 `false`。 + 例子:`BATCH_UPDATE_ENABLED=true` + 如果你遇到了数据库连接数过多的问题,可以尝试启用该选项。 -13. `BATCH_UPDATE_INTERVAL=5`:批量更新聚合的时间间隔,单位为秒,默认为 `5`。 +14. `BATCH_UPDATE_INTERVAL=5`:批量更新聚合的时间间隔,单位为秒,默认为 `5`。 + 例子:`BATCH_UPDATE_INTERVAL=5` -14. 请求频率限制: +15. 请求频率限制: + `GLOBAL_API_RATE_LIMIT`:全局 API 速率限制(除中继请求外),单 ip 三分钟内的最大请求数,默认为 `180`。 + `GLOBAL_WEB_RATE_LIMIT`:全局 Web 速率限制,单 ip 三分钟内的最大请求数,默认为 `60`。 -15. 编码器缓存设置: +16. 编码器缓存设置: + `TIKTOKEN_CACHE_DIR`:默认程序启动时会联网下载一些通用的词元的编码,如:`gpt-3.5-turbo`,在一些网络环境不稳定,或者离线情况,可能会导致启动有问题,可以配置此目录缓存数据,可迁移到离线环境。 + `DATA_GYM_CACHE_DIR`:目前该配置作用与 `TIKTOKEN_CACHE_DIR` 一致,但是优先级没有它高。 -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 用户令牌。 +17. `RELAY_TIMEOUT`:中继超时设置,单位为秒,默认不设置超时时间。 +18. `SQLITE_BUSY_TIMEOUT`:SQLite 锁等待超时设置,单位为毫秒,默认 `3000`。 +19. `GEMINI_SAFETY_SETTING`:Gemini 的安全设置,默认 `BLOCK_NONE`。 +20. `GEMINI_VERSION`:One API 所使用的 Gemini 版本,默认为 `v1`。 +21. `THEME`:系统的主题设置,默认为 `default`,具体可选值参考[此处](./web/README.md)。 +22. `ENABLE_METRIC`:是否根据请求成功率禁用渠道,默认不开启,可选值为 `true` 和 `false`。 +23. `METRIC_QUEUE_SIZE`:请求成功率统计队列大小,默认为 `10`。 +24. `METRIC_SUCCESS_RATE_THRESHOLD`:请求成功率阈值,默认为 `0.8`。 +25. `INITIAL_ROOT_TOKEN`:如果设置了该值,则在系统首次启动时会自动创建一个值为该环境变量值的 root 用户令牌。 ### 命令行参数 1. `--port `: 指定服务器监听的端口号,默认为 `3000`。 From a10232f43a435f7105a9efc49af23525ad94e118 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 13 Apr 2024 11:39:31 +0800 Subject: [PATCH 078/121] feat: add gpt-4-turbo support (close #1304) --- relay/adaptor/openai/constants.go | 2 +- relay/billing/ratio/model.go | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/relay/adaptor/openai/constants.go b/relay/adaptor/openai/constants.go index ea236ea1..2ffff007 100644 --- a/relay/adaptor/openai/constants.go +++ b/relay/adaptor/openai/constants.go @@ -6,7 +6,7 @@ var ModelList = []string{ "gpt-3.5-turbo-instruct", "gpt-4", "gpt-4-0314", "gpt-4-0613", "gpt-4-1106-preview", "gpt-4-0125-preview", "gpt-4-32k", "gpt-4-32k-0314", "gpt-4-32k-0613", - "gpt-4-turbo-preview", + "gpt-4-turbo-preview", "gpt-4-turbo", "gpt-4-turbo-2024-04-09", "gpt-4-vision-preview", "text-embedding-ada-002", "text-embedding-3-small", "text-embedding-3-large", "text-curie-001", "text-babbage-001", "text-ada-001", "text-davinci-002", "text-davinci-003", diff --git a/relay/billing/ratio/model.go b/relay/billing/ratio/model.go index 108924a1..24d7615d 100644 --- a/relay/billing/ratio/model.go +++ b/relay/billing/ratio/model.go @@ -29,6 +29,8 @@ var ModelRatio = map[string]float64{ "gpt-4-1106-preview": 5, // $0.01 / 1K tokens "gpt-4-0125-preview": 5, // $0.01 / 1K tokens "gpt-4-turbo-preview": 5, // $0.01 / 1K tokens + "gpt-4-turbo": 5, // $0.01 / 1K tokens + "gpt-4-turbo-2024-04-09": 5, // $0.01 / 1K tokens "gpt-4-vision-preview": 5, // $0.01 / 1K tokens "gpt-3.5-turbo": 0.25, // $0.0005 / 1K tokens "gpt-3.5-turbo-0301": 0.75, @@ -256,7 +258,7 @@ func GetCompletionRatio(name string) float64 { return 4.0 / 3.0 } if strings.HasPrefix(name, "gpt-4") { - if strings.HasSuffix(name, "preview") { + if strings.HasPrefix(name, "gpt-4-turbo") { return 3 } return 2 From 7bf61f916504097ac23efb765b938d8f94c342ce Mon Sep 17 00:00:00 2001 From: JustSong Date: Mon, 15 Apr 2024 23:09:12 +0800 Subject: [PATCH 079/121] fix: fix retry not working (close #1314) --- middleware/distributor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/middleware/distributor.go b/middleware/distributor.go index 6e0d2718..19b34ff2 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -39,7 +39,7 @@ func Distribute() func(c *gin.Context) { return } } else { - requestModel := c.GetString("request_model") + requestModel = c.GetString("request_model") var err error channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, requestModel, false) if err != nil { From 1a0b039bcf54e110d649b54b58d64871aa80ae8f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Apr 2024 21:52:51 +0800 Subject: [PATCH 080/121] chore(deps): bump golang.org/x/net from 0.17.0 to 0.23.0 (#1335) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.17.0 to 0.23.0. - [Commits](https://github.com/golang/net/compare/v0.17.0...v0.23.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 6ace51f2..789d3c84 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/pkoukk/tiktoken-go v0.1.5 github.com/smartystreets/goconvey v1.8.1 github.com/stretchr/testify v1.8.3 - golang.org/x/crypto v0.17.0 + golang.org/x/crypto v0.21.0 golang.org/x/image v0.14.0 gorm.io/driver/mysql v1.4.3 gorm.io/driver/postgres v1.5.2 @@ -62,9 +62,9 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect golang.org/x/arch v0.3.0 // indirect - golang.org/x/net v0.17.0 // indirect + golang.org/x/net v0.23.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 3ead2711..d29402d2 100644 --- a/go.sum +++ b/go.sum @@ -160,13 +160,13 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUu golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -176,8 +176,8 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= From fc9a7849502a00460d30828918f02481c677183f Mon Sep 17 00:00:00 2001 From: "Laisky.Cai" Date: Sat, 20 Apr 2024 00:40:47 +0800 Subject: [PATCH 081/121] feat: support aws bedrockruntime claude3 (#1328) * feat: support aws bedrockruntime claude3 closes #622, closes #749, closes #1300 * fix: convert to aws claude model id * fix: Update AWS adapter to handle stream completions and calculate usage metrics Based on the file summaries provided, here are the important bullet points for the commit message: - Add functionality to handle stream completion events from AWS in the relay/adaptor/aws/main.go file - Marshall AWS response to OpenAI format and calculate usage metrics in the same file - Implement a custom render function for streaming events in the same file - Improve error handling for JSON unmarshalling and marshalling errors in the same file * fix: Implement AWS handler with usage tracking and error handling - Implemented streaming response handling for AWS handler - Set response content type to text/event-stream - Added error handling for failed marshaling/unmarshaling - Updated return values to include `relaymodel.ErrorWithStatusCode` and `relaymodel.Usage` - Improved error handling and response formatting for AWS adaptor * fix: Refactor AWS Adapter for Improved Model Mapping and Error Handling * Refactor AWS adapter to improve model management - Replace hardcoded model list in `adapter.go` with a function to get models from `awsModelIDMap` - Update `GetModelList` function to return model list directly - Add `GetChannelName` function to get channel name from `Adaptor` object * Improve error handling and code organization in main.go - Replace switch statement with a map to map AWS model IDs to OpenAI model IDs - Return an error if the model is not found in the map - Use a single return statement instead of wrapping multiple return statements in the `awsModelID` function - Add a new error message for when the model is not found in the map in the `Handler` function * fix: bug fix * chore: change variable name & package * chore: change variable name * perf: update config related code --------- Co-authored-by: JustSong --- common/config/config.go | 7 +- common/config/key.go | 3 + common/ctxkey/key.go | 7 + controller/channel-test.go | 2 +- controller/relay.go | 5 +- go.mod | 86 ++++--- go.sum | 240 ++++++++---------- middleware/distributor.go | 3 +- relay/adaptor.go | 3 + relay/adaptor/anthropic/main.go | 8 +- relay/adaptor/aws/adapter.go | 74 ++++++ relay/adaptor/aws/main.go | 215 ++++++++++++++++ relay/adaptor/aws/model.go | 17 ++ relay/apitype/define.go | 1 + relay/channeltype/define.go | 1 + relay/channeltype/helper.go | 3 + relay/channeltype/url.go | 1 + relay/controller/image.go | 3 +- relay/controller/text.go | 17 +- web/default/package.json | 2 +- .../src/constants/channel.constants.js | 3 +- web/default/src/pages/Channel/EditChannel.js | 63 ++++- 22 files changed, 566 insertions(+), 198 deletions(-) create mode 100644 common/ctxkey/key.go create mode 100644 relay/adaptor/aws/adapter.go create mode 100644 relay/adaptor/aws/main.go create mode 100644 relay/adaptor/aws/model.go diff --git a/common/config/config.go b/common/config/config.go index 4d54f4e5..0864d844 100644 --- a/common/config/config.go +++ b/common/config/config.go @@ -4,6 +4,7 @@ import ( "github.com/songquanpeng/one-api/common/env" "os" "strconv" + "strings" "sync" "time" @@ -51,9 +52,9 @@ var EmailDomainWhitelist = []string{ "foxmail.com", } -var DebugEnabled = os.Getenv("DEBUG") == "true" -var DebugSQLEnabled = os.Getenv("DEBUG_SQL") == "true" -var MemoryCacheEnabled = os.Getenv("MEMORY_CACHE_ENABLED") == "true" +var DebugEnabled = strings.ToLower(os.Getenv("DEBUG")) == "true" +var DebugSQLEnabled = strings.ToLower(os.Getenv("DEBUG_SQL")) == "true" +var MemoryCacheEnabled = strings.ToLower(os.Getenv("MEMORY_CACHE_ENABLED")) == "true" var LogConsumeEnabled = true diff --git a/common/config/key.go b/common/config/key.go index 4b503c2d..d2bab7d2 100644 --- a/common/config/key.go +++ b/common/config/key.go @@ -6,4 +6,7 @@ const ( KeyAPIVersion = KeyPrefix + "api_version" KeyLibraryID = KeyPrefix + "library_id" KeyPlugin = KeyPrefix + "plugin" + KeySK = KeyPrefix + "sk" + KeyAK = KeyPrefix + "ak" + KeyRegion = KeyPrefix + "region" ) diff --git a/common/ctxkey/key.go b/common/ctxkey/key.go new file mode 100644 index 00000000..6f1002bd --- /dev/null +++ b/common/ctxkey/key.go @@ -0,0 +1,7 @@ +package ctxkey + +var ( + RequestModel = "request_model" + ConvertedRequest = "converted_request" + OriginalModel = "original_model" +) diff --git a/controller/channel-test.go b/controller/channel-test.go index ddbe0b4a..a2e7af3d 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -88,7 +88,7 @@ func testChannel(channel *model.Channel) (err error, openaiErr *relaymodel.Error if err != nil { return err, nil } - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { err := controller.RelayErrorHandler(resp) return fmt.Errorf("status code %d: %s", resp.StatusCode, err.Error.Message), &err.Error } diff --git a/controller/relay.go b/controller/relay.go index 56359a1c..49408d13 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -7,6 +7,7 @@ import ( "github.com/gin-gonic/gin" "github.com/songquanpeng/one-api/common" "github.com/songquanpeng/one-api/common/config" + "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/common/helper" "github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/middleware" @@ -54,7 +55,7 @@ func Relay(c *gin.Context) { lastFailedChannelId := channelId channelName := c.GetString("channel_name") group := c.GetString("group") - originalModel := c.GetString("original_model") + originalModel := c.GetString(ctxkey.OriginalModel) go processChannelRelayError(ctx, channelId, channelName, bizErr) requestId := c.GetString(logger.RequestIdKey) retryTimes := config.RetryTimes @@ -65,7 +66,7 @@ func Relay(c *gin.Context) { for i := retryTimes; i > 0; i-- { channel, err := dbmodel.CacheGetRandomSatisfiedChannel(group, originalModel, i != retryTimes) if err != nil { - logger.Errorf(ctx, "CacheGetRandomSatisfiedChannel failed: %w", err) + logger.Errorf(ctx, "CacheGetRandomSatisfiedChannel failed: %+v", err) break } logger.Infof(ctx, "using channel #%d to retry (remain times %d)", channel.Id, i) diff --git a/go.mod b/go.mod index 789d3c84..1754ea58 100644 --- a/go.mod +++ b/go.mod @@ -1,70 +1,84 @@ module github.com/songquanpeng/one-api // +heroku goVersion go1.18 -go 1.18 +go 1.20 require ( - github.com/gin-contrib/cors v1.4.0 - github.com/gin-contrib/gzip v0.0.6 - github.com/gin-contrib/sessions v0.0.5 - github.com/gin-contrib/static v0.0.1 + github.com/aws/aws-sdk-go-v2 v1.26.1 + github.com/aws/aws-sdk-go-v2/credentials v1.17.11 + github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.7.4 + github.com/gin-contrib/cors v1.7.1 + github.com/gin-contrib/gzip v1.0.0 + github.com/gin-contrib/sessions v1.0.0 + github.com/gin-contrib/static v1.1.1 github.com/gin-gonic/gin v1.9.1 - github.com/go-playground/validator/v10 v10.14.0 + github.com/go-playground/validator/v10 v10.19.0 github.com/go-redis/redis/v8 v8.11.5 github.com/golang-jwt/jwt v3.2.2+incompatible - github.com/google/uuid v1.3.0 - github.com/gorilla/websocket v1.5.0 - github.com/pkoukk/tiktoken-go v0.1.5 + github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.1 + github.com/jinzhu/copier v0.4.0 + github.com/pkg/errors v0.9.1 + github.com/pkoukk/tiktoken-go v0.1.6 github.com/smartystreets/goconvey v1.8.1 - github.com/stretchr/testify v1.8.3 - golang.org/x/crypto v0.21.0 - golang.org/x/image v0.14.0 - gorm.io/driver/mysql v1.4.3 - gorm.io/driver/postgres v1.5.2 - gorm.io/driver/sqlite v1.4.3 - gorm.io/gorm v1.25.0 + github.com/stretchr/testify v1.9.0 + golang.org/x/crypto v0.22.0 + golang.org/x/image v0.15.0 + gorm.io/driver/mysql v1.5.6 + gorm.io/driver/postgres v1.5.7 + gorm.io/driver/sqlite v1.5.5 + gorm.io/gorm v1.25.9 ) require ( - github.com/bytedance/sonic v1.9.1 // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect - github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect + github.com/aws/smithy-go v1.20.2 // indirect + github.com/bytedance/sonic v1.11.5 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudwego/base64x v0.1.3 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/dlclark/regexp2 v1.10.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-sql-driver/mysql v1.6.0 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect - github.com/gorilla/context v1.1.1 // indirect - github.com/gorilla/securecookie v1.1.1 // indirect - github.com/gorilla/sessions v1.2.1 // indirect + github.com/gorilla/context v1.1.2 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect + github.com/gorilla/sessions v1.2.2 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/pgx/v5 v5.5.4 // indirect + github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect + github.com/jackc/pgx/v5 v5.5.5 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/jtolds/gls v4.20.0+incompatible // indirect - github.com/klauspost/cpuid/v2 v2.2.4 // indirect - github.com/leodido/go-urn v1.2.4 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pelletier/go-toml/v2 v2.2.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/smarty/assertions v1.15.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.11 // indirect - golang.org/x/arch v0.3.0 // indirect - golang.org/x/net v0.23.0 // indirect - golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.18.0 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.7.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index d29402d2..b98b377a 100644 --- a/go.sum +++ b/go.sum @@ -1,136 +1,133 @@ -github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= -github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= -github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA= +github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 h1:x6xsQXGSmW6frevwDA+vi/wqhp1ct18mVXYN08/93to= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2/go.mod h1:lPprDr1e6cJdyYeGXnRaJoP4Md+cDBvi2eOj00BlGmg= +github.com/aws/aws-sdk-go-v2/credentials v1.17.11 h1:YuIB1dJNf1Re822rriUOTxopaHHvIq0l/pX3fwO+Tzs= +github.com/aws/aws-sdk-go-v2/credentials v1.17.11/go.mod h1:AQtFPsDH9bI2O+71anW6EKL+NcD7LG3dpKGMV4SShgo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc= +github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.7.4 h1:JgHnonzbnA3pbqj76wYsSZIZZQYBxkmMEjvL6GHy8XU= +github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.7.4/go.mod h1:nZspkhg+9p8iApLFoyAqfyuMP0F38acy2Hm3r5r95Cg= +github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= +github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/bytedance/sonic v1.11.5 h1:G00FYjjqll5iQ1PYXynbg/hyzqBqavH8Mo9/oTopd9k= +github.com/bytedance/sonic v1.11.5/go.mod h1:X2PC2giUdj/Cv2lliWFLk6c/DUQok5rViJSemeB0wDw= +github.com/bytedance/sonic/loader v0.1.0/go.mod h1:UmRT+IRTGKz/DAkzcEGzyVqQFJ7H9BqwBO3pm9H/+HY= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudwego/base64x v0.1.3 h1:b5J/l8xolB7dyDTTmhJP2oTs5LdrjyrUFuNxdfq5hAg= +github.com/cloudwego/base64x v0.1.3/go.mod h1:1+1K5BUHIQzyapgpF7LwvOGAEDicKtt1umPV+aN8pi8= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= -github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= -github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= -github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g= -github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs= -github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= -github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= -github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE= -github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/cors v1.7.1 h1:s9SIppU/rk8enVvkzwiC2VK3UZ/0NNGsWfUKvV55rqs= +github.com/gin-contrib/cors v1.7.1/go.mod h1:n/Zj7B4xyrgk/cX1WCX2dkzFfaNm/xJb6oIUk7WTtps= +github.com/gin-contrib/gzip v1.0.0 h1:UKN586Po/92IDX6ie5CWLgMI81obiIp5nSP85T3wlTk= +github.com/gin-contrib/gzip v1.0.0/go.mod h1:CtG7tQrPB3vIBo6Gat9FVUsis+1emjvQqd66ME5TdnE= +github.com/gin-contrib/sessions v1.0.0 h1:r5GLta4Oy5xo9rAwMHx8B4wLpeRGHMdz9NafzJAdP8Y= +github.com/gin-contrib/sessions v1.0.0/go.mod h1:DN0f4bvpqMQElDdi+gNGScrP2QEI04IErRyMFyorUOI= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-contrib/static v0.0.1 h1:JVxuvHPuUfkoul12N7dtQw7KRn/pSMq7Ue1Va9Swm1U= -github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTIbD8TvWl7Hs= -github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= -github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/gin-contrib/static v1.1.1 h1:XEvBd4DDLG1HBlyPBQU1XO8NlTpw6mgdqcPteetYA5k= +github.com/gin-contrib/static v1.1.1/go.mod h1:yRGmar7+JYvbMLRPIi4H5TVVSBwULfT9vetnVD0IO74= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= -github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= -github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= -github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4= +github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= -github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= -github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= -github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= -github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= +github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= +github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8= -github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= -github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= -github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= -github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= -github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= -github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= -github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkoukk/tiktoken-go v0.1.5 h1:hAlT4dCf6Uk50x8E7HQrddhH3EWMKUN+LArExQQsQx4= -github.com/pkoukk/tiktoken-go v0.1.5/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= +github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg= +github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw= +github.com/pkoukk/tiktoken-go v0.1.6/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= @@ -138,81 +135,54 @@ github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= -golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= -golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= +golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= +golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/mysql v1.4.3 h1:/JhWJhO2v17d8hjApTltKNADm7K7YI2ogkR7avJUL3k= -gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c= -gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0= -gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8= -gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= -gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= -gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= -gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= -gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU= -gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= +gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM= +gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= +gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E= +gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8= +gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/middleware/distributor.go b/middleware/distributor.go index 19b34ff2..81075e51 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/gin-gonic/gin" "github.com/songquanpeng/one-api/common/config" + "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/model" "github.com/songquanpeng/one-api/relay/channeltype" @@ -62,7 +63,7 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode c.Set("channel_id", channel.Id) c.Set("channel_name", channel.Name) c.Set("model_mapping", channel.GetModelMapping()) - c.Set("original_model", modelName) // for retry + c.Set(ctxkey.OriginalModel, modelName) // for retry c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key)) c.Set("base_url", channel.GetBaseURL()) // this is for backward compatibility diff --git a/relay/adaptor.go b/relay/adaptor.go index c90bd708..2ba38bb3 100644 --- a/relay/adaptor.go +++ b/relay/adaptor.go @@ -5,6 +5,7 @@ import ( "github.com/songquanpeng/one-api/relay/adaptor/aiproxy" "github.com/songquanpeng/one-api/relay/adaptor/ali" "github.com/songquanpeng/one-api/relay/adaptor/anthropic" + "github.com/songquanpeng/one-api/relay/adaptor/aws" "github.com/songquanpeng/one-api/relay/adaptor/baidu" "github.com/songquanpeng/one-api/relay/adaptor/gemini" "github.com/songquanpeng/one-api/relay/adaptor/ollama" @@ -24,6 +25,8 @@ func GetAdaptor(apiType int) adaptor.Adaptor { return &ali.Adaptor{} case apitype.Anthropic: return &anthropic.Adaptor{} + case apitype.AwsClaude: + return &aws.Adaptor{} case apitype.Baidu: return &baidu.Adaptor{} case apitype.Gemini: diff --git a/relay/adaptor/anthropic/main.go b/relay/adaptor/anthropic/main.go index 6bb82d01..aa9e754f 100644 --- a/relay/adaptor/anthropic/main.go +++ b/relay/adaptor/anthropic/main.go @@ -91,7 +91,7 @@ func ConvertRequest(textRequest model.GeneralOpenAIRequest) *Request { } // https://docs.anthropic.com/claude/reference/messages-streaming -func streamResponseClaude2OpenAI(claudeResponse *StreamResponse) (*openai.ChatCompletionsStreamResponse, *Response) { +func StreamResponseClaude2OpenAI(claudeResponse *StreamResponse) (*openai.ChatCompletionsStreamResponse, *Response) { var response *Response var responseText string var stopReason string @@ -129,7 +129,7 @@ func streamResponseClaude2OpenAI(claudeResponse *StreamResponse) (*openai.ChatCo return &openaiResponse, response } -func responseClaude2OpenAI(claudeResponse *Response) *openai.TextResponse { +func ResponseClaude2OpenAI(claudeResponse *Response) *openai.TextResponse { var responseText string if len(claudeResponse.Content) > 0 { responseText = claudeResponse.Content[0].Text @@ -199,7 +199,7 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC logger.SysError("error unmarshalling stream response: " + err.Error()) return true } - response, meta := streamResponseClaude2OpenAI(&claudeResponse) + response, meta := StreamResponseClaude2OpenAI(&claudeResponse) if meta != nil { usage.PromptTokens += meta.Usage.InputTokens usage.CompletionTokens += meta.Usage.OutputTokens @@ -254,7 +254,7 @@ func Handler(c *gin.Context, resp *http.Response, promptTokens int, modelName st StatusCode: resp.StatusCode, }, nil } - fullTextResponse := responseClaude2OpenAI(&claudeResponse) + fullTextResponse := ResponseClaude2OpenAI(&claudeResponse) fullTextResponse.Model = modelName usage := model.Usage{ PromptTokens: claudeResponse.Usage.InputTokens, diff --git a/relay/adaptor/aws/adapter.go b/relay/adaptor/aws/adapter.go new file mode 100644 index 00000000..7f064efe --- /dev/null +++ b/relay/adaptor/aws/adapter.go @@ -0,0 +1,74 @@ +package aws + +import ( + "github.com/songquanpeng/one-api/common/ctxkey" + "io" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/pkg/errors" + "github.com/songquanpeng/one-api/relay/adaptor" + "github.com/songquanpeng/one-api/relay/adaptor/anthropic" + "github.com/songquanpeng/one-api/relay/meta" + "github.com/songquanpeng/one-api/relay/model" +) + +var _ adaptor.Adaptor = new(Adaptor) + +type Adaptor struct { +} + +func (a *Adaptor) Init(meta *meta.Meta) { + +} + +func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { + return "", nil +} + +func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { + return nil +} + +func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) { + if request == nil { + return nil, errors.New("request is nil") + } + + claudeReq := anthropic.ConvertRequest(*request) + c.Set(ctxkey.RequestModel, request.Model) + c.Set(ctxkey.ConvertedRequest, claudeReq) + return claudeReq, nil +} + +func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { + if request == nil { + return nil, errors.New("request is nil") + } + return request, nil +} + +func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { + return nil, nil +} + +func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { + if meta.IsStream { + err, usage = StreamHandler(c, resp) + } else { + err, usage = Handler(c, resp, meta.PromptTokens, meta.ActualModelName) + } + return +} + +func (a *Adaptor) GetModelList() (models []string) { + for n := range awsModelIDMap { + models = append(models, n) + } + + return +} + +func (a *Adaptor) GetChannelName() string { + return "aws" +} diff --git a/relay/adaptor/aws/main.go b/relay/adaptor/aws/main.go new file mode 100644 index 00000000..7473a549 --- /dev/null +++ b/relay/adaptor/aws/main.go @@ -0,0 +1,215 @@ +// Package aws provides the AWS adaptor for the relay service. +package aws + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/songquanpeng/one-api/common/config" + "github.com/songquanpeng/one-api/common/ctxkey" + "io" + "net/http" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/bedrockruntime" + "github.com/aws/aws-sdk-go-v2/service/bedrockruntime/types" + "github.com/gin-gonic/gin" + "github.com/jinzhu/copier" + "github.com/pkg/errors" + "github.com/songquanpeng/one-api/common" + "github.com/songquanpeng/one-api/common/helper" + "github.com/songquanpeng/one-api/common/logger" + "github.com/songquanpeng/one-api/relay/adaptor/anthropic" + relaymodel "github.com/songquanpeng/one-api/relay/model" +) + +func newAwsClient(c *gin.Context) (*bedrockruntime.Client, error) { + ak := c.GetString(config.KeyAK) + sk := c.GetString(config.KeySK) + region := c.GetString(config.KeyRegion) + client := bedrockruntime.New(bedrockruntime.Options{ + Region: region, + Credentials: aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(ak, sk, "")), + }) + + return client, nil +} + +func wrapErr(err error) *relaymodel.ErrorWithStatusCode { + return &relaymodel.ErrorWithStatusCode{ + StatusCode: http.StatusInternalServerError, + Error: relaymodel.Error{ + Message: fmt.Sprintf("%s", err.Error()), + }, + } +} + +// https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html +var awsModelIDMap = map[string]string{ + "claude-instant-1.2": "anthropic.claude-instant-v1", + "claude-2.0": "anthropic.claude-v2", + "claude-2.1": "anthropic.claude-v2:1", + "claude-3-sonnet-20240229": "anthropic.claude-3-sonnet-20240229-v1:0", + "claude-3-opus-20240229": "anthropic.claude-3-opus-20240229-v1:0", + "claude-3-haiku-20240307": "anthropic.claude-3-haiku-20240307-v1:0", +} + +func awsModelID(requestModel string) (string, error) { + if awsModelID, ok := awsModelIDMap[requestModel]; ok { + return awsModelID, nil + } + + return "", errors.Errorf("model %s not found", requestModel) +} + +func Handler(c *gin.Context, resp *http.Response, promptTokens int, modelName string) (*relaymodel.ErrorWithStatusCode, *relaymodel.Usage) { + awsCli, err := newAwsClient(c) + if err != nil { + return wrapErr(errors.Wrap(err, "newAwsClient")), nil + } + + awsModelId, err := awsModelID(c.GetString(ctxkey.RequestModel)) + if err != nil { + return wrapErr(errors.Wrap(err, "awsModelID")), nil + } + + awsReq := &bedrockruntime.InvokeModelInput{ + ModelId: aws.String(awsModelId), + Accept: aws.String("application/json"), + ContentType: aws.String("application/json"), + } + + claudeReq_, ok := c.Get(ctxkey.ConvertedRequest) + if !ok { + return wrapErr(errors.New("request not found")), nil + } + claudeReq := claudeReq_.(*anthropic.Request) + awsClaudeReq := &Request{ + AnthropicVersion: "bedrock-2023-05-31", + } + if err = copier.Copy(awsClaudeReq, claudeReq); err != nil { + return wrapErr(errors.Wrap(err, "copy request")), nil + } + + awsReq.Body, err = json.Marshal(awsClaudeReq) + if err != nil { + return wrapErr(errors.Wrap(err, "marshal request")), nil + } + + awsResp, err := awsCli.InvokeModel(c.Request.Context(), awsReq) + if err != nil { + return wrapErr(errors.Wrap(err, "InvokeModel")), nil + } + + claudeResponse := new(anthropic.Response) + err = json.Unmarshal(awsResp.Body, claudeResponse) + if err != nil { + return wrapErr(errors.Wrap(err, "unmarshal response")), nil + } + + openaiResp := anthropic.ResponseClaude2OpenAI(claudeResponse) + openaiResp.Model = modelName + usage := relaymodel.Usage{ + PromptTokens: claudeResponse.Usage.InputTokens, + CompletionTokens: claudeResponse.Usage.OutputTokens, + TotalTokens: claudeResponse.Usage.InputTokens + claudeResponse.Usage.OutputTokens, + } + openaiResp.Usage = usage + + c.JSON(http.StatusOK, openaiResp) + return nil, &usage +} + +func StreamHandler(c *gin.Context, resp *http.Response) (*relaymodel.ErrorWithStatusCode, *relaymodel.Usage) { + createdTime := helper.GetTimestamp() + awsCli, err := newAwsClient(c) + if err != nil { + return wrapErr(errors.Wrap(err, "newAwsClient")), nil + } + + awsModelId, err := awsModelID(c.GetString(ctxkey.RequestModel)) + if err != nil { + return wrapErr(errors.Wrap(err, "awsModelID")), nil + } + + awsReq := &bedrockruntime.InvokeModelWithResponseStreamInput{ + ModelId: aws.String(awsModelId), + Accept: aws.String("application/json"), + ContentType: aws.String("application/json"), + } + + claudeReq_, ok := c.Get(ctxkey.ConvertedRequest) + if !ok { + return wrapErr(errors.New("request not found")), nil + } + claudeReq := claudeReq_.(*anthropic.Request) + + awsClaudeReq := &Request{ + AnthropicVersion: "bedrock-2023-05-31", + } + if err = copier.Copy(awsClaudeReq, claudeReq); err != nil { + return wrapErr(errors.Wrap(err, "copy request")), nil + } + awsReq.Body, err = json.Marshal(awsClaudeReq) + if err != nil { + return wrapErr(errors.Wrap(err, "marshal request")), nil + } + + awsResp, err := awsCli.InvokeModelWithResponseStream(c.Request.Context(), awsReq) + if err != nil { + return wrapErr(errors.Wrap(err, "InvokeModelWithResponseStream")), nil + } + stream := awsResp.GetStream() + defer stream.Close() + + c.Writer.Header().Set("Content-Type", "text/event-stream") + var usage relaymodel.Usage + var id string + c.Stream(func(w io.Writer) bool { + event, ok := <-stream.Events() + if !ok { + c.Render(-1, common.CustomEvent{Data: "data: [DONE]"}) + return false + } + + switch v := event.(type) { + case *types.ResponseStreamMemberChunk: + claudeResp := new(anthropic.StreamResponse) + err := json.NewDecoder(bytes.NewReader(v.Value.Bytes)).Decode(claudeResp) + if err != nil { + logger.SysError("error unmarshalling stream response: " + err.Error()) + return false + } + + response, meta := anthropic.StreamResponseClaude2OpenAI(claudeResp) + if meta != nil { + usage.PromptTokens += meta.Usage.InputTokens + usage.CompletionTokens += meta.Usage.OutputTokens + id = fmt.Sprintf("chatcmpl-%s", meta.Id) + return true + } + if response == nil { + return true + } + response.Id = id + response.Model = c.GetString(ctxkey.OriginalModel) + response.Created = createdTime + jsonStr, err := json.Marshal(response) + if err != nil { + logger.SysError("error marshalling stream response: " + err.Error()) + return true + } + c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonStr)}) + return true + case *types.UnknownUnionMember: + fmt.Println("unknown tag:", v.Tag) + return false + default: + fmt.Println("union is nil or unknown type") + return false + } + }) + + return nil, &usage +} diff --git a/relay/adaptor/aws/model.go b/relay/adaptor/aws/model.go new file mode 100644 index 00000000..bcbfb584 --- /dev/null +++ b/relay/adaptor/aws/model.go @@ -0,0 +1,17 @@ +package aws + +import "github.com/songquanpeng/one-api/relay/adaptor/anthropic" + +// Request is the request to AWS Claude +// +// https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html +type Request struct { + // AnthropicVersion should be "bedrock-2023-05-31" + AnthropicVersion string `json:"anthropic_version"` + Messages []anthropic.Message `json:"messages"` + MaxTokens int `json:"max_tokens,omitempty"` + Temperature float64 `json:"temperature,omitempty"` + TopP float64 `json:"top_p,omitempty"` + TopK int `json:"top_k,omitempty"` + StopSequences []string `json:"stop_sequences,omitempty"` +} diff --git a/relay/apitype/define.go b/relay/apitype/define.go index 82d32a50..3760ba00 100644 --- a/relay/apitype/define.go +++ b/relay/apitype/define.go @@ -12,6 +12,7 @@ const ( Tencent Gemini Ollama + AwsClaude Dummy // this one is only for count, do not add any channel after this ) diff --git a/relay/channeltype/define.go b/relay/channeltype/define.go index 80027a80..faa0d443 100644 --- a/relay/channeltype/define.go +++ b/relay/channeltype/define.go @@ -34,6 +34,7 @@ const ( Ollama LingYiWanWu StepFun + AwsClaude Dummy ) diff --git a/relay/channeltype/helper.go b/relay/channeltype/helper.go index 01c2918c..89e40142 100644 --- a/relay/channeltype/helper.go +++ b/relay/channeltype/helper.go @@ -25,6 +25,9 @@ func ToAPIType(channelType int) int { apiType = apitype.Gemini case Ollama: apiType = apitype.Ollama + case AwsClaude: + apiType = apitype.AwsClaude } + return apiType } diff --git a/relay/channeltype/url.go b/relay/channeltype/url.go index eec59116..9ac29f30 100644 --- a/relay/channeltype/url.go +++ b/relay/channeltype/url.go @@ -34,6 +34,7 @@ var ChannelBaseURLs = []string{ "http://localhost:11434", // 30 "https://api.lingyiwanwu.com", // 31 "https://api.stepfun.com", // 32 + "", // 33 } func init() { diff --git a/relay/controller/image.go b/relay/controller/image.go index 80769845..b878e8f1 100644 --- a/relay/controller/image.go +++ b/relay/controller/image.go @@ -106,9 +106,10 @@ func RelayImageHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus } defer func(ctx context.Context) { - if resp.StatusCode != http.StatusOK { + if resp != nil && resp.StatusCode != http.StatusOK { return } + err := model.PostConsumeTokenQuota(meta.TokenId, quota) if err != nil { logger.SysError("error consuming token remain quota: " + err.Error()) diff --git a/relay/controller/text.go b/relay/controller/text.go index 0332a23f..0e018651 100644 --- a/relay/controller/text.go +++ b/relay/controller/text.go @@ -4,6 +4,9 @@ import ( "bytes" "encoding/json" "fmt" + "io" + "net/http" + "github.com/gin-gonic/gin" "github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/relay" @@ -14,9 +17,6 @@ import ( "github.com/songquanpeng/one-api/relay/channeltype" "github.com/songquanpeng/one-api/relay/meta" "github.com/songquanpeng/one-api/relay/model" - "io" - "net/http" - "strings" ) func RelayTextHelper(c *gin.Context) *model.ErrorWithStatusCode { @@ -86,12 +86,13 @@ func RelayTextHelper(c *gin.Context) *model.ErrorWithStatusCode { logger.Errorf(ctx, "DoRequest failed: %s", err.Error()) return openai.ErrorWrapper(err, "do_request_failed", http.StatusInternalServerError) } - errorHappened := (resp.StatusCode != http.StatusOK) || (meta.IsStream && resp.Header.Get("Content-Type") == "application/json") - if errorHappened { - billing.ReturnPreConsumedQuota(ctx, preConsumedQuota, meta.TokenId) - return RelayErrorHandler(resp) + if resp != nil { + errorHappened := (resp.StatusCode != http.StatusOK) || (meta.IsStream && resp.Header.Get("Content-Type") == "application/json") + if errorHappened { + billing.ReturnPreConsumedQuota(ctx, preConsumedQuota, meta.TokenId) + return RelayErrorHandler(resp) + } } - meta.IsStream = meta.IsStream || strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream") // do response usage, respErr := adaptor.DoResponse(c, resp, meta) diff --git a/web/default/package.json b/web/default/package.json index 438f020c..ba45011f 100644 --- a/web/default/package.json +++ b/web/default/package.json @@ -18,7 +18,7 @@ }, "scripts": { "start": "react-scripts start", - "build": "react-scripts build && mv -f build ../build/default", + "build": "react-scripts build && rm -rf ../build/default && mv -f build ../build/default", "test": "react-scripts test", "eject": "react-scripts eject" }, diff --git a/web/default/src/constants/channel.constants.js b/web/default/src/constants/channel.constants.js index 7535b666..82fc7d44 100644 --- a/web/default/src/constants/channel.constants.js +++ b/web/default/src/constants/channel.constants.js @@ -1,6 +1,7 @@ export const CHANNEL_OPTIONS = [ { key: 1, text: 'OpenAI', value: 1, color: 'green' }, { key: 14, text: 'Anthropic Claude', value: 14, color: 'black' }, + { key: 33, text: 'AWS Claude', value: 33, color: 'black' }, { key: 3, text: 'Azure OpenAI', value: 3, color: 'olive' }, { key: 11, text: 'Google PaLM2', value: 11, color: 'orange' }, { key: 24, text: 'Google Gemini', value: 24, color: 'orange' }, @@ -31,4 +32,4 @@ export const CHANNEL_OPTIONS = [ { key: 9, text: '代理:AI.LS', value: 9, color: 'yellow' }, { key: 12, text: '代理:API2GPT', value: 12, color: 'blue' }, { key: 13, text: '代理:AIGC2D', value: 13, color: 'purple' } -]; \ No newline at end of file +]; diff --git a/web/default/src/pages/Channel/EditChannel.js b/web/default/src/pages/Channel/EditChannel.js index 203cd714..6ee9a02d 100644 --- a/web/default/src/pages/Channel/EditChannel.js +++ b/web/default/src/pages/Channel/EditChannel.js @@ -54,6 +54,11 @@ const EditChannel = () => { const [basicModels, setBasicModels] = useState([]); const [fullModels, setFullModels] = useState([]); const [customModel, setCustomModel] = useState(''); + const [config, setConfig] = useState({ + region: '', + sk: '', + ak: '' + }); const handleInputChange = (e, { name, value }) => { setInputs((inputs) => ({ ...inputs, [name]: value })); if (name === 'type') { @@ -65,6 +70,10 @@ const EditChannel = () => { } }; + const handleConfigChange = (e, { name, value }) => { + setConfig((inputs) => ({ ...inputs, [name]: value })); + }; + const loadChannel = async () => { let res = await API.get(`/api/channel/${channelId}`); const { success, message, data } = res.data; @@ -83,6 +92,7 @@ const EditChannel = () => { data.model_mapping = JSON.stringify(JSON.parse(data.model_mapping), null, 2); } setInputs(data); + setConfig(JSON.parse(data.config)); setBasicModels(getChannelModels(data.type)); } else { showError(message); @@ -144,6 +154,13 @@ const EditChannel = () => { }, []); const submit = async () => { + // some provider as AWS need both AK and SK rather than a single key, + // so we need to combine them into a single key to achieve the best compatibility. + if (inputs.ak && inputs.sk) { + console.log(`combine ak ${inputs.ak} and sk ${inputs.sk}`, inputs.ak, inputs.sk); + inputs.key = `${inputs.ak}\n${inputs.sk}`; + } + if (!isEdit && (inputs.name === '' || inputs.key === '')) { showInfo('请填写渠道名称和渠道密钥!'); return; @@ -169,6 +186,7 @@ const EditChannel = () => { let res; localInputs.models = localInputs.models.join(','); localInputs.group = localInputs.groups.join(','); + localInputs.config = JSON.stringify(config); if (isEdit) { res = await API.put(`/api/channel/`, { ...localInputs, id: parseInt(channelId) }); } else { @@ -345,7 +363,9 @@ const EditChannel = () => { fluid multiple search - onLabelClick={(e, { value }) => {copy(value).then()}} + onLabelClick={(e, { value }) => { + copy(value).then(); + }} selection onChange={handleInputChange} value={inputs.models} @@ -392,7 +412,40 @@ const EditChannel = () => { /> { - batch ? + inputs.type === 33 && ( + + + + + + ) + } + { + inputs.type !== 33 && (batch ? { value={inputs.key} autoComplete='new-password' /> - + ) } { - !isEdit && ( + inputs.type !== 33 && !isEdit && ( { ) } { - inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && ( + inputs.type !== 3 && inputs.type !== 33 && inputs.type !== 8 && inputs.type !== 22 && ( Date: Sat, 20 Apr 2024 00:43:07 +0800 Subject: [PATCH 082/121] docs: update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d42c6b49..c7473dc9 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 ## 功能 1. 支持多种大模型: + [x] [OpenAI ChatGPT 系列模型](https://platform.openai.com/docs/guides/gpt/chat-completions-api)(支持 [Azure OpenAI API](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference)) - + [x] [Anthropic Claude 系列模型](https://anthropic.com) + + [x] [Anthropic Claude 系列模型](https://anthropic.com) (支持 AWS Claude) + [x] [Google PaLM2/Gemini 系列模型](https://developers.generativeai.google) + [x] [Mistral 系列模型](https://mistral.ai/) + [x] [百度文心一言系列模型](https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html) From ddee58df36ca590279b17244feeccfb7043501d8 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 20 Apr 2024 00:54:34 +0800 Subject: [PATCH 083/121] fix: fix loading --- web/default/src/pages/Channel/EditChannel.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/default/src/pages/Channel/EditChannel.js b/web/default/src/pages/Channel/EditChannel.js index 6ee9a02d..edfa5a75 100644 --- a/web/default/src/pages/Channel/EditChannel.js +++ b/web/default/src/pages/Channel/EditChannel.js @@ -92,7 +92,9 @@ const EditChannel = () => { data.model_mapping = JSON.stringify(JSON.parse(data.model_mapping), null, 2); } setInputs(data); - setConfig(JSON.parse(data.config)); + if (data.config !== '') { + setConfig(JSON.parse(data.config)); + } setBasicModels(getChannelModels(data.type)); } else { showError(message); From f452bd481e8c8612278c42fd7c9041bb96faac92 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 20 Apr 2024 00:57:50 +0800 Subject: [PATCH 084/121] ci: update ci condition --- .github/workflows/docker-image-amd64-en.yml | 2 +- .github/workflows/docker-image-amd64.yml | 2 +- .github/workflows/docker-image-arm64.yml | 2 +- .github/workflows/linux-release.yml | 2 +- .github/workflows/macos-release.yml | 2 +- .github/workflows/windows-release.yml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-image-amd64-en.yml b/.github/workflows/docker-image-amd64-en.yml index af488256..31c01e80 100644 --- a/.github/workflows/docker-image-amd64-en.yml +++ b/.github/workflows/docker-image-amd64-en.yml @@ -3,7 +3,7 @@ name: Publish Docker image (amd64, English) on: push: tags: - - '*' + - 'v*.*.*' workflow_dispatch: inputs: name: diff --git a/.github/workflows/docker-image-amd64.yml b/.github/workflows/docker-image-amd64.yml index 2079d31f..1b9983c6 100644 --- a/.github/workflows/docker-image-amd64.yml +++ b/.github/workflows/docker-image-amd64.yml @@ -3,7 +3,7 @@ name: Publish Docker image (amd64) on: push: tags: - - '*' + - 'v*.*.*' workflow_dispatch: inputs: name: diff --git a/.github/workflows/docker-image-arm64.yml b/.github/workflows/docker-image-arm64.yml index 39d1a401..dc2b4b97 100644 --- a/.github/workflows/docker-image-arm64.yml +++ b/.github/workflows/docker-image-arm64.yml @@ -3,7 +3,7 @@ name: Publish Docker image (arm64) on: push: tags: - - '*' + - 'v*.*.*' - '!*-alpha*' workflow_dispatch: inputs: diff --git a/.github/workflows/linux-release.yml b/.github/workflows/linux-release.yml index 6f30a1d5..161c41e3 100644 --- a/.github/workflows/linux-release.yml +++ b/.github/workflows/linux-release.yml @@ -5,7 +5,7 @@ permissions: on: push: tags: - - '*' + - 'v*.*.*' - '!*-alpha*' workflow_dispatch: inputs: diff --git a/.github/workflows/macos-release.yml b/.github/workflows/macos-release.yml index 359c2c92..94b3e47b 100644 --- a/.github/workflows/macos-release.yml +++ b/.github/workflows/macos-release.yml @@ -5,7 +5,7 @@ permissions: on: push: tags: - - '*' + - 'v*.*.*' - '!*-alpha*' workflow_dispatch: inputs: diff --git a/.github/workflows/windows-release.yml b/.github/workflows/windows-release.yml index 4e99b75c..18641ae8 100644 --- a/.github/workflows/windows-release.yml +++ b/.github/workflows/windows-release.yml @@ -5,7 +5,7 @@ permissions: on: push: tags: - - '*' + - 'v*.*.*' - '!*-alpha*' workflow_dispatch: inputs: From 2369025842b828ac38f4427fd1ebab8d03b1fe7f Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 20 Apr 2024 01:15:33 +0800 Subject: [PATCH 085/121] fix: use prefix to match more json response --- relay/controller/text.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/relay/controller/text.go b/relay/controller/text.go index 0e018651..23e94234 100644 --- a/relay/controller/text.go +++ b/relay/controller/text.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/http" + "strings" "github.com/gin-gonic/gin" "github.com/songquanpeng/one-api/common/logger" @@ -87,7 +88,7 @@ func RelayTextHelper(c *gin.Context) *model.ErrorWithStatusCode { return openai.ErrorWrapper(err, "do_request_failed", http.StatusInternalServerError) } if resp != nil { - errorHappened := (resp.StatusCode != http.StatusOK) || (meta.IsStream && resp.Header.Get("Content-Type") == "application/json") + errorHappened := (resp.StatusCode != http.StatusOK) || (meta.IsStream && strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json")) if errorHappened { billing.ReturnPreConsumedQuota(ctx, preConsumedQuota, meta.TokenId) return RelayErrorHandler(resp) From 35ba1da98415fbed5aa7642ed2a6d95b600f38ef Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 21 Apr 2024 11:04:34 +0800 Subject: [PATCH 086/121] fix: fix cannot submit aws claude config (close #1343) --- web/default/src/pages/Channel/EditChannel.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/web/default/src/pages/Channel/EditChannel.js b/web/default/src/pages/Channel/EditChannel.js index edfa5a75..c59b64d6 100644 --- a/web/default/src/pages/Channel/EditChannel.js +++ b/web/default/src/pages/Channel/EditChannel.js @@ -156,13 +156,9 @@ const EditChannel = () => { }, []); const submit = async () => { - // some provider as AWS need both AK and SK rather than a single key, - // so we need to combine them into a single key to achieve the best compatibility. - if (inputs.ak && inputs.sk) { - console.log(`combine ak ${inputs.ak} and sk ${inputs.sk}`, inputs.ak, inputs.sk); - inputs.key = `${inputs.ak}\n${inputs.sk}`; + if (inputs.key === "") { + inputs.key = `${config.ak}|${config.sk}|${config.region}`; } - if (!isEdit && (inputs.name === '' || inputs.key === '')) { showInfo('请填写渠道名称和渠道密钥!'); return; From a873cbd3927259618179a8ea55cf439508540573 Mon Sep 17 00:00:00 2001 From: "Laisky.Cai" Date: Sun, 21 Apr 2024 14:35:51 +0800 Subject: [PATCH 087/121] fix: logger race (#1339) - Refactor logger using sync.Once to improve performance - Initiate log setup in a goroutine to prevent blocking - Integrate gin.DefaultErrorWriter and gin.DefaultWriter for logging - Introduce request ID generation for better request tracking - Simplify setup logic by removing redundant variables and code --- common/image/image.go | 2 +- common/logger/logger.go | 44 +++++++++++++++-------------------------- 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/common/image/image.go b/common/image/image.go index de8fefd3..12f0adff 100644 --- a/common/image/image.go +++ b/common/image/image.go @@ -16,7 +16,7 @@ import ( ) // Regex to match data URL pattern -var dataURLPattern = regexp.MustCompile(`data:image/([^;]+);base64,(.*)`) +var dataURLPattern = regexp.MustCompile(`data:image/([^;]+);base64,(.*)`) func IsImageUrl(url string) (bool, error) { resp, err := http.Head(url) diff --git a/common/logger/logger.go b/common/logger/logger.go index 957d8a11..858e33e2 100644 --- a/common/logger/logger.go +++ b/common/logger/logger.go @@ -3,15 +3,16 @@ package logger import ( "context" "fmt" - "github.com/gin-gonic/gin" - "github.com/songquanpeng/one-api/common/config" - "github.com/songquanpeng/one-api/common/helper" "io" "log" "os" "path/filepath" "sync" "time" + + "github.com/gin-gonic/gin" + "github.com/songquanpeng/one-api/common/config" + "github.com/songquanpeng/one-api/common/helper" ) const ( @@ -21,28 +22,20 @@ const ( loggerError = "ERR" ) -var setupLogLock sync.Mutex -var setupLogWorking bool +var setupLogOnce sync.Once func SetupLogger() { - if LogDir != "" { - ok := setupLogLock.TryLock() - if !ok { - log.Println("setup log is already working") - return + setupLogOnce.Do(func() { + if LogDir != "" { + logPath := filepath.Join(LogDir, fmt.Sprintf("oneapi-%s.log", time.Now().Format("20060102"))) + fd, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + log.Fatal("failed to open log file") + } + gin.DefaultWriter = io.MultiWriter(os.Stdout, fd) + gin.DefaultErrorWriter = io.MultiWriter(os.Stderr, fd) } - defer func() { - setupLogLock.Unlock() - setupLogWorking = false - }() - logPath := filepath.Join(LogDir, fmt.Sprintf("oneapi-%s.log", time.Now().Format("20060102"))) - fd, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - log.Fatal("failed to open log file") - } - gin.DefaultWriter = io.MultiWriter(os.Stdout, fd) - gin.DefaultErrorWriter = io.MultiWriter(os.Stderr, fd) - } + }) } func SysLog(s string) { @@ -100,12 +93,7 @@ func logHelper(ctx context.Context, level string, msg string) { } now := time.Now() _, _ = fmt.Fprintf(writer, "[%s] %v | %s | %s \n", level, now.Format("2006/01/02 - 15:04:05"), id, msg) - if !setupLogWorking { - setupLogWorking = true - go func() { - SetupLogger() - }() - } + SetupLogger() } func FatalLog(v ...any) { From 129282f4a9f9016815b62c767998cd5aec999140 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 21 Apr 2024 14:36:48 +0800 Subject: [PATCH 088/121] fix: fix wrong log type --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index a0621711..bdcdcd61 100644 --- a/main.go +++ b/main.go @@ -71,7 +71,7 @@ func main() { } if config.MemoryCacheEnabled { logger.SysLog("memory cache enabled") - logger.SysError(fmt.Sprintf("sync frequency: %d seconds", config.SyncFrequency)) + logger.SysLog(fmt.Sprintf("sync frequency: %d seconds", config.SyncFrequency)) model.InitChannelCache() } if config.MemoryCacheEnabled { From a2a00dfbc3c99573d97e21d64a1f896a7588c00f Mon Sep 17 00:00:00 2001 From: tylinux Date: Sun, 21 Apr 2024 14:53:03 +0800 Subject: [PATCH 089/121] feat: groq support Llama3 now (#1333) * feat: groq support Llama3 now * fix: update model ratio --------- Co-authored-by: JustSong --- relay/adaptor/groq/constants.go | 2 ++ relay/billing/ratio/model.go | 14 ++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/relay/adaptor/groq/constants.go b/relay/adaptor/groq/constants.go index fc9a9ebd..1aa2574b 100644 --- a/relay/adaptor/groq/constants.go +++ b/relay/adaptor/groq/constants.go @@ -7,4 +7,6 @@ var ModelList = []string{ "llama2-7b-2048", "llama2-70b-4096", "mixtral-8x7b-32768", + "llama3-8b-8192", + "llama3-70b-8192", } diff --git a/relay/billing/ratio/model.go b/relay/billing/ratio/model.go index 24d7615d..d86881cf 100644 --- a/relay/billing/ratio/model.go +++ b/relay/billing/ratio/model.go @@ -147,11 +147,13 @@ var ModelRatio = map[string]float64{ "mistral-medium-latest": 2.7 / 1000 * USD, "mistral-large-latest": 8.0 / 1000 * USD, "mistral-embed": 0.1 / 1000 * USD, - // https://wow.groq.com/ - "llama2-70b-4096": 0.7 / 1000 * USD, - "llama2-7b-2048": 0.1 / 1000 * USD, + // https://wow.groq.com/#:~:text=inquiries%C2%A0here.-,Model,-Current%20Speed + "llama3-70b-8192": 0.59 / 1000 * USD, "mixtral-8x7b-32768": 0.27 / 1000 * USD, + "llama3-8b-8192": 0.05 / 1000 * USD, "gemma-7b-it": 0.1 / 1000 * USD, + "llama2-70b-4096": 0.64 / 1000 * USD, + "llama2-7b-2048": 0.1 / 1000 * USD, // https://platform.lingyiwanwu.com/docs#-计费单元 "yi-34b-chat-0205": 2.5 / 1000 * RMB, "yi-34b-chat-200k": 12.0 / 1000 * RMB, @@ -277,7 +279,11 @@ func GetCompletionRatio(name string) float64 { } switch name { case "llama2-70b-4096": - return 0.8 / 0.7 + return 0.8 / 0.64 + case "llama3-8b-8192": + return 2 + case "llama3-70b-8192": + return 0.79 / 0.59 } return 1 } From 8572fac7a2c42b1d76e35b80b9b317131f10e9d3 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 21 Apr 2024 15:50:35 +0800 Subject: [PATCH 090/121] fix: add back chat dropdown item for chatgpt next web (close #1330) --- web/default/src/components/TokensTable.js | 1 + 1 file changed, 1 insertion(+) diff --git a/web/default/src/components/TokensTable.js b/web/default/src/components/TokensTable.js index 4f6c118e..40890f5d 100644 --- a/web/default/src/components/TokensTable.js +++ b/web/default/src/components/TokensTable.js @@ -13,6 +13,7 @@ const COPY_OPTIONS = [ ]; const OPEN_LINK_OPTIONS = [ + { key: 'next', text: 'ChatGPT Next Web', value: 'next' }, { key: 'ama', text: 'BotGem', value: 'ama' }, { key: 'opencat', text: 'OpenCat', value: 'opencat' }, ]; From b2679cca65ef03304c826c42bbad633c440ae2d4 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 21 Apr 2024 15:57:01 +0800 Subject: [PATCH 091/121] fix: fix preview completion ratio (close #1326) --- relay/billing/ratio/model.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/relay/billing/ratio/model.go b/relay/billing/ratio/model.go index d86881cf..b410df94 100644 --- a/relay/billing/ratio/model.go +++ b/relay/billing/ratio/model.go @@ -260,7 +260,7 @@ func GetCompletionRatio(name string) float64 { return 4.0 / 3.0 } if strings.HasPrefix(name, "gpt-4") { - if strings.HasPrefix(name, "gpt-4-turbo") { + if strings.HasPrefix(name, "gpt-4-turbo") || strings.HasSuffix(name, "preview") { return 3 } return 2 From 541182102eb89a4029e4b80dd77e76807f5203a9 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 21 Apr 2024 16:22:28 +0800 Subject: [PATCH 092/121] fix: ignore empty choice response for azure (close #1324) --- relay/adaptor/openai/main.go | 71 +++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/relay/adaptor/openai/main.go b/relay/adaptor/openai/main.go index 68d8f48f..72c675e1 100644 --- a/relay/adaptor/openai/main.go +++ b/relay/adaptor/openai/main.go @@ -15,6 +15,12 @@ import ( "strings" ) +const ( + dataPrefix = "data: " + done = "[DONE]" + dataPrefixLength = len(dataPrefix) +) + func StreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*model.ErrorWithStatusCode, string, *model.Usage) { responseText := "" scanner := bufio.NewScanner(resp.Body) @@ -36,39 +42,46 @@ func StreamHandler(c *gin.Context, resp *http.Response, relayMode int) (*model.E go func() { for scanner.Scan() { data := scanner.Text() - if len(data) < 6 { // ignore blank line or wrong format + if len(data) < dataPrefixLength { // ignore blank line or wrong format continue } - if data[:6] != "data: " && data[:6] != "[DONE]" { + if data[:dataPrefixLength] != dataPrefix && data[:dataPrefixLength] != done { continue } - dataChan <- data - data = data[6:] - if !strings.HasPrefix(data, "[DONE]") { - switch relayMode { - case relaymode.ChatCompletions: - var streamResponse ChatCompletionsStreamResponse - err := json.Unmarshal([]byte(data), &streamResponse) - if err != nil { - logger.SysError("error unmarshalling stream response: " + err.Error()) - continue // just ignore the error - } - for _, choice := range streamResponse.Choices { - responseText += conv.AsString(choice.Delta.Content) - } - if streamResponse.Usage != nil { - usage = streamResponse.Usage - } - case relaymode.Completions: - var streamResponse CompletionsStreamResponse - err := json.Unmarshal([]byte(data), &streamResponse) - if err != nil { - logger.SysError("error unmarshalling stream response: " + err.Error()) - continue - } - for _, choice := range streamResponse.Choices { - responseText += choice.Text - } + if strings.HasPrefix(data[dataPrefixLength:], done) { + dataChan <- data + continue + } + switch relayMode { + case relaymode.ChatCompletions: + var streamResponse ChatCompletionsStreamResponse + err := json.Unmarshal([]byte(data[dataPrefixLength:]), &streamResponse) + if err != nil { + logger.SysError("error unmarshalling stream response: " + err.Error()) + dataChan <- data // if error happened, pass the data to client + continue // just ignore the error + } + if len(streamResponse.Choices) == 0 { + // but for empty choice, we should not pass it to client, this is for azure + continue // just ignore empty choice + } + dataChan <- data + for _, choice := range streamResponse.Choices { + responseText += conv.AsString(choice.Delta.Content) + } + if streamResponse.Usage != nil { + usage = streamResponse.Usage + } + case relaymode.Completions: + dataChan <- data + var streamResponse CompletionsStreamResponse + err := json.Unmarshal([]byte(data[dataPrefixLength:]), &streamResponse) + if err != nil { + logger.SysError("error unmarshalling stream response: " + err.Error()) + continue + } + for _, choice := range streamResponse.Choices { + responseText += choice.Text } } } From d14e4aa01b3bd709cd1f2e4d5faeb757f03eedb1 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 21 Apr 2024 17:38:39 +0800 Subject: [PATCH 093/121] fix: key is wrongly updated --- web/default/src/pages/Channel/EditChannel.js | 21 +++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/web/default/src/pages/Channel/EditChannel.js b/web/default/src/pages/Channel/EditChannel.js index c59b64d6..b93c182c 100644 --- a/web/default/src/pages/Channel/EditChannel.js +++ b/web/default/src/pages/Channel/EditChannel.js @@ -57,7 +57,8 @@ const EditChannel = () => { const [config, setConfig] = useState({ region: '', sk: '', - ak: '' + ak: '', + user_id: '' }); const handleInputChange = (e, { name, value }) => { setInputs((inputs) => ({ ...inputs, [name]: value })); @@ -156,8 +157,10 @@ const EditChannel = () => { }, []); const submit = async () => { - if (inputs.key === "") { - inputs.key = `${config.ak}|${config.sk}|${config.region}`; + if (inputs.key === '') { + if (config.ak !== '' && config.sk !== '' && config.region !== '') { + inputs.key = `${config.ak}|${config.sk}|${config.region}`; + } } if (!isEdit && (inputs.name === '' || inputs.key === '')) { showInfo('请填写渠道名称和渠道密钥!'); @@ -442,6 +445,18 @@ const EditChannel = () => { ) } + { + inputs.type === 34 && ( + ) + } { inputs.type !== 33 && (batch ? Date: Sun, 21 Apr 2024 17:59:57 +0800 Subject: [PATCH 094/121] feat: support coze now --- README.md | 1 + common/config/key.go | 1 + controller/channel-test.go | 9 +- relay/adaptor.go | 3 + relay/adaptor/coze/adaptor.go | 75 ++++++ .../coze/constant/contenttype/define.go | 5 + relay/adaptor/coze/constant/event/define.go | 7 + .../coze/constant/messagetype/define.go | 6 + relay/adaptor/coze/constants.go | 3 + relay/adaptor/coze/helper.go | 10 + relay/adaptor/coze/main.go | 218 ++++++++++++++++++ relay/adaptor/coze/model.go | 38 +++ relay/apitype/define.go | 1 + relay/channeltype/define.go | 1 + relay/channeltype/helper.go | 2 + relay/channeltype/url.go | 1 + .../src/constants/channel.constants.js | 1 + 17 files changed, 380 insertions(+), 2 deletions(-) create mode 100644 relay/adaptor/coze/adaptor.go create mode 100644 relay/adaptor/coze/constant/contenttype/define.go create mode 100644 relay/adaptor/coze/constant/event/define.go create mode 100644 relay/adaptor/coze/constant/messagetype/define.go create mode 100644 relay/adaptor/coze/constants.go create mode 100644 relay/adaptor/coze/helper.go create mode 100644 relay/adaptor/coze/main.go create mode 100644 relay/adaptor/coze/model.go diff --git a/README.md b/README.md index c7473dc9..0ab35893 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 + [x] [Ollama](https://github.com/ollama/ollama) + [x] [零一万物](https://platform.lingyiwanwu.com/) + [x] [阶跃星辰](https://platform.stepfun.com/) + + [x] [Coze](https://www.coze.com/) 2. 支持配置镜像以及众多[第三方代理服务](https://iamazing.cn/page/openai-api-third-party-services)。 3. 支持通过**负载均衡**的方式访问多个渠道。 4. 支持 **stream 模式**,可以通过流式传输实现打字机效果。 diff --git a/common/config/key.go b/common/config/key.go index d2bab7d2..da182b37 100644 --- a/common/config/key.go +++ b/common/config/key.go @@ -9,4 +9,5 @@ const ( KeySK = KeyPrefix + "sk" KeyAK = KeyPrefix + "ak" KeyRegion = KeyPrefix + "region" + KeyUserID = KeyPrefix + "user_id" ) diff --git a/controller/channel-test.go b/controller/channel-test.go index a2e7af3d..e326d062 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -64,8 +64,12 @@ func testChannel(channel *model.Channel) (err error, openaiErr *relaymodel.Error return fmt.Errorf("invalid api type: %d, adaptor is nil", apiType), nil } adaptor.Init(meta) - modelName := adaptor.GetModelList()[0] - if !strings.Contains(channel.Models, modelName) { + var modelName string + modelList := adaptor.GetModelList() + if len(modelList) != 0 { + modelName = modelList[0] + } + if modelName == "" || !strings.Contains(channel.Models, modelName) { modelNames := strings.Split(channel.Models, ",") if len(modelNames) > 0 { modelName = modelNames[0] @@ -82,6 +86,7 @@ func testChannel(channel *model.Channel) (err error, openaiErr *relaymodel.Error if err != nil { return err, nil } + logger.SysLog(string(jsonData)) requestBody := bytes.NewBuffer(jsonData) c.Request.Body = io.NopCloser(requestBody) resp, err := adaptor.DoRequest(c, meta, requestBody) diff --git a/relay/adaptor.go b/relay/adaptor.go index 2ba38bb3..24db9e89 100644 --- a/relay/adaptor.go +++ b/relay/adaptor.go @@ -7,6 +7,7 @@ import ( "github.com/songquanpeng/one-api/relay/adaptor/anthropic" "github.com/songquanpeng/one-api/relay/adaptor/aws" "github.com/songquanpeng/one-api/relay/adaptor/baidu" + "github.com/songquanpeng/one-api/relay/adaptor/coze" "github.com/songquanpeng/one-api/relay/adaptor/gemini" "github.com/songquanpeng/one-api/relay/adaptor/ollama" "github.com/songquanpeng/one-api/relay/adaptor/openai" @@ -43,6 +44,8 @@ func GetAdaptor(apiType int) adaptor.Adaptor { return &zhipu.Adaptor{} case apitype.Ollama: return &ollama.Adaptor{} + case apitype.Coze: + return &coze.Adaptor{} } return nil } diff --git a/relay/adaptor/coze/adaptor.go b/relay/adaptor/coze/adaptor.go new file mode 100644 index 00000000..4af8c021 --- /dev/null +++ b/relay/adaptor/coze/adaptor.go @@ -0,0 +1,75 @@ +package coze + +import ( + "errors" + "fmt" + "github.com/gin-gonic/gin" + "github.com/songquanpeng/one-api/common/config" + "github.com/songquanpeng/one-api/relay/adaptor" + "github.com/songquanpeng/one-api/relay/adaptor/openai" + "github.com/songquanpeng/one-api/relay/meta" + "github.com/songquanpeng/one-api/relay/model" + "io" + "net/http" +) + +type Adaptor struct { +} + +func (a *Adaptor) Init(meta *meta.Meta) { + +} + +func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { + return fmt.Sprintf("%s/open_api/v2/chat", meta.BaseURL), nil +} + +func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { + adaptor.SetupCommonRequestHeader(c, req, meta) + req.Header.Set("Authorization", "Bearer "+meta.APIKey) + return nil +} + +func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) { + if request == nil { + return nil, errors.New("request is nil") + } + request.User = c.GetString(config.KeyUserID) + return ConvertRequest(*request), nil +} + +func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { + if request == nil { + return nil, errors.New("request is nil") + } + return request, nil +} + +func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { + return adaptor.DoRequestHelper(a, c, meta, requestBody) +} + +func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { + var responseText *string + if meta.IsStream { + err, responseText = StreamHandler(c, resp) + } else { + err, responseText = Handler(c, resp, meta.PromptTokens, meta.ActualModelName) + } + if responseText != nil { + usage = openai.ResponseText2Usage(*responseText, meta.ActualModelName, meta.PromptTokens) + } else { + usage = &model.Usage{} + } + usage.PromptTokens = meta.PromptTokens + usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens + return +} + +func (a *Adaptor) GetModelList() []string { + return ModelList +} + +func (a *Adaptor) GetChannelName() string { + return "coze" +} diff --git a/relay/adaptor/coze/constant/contenttype/define.go b/relay/adaptor/coze/constant/contenttype/define.go new file mode 100644 index 00000000..69c876bc --- /dev/null +++ b/relay/adaptor/coze/constant/contenttype/define.go @@ -0,0 +1,5 @@ +package contenttype + +const ( + Text = "text" +) diff --git a/relay/adaptor/coze/constant/event/define.go b/relay/adaptor/coze/constant/event/define.go new file mode 100644 index 00000000..c03e8c17 --- /dev/null +++ b/relay/adaptor/coze/constant/event/define.go @@ -0,0 +1,7 @@ +package event + +const ( + Message = "message" + Done = "done" + Error = "error" +) diff --git a/relay/adaptor/coze/constant/messagetype/define.go b/relay/adaptor/coze/constant/messagetype/define.go new file mode 100644 index 00000000..6c1c25db --- /dev/null +++ b/relay/adaptor/coze/constant/messagetype/define.go @@ -0,0 +1,6 @@ +package messagetype + +const ( + Answer = "answer" + FollowUp = "follow_up" +) diff --git a/relay/adaptor/coze/constants.go b/relay/adaptor/coze/constants.go new file mode 100644 index 00000000..d20fd875 --- /dev/null +++ b/relay/adaptor/coze/constants.go @@ -0,0 +1,3 @@ +package coze + +var ModelList = []string{} diff --git a/relay/adaptor/coze/helper.go b/relay/adaptor/coze/helper.go new file mode 100644 index 00000000..0396afcb --- /dev/null +++ b/relay/adaptor/coze/helper.go @@ -0,0 +1,10 @@ +package coze + +import "github.com/songquanpeng/one-api/relay/adaptor/coze/constant/event" + +func event2StopReason(e *string) string { + if e == nil || *e == event.Message { + return "" + } + return "stop" +} diff --git a/relay/adaptor/coze/main.go b/relay/adaptor/coze/main.go new file mode 100644 index 00000000..90ed6a70 --- /dev/null +++ b/relay/adaptor/coze/main.go @@ -0,0 +1,218 @@ +package coze + +import ( + "bufio" + "encoding/json" + "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/adaptor/coze/constant/messagetype" + "github.com/songquanpeng/one-api/relay/adaptor/openai" + "github.com/songquanpeng/one-api/relay/model" + "io" + "net/http" + "strings" +) + +// https://www.coze.com/open + +func stopReasonCoze2OpenAI(reason *string) string { + if reason == nil { + return "" + } + switch *reason { + case "end_turn": + return "stop" + case "stop_sequence": + return "stop" + case "max_tokens": + return "length" + default: + return *reason + } +} + +func ConvertRequest(textRequest model.GeneralOpenAIRequest) *Request { + cozeRequest := Request{ + Stream: textRequest.Stream, + User: textRequest.User, + BotId: textRequest.Model, + } + if cozeRequest.User == "" { + cozeRequest.User = "One API User" + } + for i, message := range textRequest.Messages { + if i == len(textRequest.Messages)-1 { + cozeRequest.Query = message.StringContent() + continue + } + cozeMessage := Message{ + Role: message.Role, + Content: message.StringContent(), + } + cozeRequest.ChatHistory = append(cozeRequest.ChatHistory, cozeMessage) + } + return &cozeRequest +} + +func StreamResponseCoze2OpenAI(cozeResponse *StreamResponse) (*openai.ChatCompletionsStreamResponse, *Response) { + var response *Response + var stopReason string + var choice openai.ChatCompletionsStreamResponseChoice + + if cozeResponse.Message != nil { + if cozeResponse.Message.Type != messagetype.Answer { + return nil, nil + } + choice.Delta.Content = cozeResponse.Message.Content + } + choice.Delta.Role = "assistant" + finishReason := stopReasonCoze2OpenAI(&stopReason) + if finishReason != "null" { + choice.FinishReason = &finishReason + } + var openaiResponse openai.ChatCompletionsStreamResponse + openaiResponse.Object = "chat.completion.chunk" + openaiResponse.Choices = []openai.ChatCompletionsStreamResponseChoice{choice} + openaiResponse.Id = cozeResponse.ConversationId + return &openaiResponse, response +} + +func ResponseCoze2OpenAI(cozeResponse *Response) *openai.TextResponse { + var responseText string + for _, message := range cozeResponse.Messages { + if message.Type == messagetype.Answer { + responseText = message.Content + break + } + } + choice := openai.TextResponseChoice{ + Index: 0, + Message: model.Message{ + Role: "assistant", + Content: responseText, + Name: nil, + }, + FinishReason: "stop", + } + fullTextResponse := openai.TextResponse{ + Id: fmt.Sprintf("chatcmpl-%s", cozeResponse.ConversationId), + Model: "coze-bot", + Object: "chat.completion", + Created: helper.GetTimestamp(), + Choices: []openai.TextResponseChoice{choice}, + } + return &fullTextResponse +} + +func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *string) { + var responseText string + createdTime := helper.GetTimestamp() + scanner := bufio.NewScanner(resp.Body) + scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + if i := strings.Index(string(data), "\n"); i >= 0 { + return i + 1, data[0:i], nil + } + if atEOF { + return len(data), data, nil + } + return 0, nil, nil + }) + dataChan := make(chan string) + stopChan := make(chan bool) + go func() { + for scanner.Scan() { + data := scanner.Text() + if len(data) < 5 { + continue + } + if !strings.HasPrefix(data, "data:") { + continue + } + data = strings.TrimPrefix(data, "data:") + dataChan <- data + } + stopChan <- true + }() + common.SetEventStreamHeaders(c) + var modelName string + c.Stream(func(w io.Writer) bool { + select { + case data := <-dataChan: + // some implementations may add \r at the end of data + data = strings.TrimSuffix(data, "\r") + var cozeResponse StreamResponse + err := json.Unmarshal([]byte(data), &cozeResponse) + if err != nil { + logger.SysError("error unmarshalling stream response: " + err.Error()) + return true + } + response, _ := StreamResponseCoze2OpenAI(&cozeResponse) + if response == nil { + return true + } + for _, choice := range response.Choices { + responseText += conv.AsString(choice.Delta.Content) + } + response.Model = modelName + response.Created = createdTime + jsonStr, err := json.Marshal(response) + if err != nil { + logger.SysError("error marshalling stream response: " + err.Error()) + return true + } + c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonStr)}) + return true + case <-stopChan: + c.Render(-1, common.CustomEvent{Data: "data: [DONE]"}) + return false + } + }) + _ = resp.Body.Close() + return nil, &responseText +} + +func Handler(c *gin.Context, resp *http.Response, promptTokens int, modelName string) (*model.ErrorWithStatusCode, *string) { + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return openai.ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil + } + err = resp.Body.Close() + if err != nil { + return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil + } + var cozeResponse Response + err = json.Unmarshal(responseBody, &cozeResponse) + if err != nil { + return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil + } + if cozeResponse.Code != 0 { + return &model.ErrorWithStatusCode{ + Error: model.Error{ + Message: cozeResponse.Msg, + Code: cozeResponse.Code, + }, + StatusCode: resp.StatusCode, + }, nil + } + fullTextResponse := ResponseCoze2OpenAI(&cozeResponse) + fullTextResponse.Model = modelName + 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) + var responseText string + if len(fullTextResponse.Choices) > 0 { + responseText = fullTextResponse.Choices[0].Message.StringContent() + } + return nil, &responseText +} diff --git a/relay/adaptor/coze/model.go b/relay/adaptor/coze/model.go new file mode 100644 index 00000000..d0afecfe --- /dev/null +++ b/relay/adaptor/coze/model.go @@ -0,0 +1,38 @@ +package coze + +type Message struct { + Role string `json:"role"` + Type string `json:"type"` + Content string `json:"content"` + ContentType string `json:"content_type"` +} + +type ErrorInformation struct { + Code int `json:"code"` + Msg string `json:"msg"` +} + +type Request struct { + ConversationId string `json:"conversation_id,omitempty"` + BotId string `json:"bot_id"` + User string `json:"user"` + Query string `json:"query"` + ChatHistory []Message `json:"chat_history,omitempty"` + Stream bool `json:"stream"` +} + +type Response struct { + ConversationId string `json:"conversation_id,omitempty"` + Messages []Message `json:"messages,omitempty"` + Code int `json:"code,omitempty"` + Msg string `json:"msg,omitempty"` +} + +type StreamResponse struct { + Event string `json:"event,omitempty"` + Message *Message `json:"message,omitempty"` + IsFinish bool `json:"is_finish,omitempty"` + Index int `json:"index,omitempty"` + ConversationId string `json:"conversation_id,omitempty"` + ErrorInformation *ErrorInformation `json:"error_information,omitempty"` +} diff --git a/relay/apitype/define.go b/relay/apitype/define.go index 3760ba00..a3f2b98c 100644 --- a/relay/apitype/define.go +++ b/relay/apitype/define.go @@ -13,6 +13,7 @@ const ( Gemini Ollama AwsClaude + Coze Dummy // this one is only for count, do not add any channel after this ) diff --git a/relay/channeltype/define.go b/relay/channeltype/define.go index faa0d443..6975e492 100644 --- a/relay/channeltype/define.go +++ b/relay/channeltype/define.go @@ -35,6 +35,7 @@ const ( LingYiWanWu StepFun AwsClaude + Coze Dummy ) diff --git a/relay/channeltype/helper.go b/relay/channeltype/helper.go index 89e40142..d249e208 100644 --- a/relay/channeltype/helper.go +++ b/relay/channeltype/helper.go @@ -27,6 +27,8 @@ func ToAPIType(channelType int) int { apiType = apitype.Ollama case AwsClaude: apiType = apitype.AwsClaude + case Coze: + apiType = apitype.Coze } return apiType diff --git a/relay/channeltype/url.go b/relay/channeltype/url.go index 9ac29f30..1f15dfe3 100644 --- a/relay/channeltype/url.go +++ b/relay/channeltype/url.go @@ -35,6 +35,7 @@ var ChannelBaseURLs = []string{ "https://api.lingyiwanwu.com", // 31 "https://api.stepfun.com", // 32 "", // 33 + "https://api.coze.com", // 34 } func init() { diff --git a/web/default/src/constants/channel.constants.js b/web/default/src/constants/channel.constants.js index 82fc7d44..0c1f4822 100644 --- a/web/default/src/constants/channel.constants.js +++ b/web/default/src/constants/channel.constants.js @@ -19,6 +19,7 @@ export const CHANNEL_OPTIONS = [ { key: 30, text: 'Ollama', value: 30, color: 'black' }, { key: 31, text: '零一万物', value: 31, color: 'green' }, { key: 32, text: '阶跃星辰', value: 32, color: 'blue' }, + { key: 34, text: 'Coze', value: 34, color: 'blue' }, { key: 8, text: '自定义渠道', value: 8, color: 'pink' }, { key: 22, text: '知识库:FastGPT', value: 22, color: 'blue' }, { key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple' }, From e5b3e37c465e8de167d5be99dcb0d6752b07fe8b Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 21 Apr 2024 18:04:56 +0800 Subject: [PATCH 095/121] feat: support bot prefix for coze --- relay/adaptor/coze/main.go | 5 +---- web/default/src/pages/Channel/EditChannel.js | 7 +++++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/relay/adaptor/coze/main.go b/relay/adaptor/coze/main.go index 90ed6a70..721c5d13 100644 --- a/relay/adaptor/coze/main.go +++ b/relay/adaptor/coze/main.go @@ -39,10 +39,7 @@ func ConvertRequest(textRequest model.GeneralOpenAIRequest) *Request { cozeRequest := Request{ Stream: textRequest.Stream, User: textRequest.User, - BotId: textRequest.Model, - } - if cozeRequest.User == "" { - cozeRequest.User = "One API User" + BotId: strings.TrimPrefix(textRequest.Model, "bot-"), } for i, message := range textRequest.Messages { if i == len(textRequest.Messages)-1 { diff --git a/web/default/src/pages/Channel/EditChannel.js b/web/default/src/pages/Channel/EditChannel.js index b93c182c..88eb2a48 100644 --- a/web/default/src/pages/Channel/EditChannel.js +++ b/web/default/src/pages/Channel/EditChannel.js @@ -355,6 +355,13 @@ const EditChannel = () => { ) } + { + inputs.type === 34 && ( + + 对于 Coze 而言,模型名称即 Bot ID,你可以添加一个前缀 `bot-`,例如:`bot-123456`。 + + ) + } Date: Sun, 21 Apr 2024 18:54:35 +0800 Subject: [PATCH 096/121] chore: render unknown channel type --- web/default/src/components/ChannelsTable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/default/src/components/ChannelsTable.js b/web/default/src/components/ChannelsTable.js index 5280fd2b..1258ca5a 100644 --- a/web/default/src/components/ChannelsTable.js +++ b/web/default/src/components/ChannelsTable.js @@ -33,7 +33,7 @@ function renderType(type) { } type2label[0] = { value: 0, text: '未知类型', color: 'grey' }; } - return ; + return ; } function renderBalance(type, balance) { From e30ebda0fe112349b329cc5fba4da7ef644e89eb Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 21 Apr 2024 18:55:13 +0800 Subject: [PATCH 097/121] chore: move config key to package ctxkey --- common/ctxkey/key.go | 2 +- middleware/distributor.go | 13 ++++++------- relay/adaptor/aiproxy/adaptor.go | 4 ++-- relay/adaptor/ali/adaptor.go | 6 +++--- relay/adaptor/aws/main.go | 7 +++---- relay/adaptor/azure/helper.go | 4 ++-- relay/adaptor/coze/adaptor.go | 4 ++-- relay/adaptor/xunfei/main.go | 4 ++-- relay/meta/relay_meta.go | 4 ++-- 9 files changed, 23 insertions(+), 25 deletions(-) diff --git a/common/ctxkey/key.go b/common/ctxkey/key.go index 6f1002bd..6125ce4d 100644 --- a/common/ctxkey/key.go +++ b/common/ctxkey/key.go @@ -1,6 +1,6 @@ package ctxkey -var ( +const ( RequestModel = "request_model" ConvertedRequest = "converted_request" OriginalModel = "original_model" diff --git a/middleware/distributor.go b/middleware/distributor.go index 81075e51..88a6383b 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -3,7 +3,6 @@ package middleware import ( "fmt" "github.com/gin-gonic/gin" - "github.com/songquanpeng/one-api/common/config" "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/model" @@ -69,18 +68,18 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode // this is for backward compatibility switch channel.Type { case channeltype.Azure: - c.Set(config.KeyAPIVersion, channel.Other) + c.Set(ctxkey.ConfigAPIVersion, channel.Other) case channeltype.Xunfei: - c.Set(config.KeyAPIVersion, channel.Other) + c.Set(ctxkey.ConfigAPIVersion, channel.Other) case channeltype.Gemini: - c.Set(config.KeyAPIVersion, channel.Other) + c.Set(ctxkey.ConfigAPIVersion, channel.Other) case channeltype.AIProxyLibrary: - c.Set(config.KeyLibraryID, channel.Other) + c.Set(ctxkey.ConfigLibraryID, channel.Other) case channeltype.Ali: - c.Set(config.KeyPlugin, channel.Other) + c.Set(ctxkey.ConfigPlugin, channel.Other) } cfg, _ := channel.LoadConfig() for k, v := range cfg { - c.Set(config.KeyPrefix+k, v) + c.Set(ctxkey.ConfigPrefix+k, v) } } diff --git a/relay/adaptor/aiproxy/adaptor.go b/relay/adaptor/aiproxy/adaptor.go index 7ad6225a..a446f026 100644 --- a/relay/adaptor/aiproxy/adaptor.go +++ b/relay/adaptor/aiproxy/adaptor.go @@ -4,7 +4,7 @@ import ( "errors" "fmt" "github.com/gin-gonic/gin" - "github.com/songquanpeng/one-api/common/config" + "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/relay/adaptor" "github.com/songquanpeng/one-api/relay/meta" "github.com/songquanpeng/one-api/relay/model" @@ -34,7 +34,7 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.G return nil, errors.New("request is nil") } aiProxyLibraryRequest := ConvertRequest(*request) - aiProxyLibraryRequest.LibraryId = c.GetString(config.KeyLibraryID) + aiProxyLibraryRequest.LibraryId = c.GetString(ctxkey.ConfigLibraryID) return aiProxyLibraryRequest, nil } diff --git a/relay/adaptor/ali/adaptor.go b/relay/adaptor/ali/adaptor.go index 21b5e8b8..8e7220ff 100644 --- a/relay/adaptor/ali/adaptor.go +++ b/relay/adaptor/ali/adaptor.go @@ -4,7 +4,7 @@ import ( "errors" "fmt" "github.com/gin-gonic/gin" - "github.com/songquanpeng/one-api/common/config" + "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/relay/adaptor" "github.com/songquanpeng/one-api/relay/meta" "github.com/songquanpeng/one-api/relay/model" @@ -47,8 +47,8 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *me if meta.Mode == relaymode.ImagesGenerations { req.Header.Set("X-DashScope-Async", "enable") } - if c.GetString(config.KeyPlugin) != "" { - req.Header.Set("X-DashScope-Plugin", c.GetString(config.KeyPlugin)) + if c.GetString(ctxkey.ConfigPlugin) != "" { + req.Header.Set("X-DashScope-Plugin", c.GetString(ctxkey.ConfigPlugin)) } return nil } diff --git a/relay/adaptor/aws/main.go b/relay/adaptor/aws/main.go index 7473a549..3db38d22 100644 --- a/relay/adaptor/aws/main.go +++ b/relay/adaptor/aws/main.go @@ -5,7 +5,6 @@ import ( "bytes" "encoding/json" "fmt" - "github.com/songquanpeng/one-api/common/config" "github.com/songquanpeng/one-api/common/ctxkey" "io" "net/http" @@ -25,9 +24,9 @@ import ( ) func newAwsClient(c *gin.Context) (*bedrockruntime.Client, error) { - ak := c.GetString(config.KeyAK) - sk := c.GetString(config.KeySK) - region := c.GetString(config.KeyRegion) + ak := c.GetString(ctxkey.ConfigAK) + sk := c.GetString(ctxkey.ConfigSK) + region := c.GetString(ctxkey.ConfigRegion) client := bedrockruntime.New(bedrockruntime.Options{ Region: region, Credentials: aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(ak, sk, "")), diff --git a/relay/adaptor/azure/helper.go b/relay/adaptor/azure/helper.go index dd207f37..26443bc4 100644 --- a/relay/adaptor/azure/helper.go +++ b/relay/adaptor/azure/helper.go @@ -2,14 +2,14 @@ package azure import ( "github.com/gin-gonic/gin" - "github.com/songquanpeng/one-api/common/config" + "github.com/songquanpeng/one-api/common/ctxkey" ) func GetAPIVersion(c *gin.Context) string { query := c.Request.URL.Query() apiVersion := query.Get("api-version") if apiVersion == "" { - apiVersion = c.GetString(config.KeyAPIVersion) + apiVersion = c.GetString(ctxkey.ConfigAPIVersion) } return apiVersion } diff --git a/relay/adaptor/coze/adaptor.go b/relay/adaptor/coze/adaptor.go index 4af8c021..49979ef6 100644 --- a/relay/adaptor/coze/adaptor.go +++ b/relay/adaptor/coze/adaptor.go @@ -4,7 +4,7 @@ import ( "errors" "fmt" "github.com/gin-gonic/gin" - "github.com/songquanpeng/one-api/common/config" + "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/relay/adaptor" "github.com/songquanpeng/one-api/relay/adaptor/openai" "github.com/songquanpeng/one-api/relay/meta" @@ -34,7 +34,7 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.G if request == nil { return nil, errors.New("request is nil") } - request.User = c.GetString(config.KeyUserID) + request.User = c.GetString(ctxkey.ConfigUserID) return ConvertRequest(*request), nil } diff --git a/relay/adaptor/xunfei/main.go b/relay/adaptor/xunfei/main.go index 369e6227..70a926fd 100644 --- a/relay/adaptor/xunfei/main.go +++ b/relay/adaptor/xunfei/main.go @@ -9,7 +9,7 @@ import ( "github.com/gin-gonic/gin" "github.com/gorilla/websocket" "github.com/songquanpeng/one-api/common" - "github.com/songquanpeng/one-api/common/config" + "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/common/helper" "github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/common/random" @@ -280,7 +280,7 @@ func getAPIVersion(c *gin.Context, modelName string) string { return apiVersion } - apiVersion = c.GetString(config.KeyAPIVersion) + apiVersion = c.GetString(ctxkey.ConfigAPIVersion) if apiVersion != "" { return apiVersion } diff --git a/relay/meta/relay_meta.go b/relay/meta/relay_meta.go index 22ef1567..421c317e 100644 --- a/relay/meta/relay_meta.go +++ b/relay/meta/relay_meta.go @@ -2,7 +2,7 @@ package meta import ( "github.com/gin-gonic/gin" - "github.com/songquanpeng/one-api/common/config" + "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/relay/adaptor/azure" "github.com/songquanpeng/one-api/relay/channeltype" "github.com/songquanpeng/one-api/relay/relaymode" @@ -41,7 +41,7 @@ func GetByContext(c *gin.Context) *Meta { Group: c.GetString("group"), ModelMapping: c.GetStringMapString("model_mapping"), BaseURL: c.GetString("base_url"), - APIVersion: c.GetString(config.KeyAPIVersion), + APIVersion: c.GetString(ctxkey.ConfigAPIVersion), APIKey: strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "), Config: nil, RequestURLPath: c.Request.URL.String(), From 83517f687c13b4d76728d6f1f14991bbe01899a7 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 21 Apr 2024 18:55:25 +0800 Subject: [PATCH 098/121] chore: move config key to package ctxkey --- common/config/key.go | 13 ------------- common/ctxkey/config.go | 13 +++++++++++++ 2 files changed, 13 insertions(+), 13 deletions(-) delete mode 100644 common/config/key.go create mode 100644 common/ctxkey/config.go diff --git a/common/config/key.go b/common/config/key.go deleted file mode 100644 index da182b37..00000000 --- a/common/config/key.go +++ /dev/null @@ -1,13 +0,0 @@ -package config - -const ( - KeyPrefix = "cfg_" - - KeyAPIVersion = KeyPrefix + "api_version" - KeyLibraryID = KeyPrefix + "library_id" - KeyPlugin = KeyPrefix + "plugin" - KeySK = KeyPrefix + "sk" - KeyAK = KeyPrefix + "ak" - KeyRegion = KeyPrefix + "region" - KeyUserID = KeyPrefix + "user_id" -) diff --git a/common/ctxkey/config.go b/common/ctxkey/config.go new file mode 100644 index 00000000..69e8a27a --- /dev/null +++ b/common/ctxkey/config.go @@ -0,0 +1,13 @@ +package ctxkey + +const ( + ConfigPrefix = "cfg_" + + ConfigAPIVersion = ConfigPrefix + "api_version" + ConfigLibraryID = ConfigPrefix + "library_id" + ConfigPlugin = ConfigPrefix + "plugin" + ConfigSK = ConfigPrefix + "sk" + ConfigAK = ConfigPrefix + "ak" + ConfigRegion = ConfigPrefix + "region" + ConfigUserID = ConfigPrefix + "user_id" +) From 3d149fedf45472eff92910324974c762fc37dad6 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 21 Apr 2024 19:43:23 +0800 Subject: [PATCH 099/121] chore: do not hardcode context key --- common/ctxkey/key.go | 20 +++++++++++++++++--- controller/auth/wechat.go | 3 ++- controller/billing.go | 9 +++++---- controller/channel-test.go | 5 +++-- controller/log.go | 7 ++++--- controller/model.go | 9 +++++---- controller/redemption.go | 3 ++- controller/relay.go | 12 ++++++------ controller/token.go | 17 +++++++++-------- controller/user.go | 15 ++++++++------- middleware/auth.go | 13 +++++++------ middleware/distributor.go | 18 +++++++++--------- relay/controller/audio.go | 19 ++++++++++--------- relay/controller/image.go | 5 +++-- relay/meta/relay_meta.go | 16 ++++++++-------- 15 files changed, 98 insertions(+), 73 deletions(-) diff --git a/common/ctxkey/key.go b/common/ctxkey/key.go index 6125ce4d..568cb095 100644 --- a/common/ctxkey/key.go +++ b/common/ctxkey/key.go @@ -1,7 +1,21 @@ package ctxkey const ( - RequestModel = "request_model" - ConvertedRequest = "converted_request" - OriginalModel = "original_model" + Id = "id" + Username = "username" + Role = "role" + Status = "status" + Channel = "channel" + ChannelId = "channel_id" + SpecificChannelId = "specific_channel_id" + RequestModel = "request_model" + ConvertedRequest = "converted_request" + OriginalModel = "original_model" + Group = "group" + ModelMapping = "model_mapping" + ChannelName = "channel_name" + TokenId = "token_id" + TokenName = "token_name" + BaseURL = "base_url" + AvailableModels = "available_models" ) diff --git a/controller/auth/wechat.go b/controller/auth/wechat.go index a64746c9..a561aec0 100644 --- a/controller/auth/wechat.go +++ b/controller/auth/wechat.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/gin-gonic/gin" "github.com/songquanpeng/one-api/common/config" + "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/controller" "github.com/songquanpeng/one-api/model" "net/http" @@ -136,7 +137,7 @@ func WeChatBind(c *gin.Context) { }) return } - id := c.GetInt("id") + id := c.GetInt(ctxkey.Id) user := model.User{ Id: id, } diff --git a/controller/billing.go b/controller/billing.go index dd518678..0d03e4c1 100644 --- a/controller/billing.go +++ b/controller/billing.go @@ -3,6 +3,7 @@ package controller import ( "github.com/gin-gonic/gin" "github.com/songquanpeng/one-api/common/config" + "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/model" relaymodel "github.com/songquanpeng/one-api/relay/model" ) @@ -14,13 +15,13 @@ func GetSubscription(c *gin.Context) { var token *model.Token var expiredTime int64 if config.DisplayTokenStatEnabled { - tokenId := c.GetInt("token_id") + tokenId := c.GetInt(ctxkey.TokenId) token, err = model.GetTokenById(tokenId) expiredTime = token.ExpiredTime remainQuota = token.RemainQuota usedQuota = token.UsedQuota } else { - userId := c.GetInt("id") + userId := c.GetInt(ctxkey.Id) remainQuota, err = model.GetUserQuota(userId) if err != nil { usedQuota, err = model.GetUserUsedQuota(userId) @@ -64,11 +65,11 @@ func GetUsage(c *gin.Context) { var err error var token *model.Token if config.DisplayTokenStatEnabled { - tokenId := c.GetInt("token_id") + tokenId := c.GetInt(ctxkey.TokenId) token, err = model.GetTokenById(tokenId) quota = token.UsedQuota } else { - userId := c.GetInt("id") + userId := c.GetInt(ctxkey.Id) quota, err = model.GetUserUsedQuota(userId) } if err != nil { diff --git a/controller/channel-test.go b/controller/channel-test.go index e326d062..a84dc797 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "github.com/songquanpeng/one-api/common/config" + "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/common/message" "github.com/songquanpeng/one-api/middleware" @@ -54,8 +55,8 @@ func testChannel(channel *model.Channel) (err error, openaiErr *relaymodel.Error } c.Request.Header.Set("Authorization", "Bearer "+channel.Key) c.Request.Header.Set("Content-Type", "application/json") - c.Set("channel", channel.Type) - c.Set("base_url", channel.GetBaseURL()) + c.Set(ctxkey.Channel, channel.Type) + c.Set(ctxkey.BaseURL, channel.GetBaseURL()) middleware.SetupContextForSelectedChannel(c, channel, "") meta := meta.GetByContext(c) apiType := channeltype.ToAPIType(channel.Type) diff --git a/controller/log.go b/controller/log.go index 9377b338..665f49be 100644 --- a/controller/log.go +++ b/controller/log.go @@ -3,6 +3,7 @@ package controller import ( "github.com/gin-gonic/gin" "github.com/songquanpeng/one-api/common/config" + "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/model" "net/http" "strconv" @@ -41,7 +42,7 @@ func GetUserLogs(c *gin.Context) { if p < 0 { p = 0 } - userId := c.GetInt("id") + userId := c.GetInt(ctxkey.Id) logType, _ := strconv.Atoi(c.Query("type")) startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64) endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64) @@ -83,7 +84,7 @@ func SearchAllLogs(c *gin.Context) { func SearchUserLogs(c *gin.Context) { keyword := c.Query("keyword") - userId := c.GetInt("id") + userId := c.GetInt(ctxkey.Id) logs, err := model.SearchUserLogs(userId, keyword) if err != nil { c.JSON(http.StatusOK, gin.H{ @@ -122,7 +123,7 @@ func GetLogsStat(c *gin.Context) { } func GetLogsSelfStat(c *gin.Context) { - username := c.GetString("username") + username := c.GetString(ctxkey.Username) logType, _ := strconv.Atoi(c.Query("type")) startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64) endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64) diff --git a/controller/model.go b/controller/model.go index 77e2e94e..dcbe709e 100644 --- a/controller/model.go +++ b/controller/model.go @@ -3,6 +3,7 @@ package controller import ( "fmt" "github.com/gin-gonic/gin" + "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/model" relay "github.com/songquanpeng/one-api/relay" "github.com/songquanpeng/one-api/relay/adaptor/openai" @@ -131,10 +132,10 @@ func ListAllModels(c *gin.Context) { func ListModels(c *gin.Context) { ctx := c.Request.Context() var availableModels []string - if c.GetString("available_models") != "" { - availableModels = strings.Split(c.GetString("available_models"), ",") + if c.GetString(ctxkey.AvailableModels) != "" { + availableModels = strings.Split(c.GetString(ctxkey.AvailableModels), ",") } else { - userId := c.GetInt("id") + userId := c.GetInt(ctxkey.Id) userGroup, _ := model.CacheGetUserGroup(userId) availableModels, _ = model.CacheGetGroupModels(ctx, userGroup) } @@ -186,7 +187,7 @@ func RetrieveModel(c *gin.Context) { func GetUserAvailableModels(c *gin.Context) { ctx := c.Request.Context() - id := c.GetInt("id") + id := c.GetInt(ctxkey.Id) userGroup, err := model.CacheGetUserGroup(id) if err != nil { c.JSON(http.StatusOK, gin.H{ diff --git a/controller/redemption.go b/controller/redemption.go index 8d2b3f38..1d0ffbad 100644 --- a/controller/redemption.go +++ b/controller/redemption.go @@ -3,6 +3,7 @@ package controller import ( "github.com/gin-gonic/gin" "github.com/songquanpeng/one-api/common/config" + "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/common/helper" "github.com/songquanpeng/one-api/common/random" "github.com/songquanpeng/one-api/model" @@ -109,7 +110,7 @@ func AddRedemption(c *gin.Context) { for i := 0; i < redemption.Count; i++ { key := random.GetUUID() cleanRedemption := model.Redemption{ - UserId: c.GetInt("id"), + UserId: c.GetInt(ctxkey.Id), Name: redemption.Name, Key: key, CreatedTime: helper.GetTimestamp(), diff --git a/controller/relay.go b/controller/relay.go index 49408d13..5fd22f85 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -46,15 +46,15 @@ func Relay(c *gin.Context) { requestBody, _ := common.GetRequestBody(c) logger.Debugf(ctx, "request body: %s", string(requestBody)) } - channelId := c.GetInt("channel_id") + channelId := c.GetInt(ctxkey.ChannelId) bizErr := relayHelper(c, relayMode) if bizErr == nil { monitor.Emit(channelId, true) return } lastFailedChannelId := channelId - channelName := c.GetString("channel_name") - group := c.GetString("group") + channelName := c.GetString(ctxkey.ChannelName) + group := c.GetString(ctxkey.Group) originalModel := c.GetString(ctxkey.OriginalModel) go processChannelRelayError(ctx, channelId, channelName, bizErr) requestId := c.GetString(logger.RequestIdKey) @@ -80,9 +80,9 @@ func Relay(c *gin.Context) { if bizErr == nil { return } - channelId := c.GetInt("channel_id") + channelId := c.GetInt(ctxkey.ChannelId) lastFailedChannelId = channelId - channelName := c.GetString("channel_name") + channelName := c.GetString(ctxkey.ChannelName) go processChannelRelayError(ctx, channelId, channelName, bizErr) } if bizErr != nil { @@ -97,7 +97,7 @@ func Relay(c *gin.Context) { } func shouldRetry(c *gin.Context, statusCode int) bool { - if _, ok := c.Get("specific_channel_id"); ok { + if _, ok := c.Get(ctxkey.SpecificChannelId); ok { return false } if statusCode == http.StatusTooManyRequests { diff --git a/controller/token.go b/controller/token.go index 557b5ce1..668ccd97 100644 --- a/controller/token.go +++ b/controller/token.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/gin-gonic/gin" "github.com/songquanpeng/one-api/common/config" + "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/common/helper" "github.com/songquanpeng/one-api/common/network" "github.com/songquanpeng/one-api/common/random" @@ -13,7 +14,7 @@ import ( ) func GetAllTokens(c *gin.Context) { - userId := c.GetInt("id") + userId := c.GetInt(ctxkey.Id) p, _ := strconv.Atoi(c.Query("p")) if p < 0 { p = 0 @@ -38,7 +39,7 @@ func GetAllTokens(c *gin.Context) { } func SearchTokens(c *gin.Context) { - userId := c.GetInt("id") + userId := c.GetInt(ctxkey.Id) keyword := c.Query("keyword") tokens, err := model.SearchUserTokens(userId, keyword) if err != nil { @@ -58,7 +59,7 @@ func SearchTokens(c *gin.Context) { func GetToken(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) - userId := c.GetInt("id") + userId := c.GetInt(ctxkey.Id) if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, @@ -83,8 +84,8 @@ func GetToken(c *gin.Context) { } func GetTokenStatus(c *gin.Context) { - tokenId := c.GetInt("token_id") - userId := c.GetInt("id") + tokenId := c.GetInt(ctxkey.TokenId) + userId := c.GetInt(ctxkey.Id) token, err := model.GetTokenByIds(tokenId, userId) if err != nil { c.JSON(http.StatusOK, gin.H{ @@ -139,7 +140,7 @@ func AddToken(c *gin.Context) { } cleanToken := model.Token{ - UserId: c.GetInt("id"), + UserId: c.GetInt(ctxkey.Id), Name: token.Name, Key: random.GenerateKey(), CreatedTime: helper.GetTimestamp(), @@ -168,7 +169,7 @@ func AddToken(c *gin.Context) { func DeleteToken(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) - userId := c.GetInt("id") + userId := c.GetInt(ctxkey.Id) err := model.DeleteTokenById(id, userId) if err != nil { c.JSON(http.StatusOK, gin.H{ @@ -185,7 +186,7 @@ func DeleteToken(c *gin.Context) { } func UpdateToken(c *gin.Context) { - userId := c.GetInt("id") + userId := c.GetInt(ctxkey.Id) statusOnly := c.Query("status_only") token := model.Token{} err := c.ShouldBindJSON(&token) diff --git a/controller/user.go b/controller/user.go index 44b4f793..af90acf6 100644 --- a/controller/user.go +++ b/controller/user.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/songquanpeng/one-api/common" "github.com/songquanpeng/one-api/common/config" + "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/common/random" "github.com/songquanpeng/one-api/model" "net/http" @@ -238,7 +239,7 @@ func GetUser(c *gin.Context) { }) return } - myRole := c.GetInt("role") + myRole := c.GetInt(ctxkey.Role) if myRole <= user.Role && myRole != model.RoleRootUser { c.JSON(http.StatusOK, gin.H{ "success": false, @@ -255,7 +256,7 @@ func GetUser(c *gin.Context) { } func GetUserDashboard(c *gin.Context) { - id := c.GetInt("id") + id := c.GetInt(ctxkey.Id) now := time.Now() startOfDay := now.Truncate(24*time.Hour).AddDate(0, 0, -6).Unix() endOfDay := now.Truncate(24 * time.Hour).Add(24*time.Hour - time.Second).Unix() @@ -278,7 +279,7 @@ func GetUserDashboard(c *gin.Context) { } func GenerateAccessToken(c *gin.Context) { - id := c.GetInt("id") + id := c.GetInt(ctxkey.Id) user, err := model.GetUserById(id, true) if err != nil { c.JSON(http.StatusOK, gin.H{ @@ -314,7 +315,7 @@ func GenerateAccessToken(c *gin.Context) { } func GetAffCode(c *gin.Context) { - id := c.GetInt("id") + id := c.GetInt(ctxkey.Id) user, err := model.GetUserById(id, true) if err != nil { c.JSON(http.StatusOK, gin.H{ @@ -342,7 +343,7 @@ func GetAffCode(c *gin.Context) { } func GetSelf(c *gin.Context) { - id := c.GetInt("id") + id := c.GetInt(ctxkey.Id) user, err := model.GetUserById(id, false) if err != nil { c.JSON(http.StatusOK, gin.H{ @@ -387,7 +388,7 @@ func UpdateUser(c *gin.Context) { }) return } - myRole := c.GetInt("role") + myRole := c.GetInt(ctxkey.Role) if myRole <= originUser.Role && myRole != model.RoleRootUser { c.JSON(http.StatusOK, gin.H{ "success": false, @@ -445,7 +446,7 @@ func UpdateSelf(c *gin.Context) { } cleanUser := model.User{ - Id: c.GetInt("id"), + Id: c.GetInt(ctxkey.Id), Username: user.Username, Password: user.Password, DisplayName: user.DisplayName, diff --git a/middleware/auth.go b/middleware/auth.go index 64ce6608..5cba490a 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -5,6 +5,7 @@ import ( "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" "github.com/songquanpeng/one-api/common/blacklist" + "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/common/network" "github.com/songquanpeng/one-api/model" "net/http" @@ -120,20 +121,20 @@ func TokenAuth() func(c *gin.Context) { abortWithMessage(c, http.StatusBadRequest, err.Error()) return } - c.Set("request_model", requestModel) + c.Set(ctxkey.RequestModel, requestModel) if token.Models != nil && *token.Models != "" { - c.Set("available_models", *token.Models) + c.Set(ctxkey.AvailableModels, *token.Models) if requestModel != "" && !isModelInList(requestModel, *token.Models) { abortWithMessage(c, http.StatusForbidden, fmt.Sprintf("该令牌无权使用模型:%s", requestModel)) return } } - c.Set("id", token.UserId) - c.Set("token_id", token.Id) - c.Set("token_name", token.Name) + c.Set(ctxkey.Id, token.UserId) + c.Set(ctxkey.TokenId, token.Id) + c.Set(ctxkey.TokenName, token.Name) if len(parts) > 1 { if model.IsAdmin(token.UserId) { - c.Set("specific_channel_id", parts[1]) + c.Set(ctxkey.SpecificChannelId, parts[1]) } else { abortWithMessage(c, http.StatusForbidden, "普通用户不支持指定渠道") return diff --git a/middleware/distributor.go b/middleware/distributor.go index 88a6383b..a4c34085 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -17,12 +17,12 @@ type ModelRequest struct { func Distribute() func(c *gin.Context) { return func(c *gin.Context) { - userId := c.GetInt("id") + userId := c.GetInt(ctxkey.Id) userGroup, _ := model.CacheGetUserGroup(userId) - c.Set("group", userGroup) + c.Set(ctxkey.Group, userGroup) var requestModel string var channel *model.Channel - channelId, ok := c.Get("specific_channel_id") + channelId, ok := c.Get(ctxkey.SpecificChannelId) if ok { id, err := strconv.Atoi(channelId.(string)) if err != nil { @@ -39,7 +39,7 @@ func Distribute() func(c *gin.Context) { return } } else { - requestModel = c.GetString("request_model") + requestModel = c.GetString(ctxkey.RequestModel) var err error channel, err = model.CacheGetRandomSatisfiedChannel(userGroup, requestModel, false) if err != nil { @@ -58,13 +58,13 @@ func Distribute() func(c *gin.Context) { } func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, modelName string) { - c.Set("channel", channel.Type) - c.Set("channel_id", channel.Id) - c.Set("channel_name", channel.Name) - c.Set("model_mapping", channel.GetModelMapping()) + c.Set(ctxkey.Channel, channel.Type) + c.Set(ctxkey.ChannelId, channel.Id) + c.Set(ctxkey.ChannelName, channel.Name) + c.Set(ctxkey.ModelMapping, channel.GetModelMapping()) c.Set(ctxkey.OriginalModel, modelName) // for retry c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key)) - c.Set("base_url", channel.GetBaseURL()) + c.Set(ctxkey.BaseURL, channel.GetBaseURL()) // this is for backward compatibility switch channel.Type { case channeltype.Azure: diff --git a/relay/controller/audio.go b/relay/controller/audio.go index 9d8cfef5..db543318 100644 --- a/relay/controller/audio.go +++ b/relay/controller/audio.go @@ -10,6 +10,7 @@ import ( "github.com/gin-gonic/gin" "github.com/songquanpeng/one-api/common" "github.com/songquanpeng/one-api/common/config" + "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/model" "github.com/songquanpeng/one-api/relay/adaptor/azure" @@ -29,12 +30,12 @@ func RelayAudioHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus ctx := c.Request.Context() audioModel := "whisper-1" - tokenId := c.GetInt("token_id") - channelType := c.GetInt("channel") - channelId := c.GetInt("channel_id") - userId := c.GetInt("id") - group := c.GetString("group") - tokenName := c.GetString("token_name") + tokenId := c.GetInt(ctxkey.TokenId) + channelType := c.GetInt(ctxkey.Channel) + channelId := c.GetInt(ctxkey.ChannelId) + userId := c.GetInt(ctxkey.Id) + group := c.GetString(ctxkey.Group) + tokenName := c.GetString(ctxkey.TokenName) var ttsRequest openai.TextToSpeechRequest if relayMode == relaymode.AudioSpeech { @@ -107,7 +108,7 @@ func RelayAudioHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus }() // map model name - modelMapping := c.GetString("model_mapping") + modelMapping := c.GetString(ctxkey.ModelMapping) if modelMapping != "" { modelMap := make(map[string]string) err := json.Unmarshal([]byte(modelMapping), &modelMap) @@ -121,8 +122,8 @@ func RelayAudioHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus baseURL := channeltype.ChannelBaseURLs[channelType] requestURL := c.Request.URL.String() - if c.GetString("base_url") != "" { - baseURL = c.GetString("base_url") + if c.GetString(ctxkey.BaseURL) != "" { + baseURL = c.GetString(ctxkey.BaseURL) } fullRequestURL := openai.GetFullRequestURL(baseURL, requestURL, channelType) diff --git a/relay/controller/image.go b/relay/controller/image.go index b878e8f1..216e4700 100644 --- a/relay/controller/image.go +++ b/relay/controller/image.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "github.com/gin-gonic/gin" + "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/model" "github.com/songquanpeng/one-api/relay" @@ -119,11 +120,11 @@ func RelayImageHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus logger.SysError("error update user quota cache: " + err.Error()) } if quota != 0 { - tokenName := c.GetString("token_name") + tokenName := c.GetString(ctxkey.TokenName) logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio) model.RecordConsumeLog(ctx, meta.UserId, meta.ChannelId, 0, 0, imageRequest.Model, tokenName, quota, logContent) model.UpdateUserUsedQuotaAndRequestCount(meta.UserId, quota) - channelId := c.GetInt("channel_id") + channelId := c.GetInt(ctxkey.ChannelId) model.UpdateChannelUsedQuota(channelId, quota) } }(c.Request.Context()) diff --git a/relay/meta/relay_meta.go b/relay/meta/relay_meta.go index 421c317e..0e8f72fe 100644 --- a/relay/meta/relay_meta.go +++ b/relay/meta/relay_meta.go @@ -33,14 +33,14 @@ type Meta struct { func GetByContext(c *gin.Context) *Meta { meta := Meta{ Mode: relaymode.GetByPath(c.Request.URL.Path), - ChannelType: c.GetInt("channel"), - ChannelId: c.GetInt("channel_id"), - TokenId: c.GetInt("token_id"), - TokenName: c.GetString("token_name"), - UserId: c.GetInt("id"), - Group: c.GetString("group"), - ModelMapping: c.GetStringMapString("model_mapping"), - BaseURL: c.GetString("base_url"), + ChannelType: c.GetInt(ctxkey.Channel), + ChannelId: c.GetInt(ctxkey.ChannelId), + TokenId: c.GetInt(ctxkey.TokenId), + TokenName: c.GetString(ctxkey.TokenName), + UserId: c.GetInt(ctxkey.Id), + Group: c.GetString(ctxkey.Group), + ModelMapping: c.GetStringMapString(ctxkey.ModelMapping), + BaseURL: c.GetString(ctxkey.BaseURL), APIVersion: c.GetString(ctxkey.ConfigAPIVersion), APIKey: strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "), Config: nil, From 779b747e9e52c6dde7edf40eb226159af470a41c Mon Sep 17 00:00:00 2001 From: Wei Tingjiang Date: Wed, 24 Apr 2024 21:26:45 +0800 Subject: [PATCH 100/121] feat: add function and tools support for Gemini (#1358) * Update model.go * Support Gemini tool_calls. * Fix gemini tool calls (also keep support functions). * Fixed the problem of arguments not being stringified. Fix panic: candidate.Content.Parts out of range --- relay/adaptor/gemini/main.go | 55 ++++++++++++++++++++++++++++++----- relay/adaptor/gemini/model.go | 12 ++++++-- 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/relay/adaptor/gemini/main.go b/relay/adaptor/gemini/main.go index 6bf0c6d7..8b934d30 100644 --- a/relay/adaptor/gemini/main.go +++ b/relay/adaptor/gemini/main.go @@ -4,6 +4,10 @@ import ( "bufio" "encoding/json" "fmt" + "io" + "net/http" + "strings" + "github.com/songquanpeng/one-api/common" "github.com/songquanpeng/one-api/common/config" "github.com/songquanpeng/one-api/common/helper" @@ -13,9 +17,6 @@ import ( "github.com/songquanpeng/one-api/relay/adaptor/openai" "github.com/songquanpeng/one-api/relay/constant" "github.com/songquanpeng/one-api/relay/model" - "io" - "net/http" - "strings" "github.com/gin-gonic/gin" ) @@ -54,7 +55,17 @@ func ConvertRequest(textRequest model.GeneralOpenAIRequest) *ChatRequest { MaxOutputTokens: textRequest.MaxTokens, }, } - if textRequest.Functions != nil { + if textRequest.Tools != nil { + functions := make([]model.Function, 0, len(textRequest.Tools)) + for _, tool := range textRequest.Tools { + functions = append(functions, tool.Function) + } + geminiRequest.Tools = []ChatTools{ + { + FunctionDeclarations: functions, + }, + } + } else if textRequest.Functions != nil { geminiRequest.Tools = []ChatTools{ { FunctionDeclarations: textRequest.Functions, @@ -154,6 +165,30 @@ type ChatPromptFeedback struct { SafetyRatings []ChatSafetyRating `json:"safetyRatings"` } +func getToolCalls(candidate *ChatCandidate) []model.Tool { + var toolCalls []model.Tool + + item := candidate.Content.Parts[0] + if item.FunctionCall == nil { + return toolCalls + } + argsBytes, err := json.Marshal(item.FunctionCall.Arguments) + if err != nil { + logger.FatalLog("getToolCalls failed: " + err.Error()) + return toolCalls + } + toolCall := model.Tool{ + Id: fmt.Sprintf("call_%s", random.GetUUID()), + Type: "function", + Function: model.Function{ + Arguments: string(argsBytes), + Name: item.FunctionCall.FunctionName, + }, + } + toolCalls = append(toolCalls, toolCall) + return toolCalls +} + func responseGeminiChat2OpenAI(response *ChatResponse) *openai.TextResponse { fullTextResponse := openai.TextResponse{ Id: fmt.Sprintf("chatcmpl-%s", random.GetUUID()), @@ -165,13 +200,19 @@ func responseGeminiChat2OpenAI(response *ChatResponse) *openai.TextResponse { choice := openai.TextResponseChoice{ Index: i, Message: model.Message{ - Role: "assistant", - Content: "", + Role: "assistant", }, FinishReason: constant.StopFinishReason, } if len(candidate.Content.Parts) > 0 { - choice.Message.Content = candidate.Content.Parts[0].Text + if candidate.Content.Parts[0].FunctionCall != nil { + choice.Message.ToolCalls = getToolCalls(&candidate) + } else { + choice.Message.Content = candidate.Content.Parts[0].Text + } + } else { + choice.Message.Content = "" + choice.FinishReason = candidate.FinishReason } fullTextResponse.Choices = append(fullTextResponse.Choices, choice) } diff --git a/relay/adaptor/gemini/model.go b/relay/adaptor/gemini/model.go index d1e3c4fd..47b74fbc 100644 --- a/relay/adaptor/gemini/model.go +++ b/relay/adaptor/gemini/model.go @@ -12,9 +12,15 @@ type InlineData struct { Data string `json:"data"` } +type FunctionCall struct { + FunctionName string `json:"name"` + Arguments any `json:"args"` +} + type Part struct { - Text string `json:"text,omitempty"` - InlineData *InlineData `json:"inlineData,omitempty"` + Text string `json:"text,omitempty"` + InlineData *InlineData `json:"inlineData,omitempty"` + FunctionCall *FunctionCall `json:"functionCall,omitempty"` } type ChatContent struct { @@ -28,7 +34,7 @@ type ChatSafetySettings struct { } type ChatTools struct { - FunctionDeclarations any `json:"functionDeclarations,omitempty"` + FunctionDeclarations any `json:"function_declarations,omitempty"` } type ChatGenerationConfig struct { From cb33e8aad585f797fbbe533de294f63f5d1e85d0 Mon Sep 17 00:00:00 2001 From: tylinux Date: Wed, 24 Apr 2024 21:29:48 +0800 Subject: [PATCH 101/121] fix: fix default theme blank screen when edit channel again (#1363) * fix: throw exception after submit channel edit * fix: replace with destructuring assignment --- web/default/src/pages/Channel/EditChannel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/default/src/pages/Channel/EditChannel.js b/web/default/src/pages/Channel/EditChannel.js index 88eb2a48..ffc9fc5f 100644 --- a/web/default/src/pages/Channel/EditChannel.js +++ b/web/default/src/pages/Channel/EditChannel.js @@ -174,7 +174,7 @@ const EditChannel = () => { showInfo('模型映射必须是合法的 JSON 格式!'); return; } - let localInputs = inputs; + let localInputs = {...inputs}; if (localInputs.base_url && localInputs.base_url.endsWith('/')) { localInputs.base_url = localInputs.base_url.slice(0, localInputs.base_url.length - 1); } From 24f026d18e608a511efa3f88ca12b373d634cabd Mon Sep 17 00:00:00 2001 From: Ghostz <137054651+ye4293@users.noreply.github.com> Date: Wed, 24 Apr 2024 21:50:01 +0800 Subject: [PATCH 102/121] feat: add cohere support (#1355) * support cohere * chore: tiny improvements --------- Co-authored-by: JustSong --- README.md | 1 + relay/adaptor.go | 3 + relay/adaptor/cohere/adaptor.go | 64 +++++ relay/adaptor/cohere/constant.go | 7 + relay/adaptor/cohere/main.go | 233 ++++++++++++++++++ relay/adaptor/cohere/model.go | 147 +++++++++++ relay/apitype/define.go | 1 + relay/billing/ratio/model.go | 16 +- relay/channeltype/define.go | 1 + relay/channeltype/helper.go | 2 + relay/channeltype/url.go | 1 + .../src/constants/channel.constants.js | 1 + 12 files changed, 476 insertions(+), 1 deletion(-) create mode 100644 relay/adaptor/cohere/adaptor.go create mode 100644 relay/adaptor/cohere/constant.go create mode 100644 relay/adaptor/cohere/main.go create mode 100644 relay/adaptor/cohere/model.go diff --git a/README.md b/README.md index 0ab35893..2f81c10d 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 + [x] [零一万物](https://platform.lingyiwanwu.com/) + [x] [阶跃星辰](https://platform.stepfun.com/) + [x] [Coze](https://www.coze.com/) + + [x] [Cohere](https://cohere.com/) 2. 支持配置镜像以及众多[第三方代理服务](https://iamazing.cn/page/openai-api-third-party-services)。 3. 支持通过**负载均衡**的方式访问多个渠道。 4. 支持 **stream 模式**,可以通过流式传输实现打字机效果。 diff --git a/relay/adaptor.go b/relay/adaptor.go index 24db9e89..293b6d79 100644 --- a/relay/adaptor.go +++ b/relay/adaptor.go @@ -7,6 +7,7 @@ import ( "github.com/songquanpeng/one-api/relay/adaptor/anthropic" "github.com/songquanpeng/one-api/relay/adaptor/aws" "github.com/songquanpeng/one-api/relay/adaptor/baidu" + "github.com/songquanpeng/one-api/relay/adaptor/cohere" "github.com/songquanpeng/one-api/relay/adaptor/coze" "github.com/songquanpeng/one-api/relay/adaptor/gemini" "github.com/songquanpeng/one-api/relay/adaptor/ollama" @@ -46,6 +47,8 @@ func GetAdaptor(apiType int) adaptor.Adaptor { return &ollama.Adaptor{} case apitype.Coze: return &coze.Adaptor{} + case apitype.Cohere: + return &cohere.Adaptor{} } return nil } diff --git a/relay/adaptor/cohere/adaptor.go b/relay/adaptor/cohere/adaptor.go new file mode 100644 index 00000000..6fdb1b04 --- /dev/null +++ b/relay/adaptor/cohere/adaptor.go @@ -0,0 +1,64 @@ +package cohere + +import ( + "errors" + "fmt" + "io" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/songquanpeng/one-api/relay/adaptor" + "github.com/songquanpeng/one-api/relay/meta" + "github.com/songquanpeng/one-api/relay/model" +) + +type Adaptor struct{} + +// ConvertImageRequest implements adaptor.Adaptor. +func (*Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { + return nil, errors.New("not implemented") +} + +// ConvertImageRequest implements adaptor.Adaptor. + +func (a *Adaptor) Init(meta *meta.Meta) { + +} + +func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { + return fmt.Sprintf("%s/v1/chat", meta.BaseURL), nil +} + +func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { + adaptor.SetupCommonRequestHeader(c, req, meta) + req.Header.Set("Authorization", "Bearer "+meta.APIKey) + return nil +} + +func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) { + if request == nil { + return nil, errors.New("request is nil") + } + return ConvertRequest(*request), nil +} + +func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { + return adaptor.DoRequestHelper(a, c, meta, requestBody) +} + +func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { + if meta.IsStream { + err, usage = StreamHandler(c, resp) + } else { + err, usage = Handler(c, resp, meta.PromptTokens, meta.ActualModelName) + } + return +} + +func (a *Adaptor) GetModelList() []string { + return ModelList +} + +func (a *Adaptor) GetChannelName() string { + return "Cohere" +} diff --git a/relay/adaptor/cohere/constant.go b/relay/adaptor/cohere/constant.go new file mode 100644 index 00000000..3ff4d655 --- /dev/null +++ b/relay/adaptor/cohere/constant.go @@ -0,0 +1,7 @@ +package cohere + +var ModelList = []string{ + "command", "command-nightly", + "command-light", "command-light-nightly", + "command-r", "command-r-plus", +} diff --git a/relay/adaptor/cohere/main.go b/relay/adaptor/cohere/main.go new file mode 100644 index 00000000..81277b07 --- /dev/null +++ b/relay/adaptor/cohere/main.go @@ -0,0 +1,233 @@ +package cohere + +import ( + "bufio" + "bytes" + "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" + "github.com/songquanpeng/one-api/common/logger" + "github.com/songquanpeng/one-api/relay/adaptor/openai" + "github.com/songquanpeng/one-api/relay/model" +) + +func stopReasonCohere2OpenAI(reason *string) string { + if reason == nil { + return "" + } + switch *reason { + case "COMPLETE": + return "stop" + default: + return *reason + } +} + +func ConvertRequest(textRequest model.GeneralOpenAIRequest) *Request { + cohereRequest := Request{ + Model: textRequest.Model, + Message: "", + MaxTokens: textRequest.MaxTokens, + Temperature: textRequest.Temperature, + P: textRequest.TopP, + K: textRequest.TopK, + Stream: textRequest.Stream, + FrequencyPenalty: textRequest.FrequencyPenalty, + PresencePenalty: textRequest.FrequencyPenalty, + Seed: int(textRequest.Seed), + } + if cohereRequest.Model == "" { + cohereRequest.Model = "command-r" + } + for _, message := range textRequest.Messages { + if message.Role == "user" { + cohereRequest.Message = message.Content.(string) + } else { + var role string + if message.Role == "assistant" { + role = "CHATBOT" + } else if message.Role == "system" { + role = "SYSTEM" + } else { + role = "USER" + } + cohereRequest.ChatHistory = append(cohereRequest.ChatHistory, ChatMessage{ + Role: role, + Message: message.Content.(string), + }) + } + } + return &cohereRequest +} + +func StreamResponseCohere2OpenAI(cohereResponse *StreamResponse) (*openai.ChatCompletionsStreamResponse, *Response) { + var response *Response + var responseText string + var finishReason string + + switch cohereResponse.EventType { + case "stream-start": + return nil, nil + case "text-generation": + responseText += cohereResponse.Text + case "stream-end": + usage := cohereResponse.Response.Meta.Tokens + response = &Response{ + Meta: Meta{ + Tokens: Usage{ + InputTokens: usage.InputTokens, + OutputTokens: usage.OutputTokens, + }, + }, + } + finishReason = *cohereResponse.Response.FinishReason + default: + return nil, nil + } + + var choice openai.ChatCompletionsStreamResponseChoice + choice.Delta.Content = responseText + choice.Delta.Role = "assistant" + if finishReason != "" { + choice.FinishReason = &finishReason + } + var openaiResponse openai.ChatCompletionsStreamResponse + openaiResponse.Object = "chat.completion.chunk" + openaiResponse.Choices = []openai.ChatCompletionsStreamResponseChoice{choice} + return &openaiResponse, response +} + +func ResponseCohere2OpenAI(cohereResponse *Response) *openai.TextResponse { + choice := openai.TextResponseChoice{ + Index: 0, + Message: model.Message{ + Role: "assistant", + Content: cohereResponse.Text, + Name: nil, + }, + FinishReason: stopReasonCohere2OpenAI(cohereResponse.FinishReason), + } + fullTextResponse := openai.TextResponse{ + Id: fmt.Sprintf("chatcmpl-%s", cohereResponse.ResponseID), + Model: "model", + Object: "chat.completion", + Created: helper.GetTimestamp(), + Choices: []openai.TextResponseChoice{choice}, + } + return &fullTextResponse +} + +func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) { + createdTime := helper.GetTimestamp() + scanner := bufio.NewScanner(resp.Body) + scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + if i := bytes.IndexByte(data, '\n'); i >= 0 { + return i + 1, data[0:i], nil + } + if atEOF { + return len(data), data, nil + } + return 0, nil, nil + }) + + dataChan := make(chan string) + stopChan := make(chan bool) + go func() { + for scanner.Scan() { + data := scanner.Text() + dataChan <- data + } + stopChan <- true + }() + common.SetEventStreamHeaders(c) + var usage model.Usage + c.Stream(func(w io.Writer) bool { + select { + case data := <-dataChan: + // some implementations may add \r at the end of data + data = strings.TrimSuffix(data, "\r") + var cohereResponse StreamResponse + err := json.Unmarshal([]byte(data), &cohereResponse) + if err != nil { + logger.SysError("error unmarshalling stream response: " + err.Error()) + return true + } + response, meta := StreamResponseCohere2OpenAI(&cohereResponse) + if meta != nil { + usage.PromptTokens += meta.Meta.Tokens.InputTokens + usage.CompletionTokens += meta.Meta.Tokens.OutputTokens + return true + } + if response == nil { + return true + } + response.Id = fmt.Sprintf("chatcmpl-%d", createdTime) + response.Model = c.GetString("original_model") + response.Created = createdTime + jsonStr, err := json.Marshal(response) + if err != nil { + logger.SysError("error marshalling stream response: " + err.Error()) + return true + } + c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonStr)}) + return true + case <-stopChan: + c.Render(-1, common.CustomEvent{Data: "data: [DONE]"}) + return false + } + }) + _ = resp.Body.Close() + return nil, &usage +} + +func Handler(c *gin.Context, resp *http.Response, promptTokens int, modelName string) (*model.ErrorWithStatusCode, *model.Usage) { + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return openai.ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil + } + err = resp.Body.Close() + if err != nil { + return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil + } + var cohereResponse Response + err = json.Unmarshal(responseBody, &cohereResponse) + if err != nil { + return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil + } + if cohereResponse.ResponseID == "" { + return &model.ErrorWithStatusCode{ + Error: model.Error{ + Message: cohereResponse.Message, + Type: cohereResponse.Message, + Param: "", + Code: resp.StatusCode, + }, + StatusCode: resp.StatusCode, + }, nil + } + fullTextResponse := ResponseCohere2OpenAI(&cohereResponse) + fullTextResponse.Model = modelName + usage := model.Usage{ + PromptTokens: cohereResponse.Meta.Tokens.InputTokens, + CompletionTokens: cohereResponse.Meta.Tokens.OutputTokens, + TotalTokens: cohereResponse.Meta.Tokens.InputTokens + cohereResponse.Meta.Tokens.OutputTokens, + } + fullTextResponse.Usage = usage + 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, &usage +} diff --git a/relay/adaptor/cohere/model.go b/relay/adaptor/cohere/model.go new file mode 100644 index 00000000..64fa9c94 --- /dev/null +++ b/relay/adaptor/cohere/model.go @@ -0,0 +1,147 @@ +package cohere + +type Request struct { + Message string `json:"message" required:"true"` + Model string `json:"model,omitempty"` // 默认值为"command-r" + Stream bool `json:"stream,omitempty"` // 默认值为false + Preamble string `json:"preamble,omitempty"` + ChatHistory []ChatMessage `json:"chat_history,omitempty"` + ConversationID string `json:"conversation_id,omitempty"` + PromptTruncation string `json:"prompt_truncation,omitempty"` // 默认值为"AUTO" + Connectors []Connector `json:"connectors,omitempty"` + Documents []Document `json:"documents,omitempty"` + Temperature float64 `json:"temperature,omitempty"` // 默认值为0.3 + MaxTokens int `json:"max_tokens,omitempty"` + MaxInputTokens int `json:"max_input_tokens,omitempty"` + K int `json:"k,omitempty"` // 默认值为0 + P float64 `json:"p,omitempty"` // 默认值为0.75 + Seed int `json:"seed,omitempty"` + StopSequences []string `json:"stop_sequences,omitempty"` + FrequencyPenalty float64 `json:"frequency_penalty,omitempty"` // 默认值为0.0 + PresencePenalty float64 `json:"presence_penalty,omitempty"` // 默认值为0.0 + Tools []Tool `json:"tools,omitempty"` + ToolResults []ToolResult `json:"tool_results,omitempty"` +} + +type ChatMessage struct { + Role string `json:"role" required:"true"` + Message string `json:"message" required:"true"` +} + +type Tool struct { + Name string `json:"name" required:"true"` + Description string `json:"description" required:"true"` + ParameterDefinitions map[string]ParameterSpec `json:"parameter_definitions"` +} + +type ParameterSpec struct { + Description string `json:"description"` + Type string `json:"type" required:"true"` + Required bool `json:"required"` +} + +type ToolResult struct { + Call ToolCall `json:"call"` + Outputs []map[string]interface{} `json:"outputs"` +} + +type ToolCall struct { + Name string `json:"name" required:"true"` + Parameters map[string]interface{} `json:"parameters" required:"true"` +} + +type StreamResponse struct { + IsFinished bool `json:"is_finished"` + EventType string `json:"event_type"` + GenerationID string `json:"generation_id,omitempty"` + SearchQueries []*SearchQuery `json:"search_queries,omitempty"` + SearchResults []*SearchResult `json:"search_results,omitempty"` + Documents []*Document `json:"documents,omitempty"` + Text string `json:"text,omitempty"` + Citations []*Citation `json:"citations,omitempty"` + Response *Response `json:"response,omitempty"` + FinishReason string `json:"finish_reason,omitempty"` +} + +type SearchQuery struct { + Text string `json:"text"` + GenerationID string `json:"generation_id"` +} + +type SearchResult struct { + SearchQuery *SearchQuery `json:"search_query"` + DocumentIDs []string `json:"document_ids"` + Connector *Connector `json:"connector"` +} + +type Connector struct { + ID string `json:"id"` +} + +type Document struct { + ID string `json:"id"` + Snippet string `json:"snippet"` + Timestamp string `json:"timestamp"` + Title string `json:"title"` + URL string `json:"url"` +} + +type Citation struct { + Start int `json:"start"` + End int `json:"end"` + Text string `json:"text"` + DocumentIDs []string `json:"document_ids"` +} + +type Response struct { + ResponseID string `json:"response_id"` + Text string `json:"text"` + GenerationID string `json:"generation_id"` + ChatHistory []*Message `json:"chat_history"` + FinishReason *string `json:"finish_reason"` + Meta Meta `json:"meta"` + Citations []*Citation `json:"citations"` + Documents []*Document `json:"documents"` + SearchResults []*SearchResult `json:"search_results"` + SearchQueries []*SearchQuery `json:"search_queries"` + Message string `json:"message"` +} + +type Message struct { + Role string `json:"role"` + Message string `json:"message"` +} + +type Version struct { + Version string `json:"version"` +} + +type Units struct { + InputTokens int `json:"input_tokens"` + OutputTokens int `json:"output_tokens"` +} + +type ChatEntry struct { + Role string `json:"role"` + Message string `json:"message"` +} + +type Meta struct { + APIVersion APIVersion `json:"api_version"` + BilledUnits BilledUnits `json:"billed_units"` + Tokens Usage `json:"tokens"` +} + +type APIVersion struct { + Version string `json:"version"` +} + +type BilledUnits struct { + InputTokens int `json:"input_tokens"` + OutputTokens int `json:"output_tokens"` +} + +type Usage struct { + InputTokens int `json:"input_tokens"` + OutputTokens int `json:"output_tokens"` +} diff --git a/relay/apitype/define.go b/relay/apitype/define.go index a3f2b98c..a1c8e6e1 100644 --- a/relay/apitype/define.go +++ b/relay/apitype/define.go @@ -14,6 +14,7 @@ const ( Ollama AwsClaude Coze + Cohere Dummy // this one is only for count, do not add any channel after this ) diff --git a/relay/billing/ratio/model.go b/relay/billing/ratio/model.go index b410df94..923d9c4f 100644 --- a/relay/billing/ratio/model.go +++ b/relay/billing/ratio/model.go @@ -2,8 +2,9 @@ package ratio import ( "encoding/json" - "github.com/songquanpeng/one-api/common/logger" "strings" + + "github.com/songquanpeng/one-api/common/logger" ) const ( @@ -162,6 +163,13 @@ var ModelRatio = map[string]float64{ "step-1v-32k": 0.024 * RMB, "step-1-32k": 0.024 * RMB, "step-1-200k": 0.15 * RMB, + // https://cohere.com/pricing + "command": 0.5, + "command-nightly": 0.5, + "command-light": 0.5, + "command-light-nightly": 0.5, + "command-r": 0.5 / 1000 * USD, + "command-r-plus ": 3.0 / 1000 * USD, } var CompletionRatio = map[string]float64{} @@ -284,6 +292,12 @@ func GetCompletionRatio(name string) float64 { return 2 case "llama3-70b-8192": return 0.79 / 0.59 + case "command", "command-light", "command-nightly", "command-light-nightly": + return 2 + case "command-r": + return 3 + case "command-r-plus": + return 5 } return 1 } diff --git a/relay/channeltype/define.go b/relay/channeltype/define.go index 6975e492..4b37e566 100644 --- a/relay/channeltype/define.go +++ b/relay/channeltype/define.go @@ -36,6 +36,7 @@ const ( StepFun AwsClaude Coze + Cohere Dummy ) diff --git a/relay/channeltype/helper.go b/relay/channeltype/helper.go index d249e208..42b77891 100644 --- a/relay/channeltype/helper.go +++ b/relay/channeltype/helper.go @@ -29,6 +29,8 @@ func ToAPIType(channelType int) int { apiType = apitype.AwsClaude case Coze: apiType = apitype.Coze + case Cohere: + apiType = apitype.Cohere } return apiType diff --git a/relay/channeltype/url.go b/relay/channeltype/url.go index 1f15dfe3..64fdcd0a 100644 --- a/relay/channeltype/url.go +++ b/relay/channeltype/url.go @@ -36,6 +36,7 @@ var ChannelBaseURLs = []string{ "https://api.stepfun.com", // 32 "", // 33 "https://api.coze.com", // 34 + "https://api.cohere.ai", //35 } func init() { diff --git a/web/default/src/constants/channel.constants.js b/web/default/src/constants/channel.constants.js index 0c1f4822..c21e19ed 100644 --- a/web/default/src/constants/channel.constants.js +++ b/web/default/src/constants/channel.constants.js @@ -20,6 +20,7 @@ export const CHANNEL_OPTIONS = [ { key: 31, text: '零一万物', value: 31, color: 'green' }, { key: 32, text: '阶跃星辰', value: 32, color: 'blue' }, { key: 34, text: 'Coze', value: 34, color: 'blue' }, + { key: 35, text: 'Cohere', value: 35, color: 'blue' }, { key: 8, text: '自定义渠道', value: 8, color: 'pink' }, { key: 22, text: '知识库:FastGPT', value: 22, color: 'blue' }, { key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple' }, From 0a650b85b44c1cbfc15263f47bdecb657bd90069 Mon Sep 17 00:00:00 2001 From: JustSong Date: Wed, 24 Apr 2024 22:08:47 +0800 Subject: [PATCH 103/121] chore: update berry --- web/berry/src/constants/ChannelConstants.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/web/berry/src/constants/ChannelConstants.js b/web/berry/src/constants/ChannelConstants.js index b74c58c7..7a96aa42 100644 --- a/web/berry/src/constants/ChannelConstants.js +++ b/web/berry/src/constants/ChannelConstants.js @@ -11,6 +11,12 @@ export const CHANNEL_OPTIONS = { value: 14, color: 'primary' }, + // 33: { + // key: 33, + // text: 'AWS Claude', + // value: 33, + // color: 'primary' + // }, 3: { key: 3, text: 'Azure OpenAI', @@ -113,6 +119,18 @@ export const CHANNEL_OPTIONS = { value: 32, color: 'primary' }, + // 34: { + // key: 34, + // text: 'Coze', + // value: 34, + // color: 'primary' + // }, + 35: { + key: 35, + text: 'Cohere', + value: 35, + color: 'primary' + }, 8: { key: 8, text: '自定义渠道', From da0842272ced7bee05a8f43c40a7a8c01b14d09a Mon Sep 17 00:00:00 2001 From: JustSong Date: Wed, 24 Apr 2024 22:19:58 +0800 Subject: [PATCH 104/121] fix: add model to response (close #1362) --- relay/adaptor/ollama/main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/relay/adaptor/ollama/main.go b/relay/adaptor/ollama/main.go index a7e4c058..b08eb0ca 100644 --- a/relay/adaptor/ollama/main.go +++ b/relay/adaptor/ollama/main.go @@ -53,6 +53,7 @@ func responseOllama2OpenAI(response *ChatResponse) *openai.TextResponse { } fullTextResponse := openai.TextResponse{ Id: fmt.Sprintf("chatcmpl-%s", random.GetUUID()), + Model: response.Model, Object: "chat.completion", Created: helper.GetTimestamp(), Choices: []openai.TextResponseChoice{choice}, From c3178720975ee0b2767bc3b0ad98cfc533f22b6d Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 26 Apr 2024 00:48:53 +0800 Subject: [PATCH 105/121] feat: support deepseek now --- README.md | 1 + relay/adaptor/deepseek/constants.go | 6 ++++++ relay/adaptor/openai/compatible.go | 4 ++++ relay/billing/ratio/model.go | 6 ++++++ relay/channeltype/define.go | 1 + relay/channeltype/url.go | 3 ++- web/berry/src/constants/ChannelConstants.js | 6 ++++++ web/default/src/constants/channel.constants.js | 1 + 8 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 relay/adaptor/deepseek/constants.go diff --git a/README.md b/README.md index 2f81c10d..01236a43 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 + [x] [阶跃星辰](https://platform.stepfun.com/) + [x] [Coze](https://www.coze.com/) + [x] [Cohere](https://cohere.com/) + + [x] [DeepSeek](https://www.deepseek.com/) 2. 支持配置镜像以及众多[第三方代理服务](https://iamazing.cn/page/openai-api-third-party-services)。 3. 支持通过**负载均衡**的方式访问多个渠道。 4. 支持 **stream 模式**,可以通过流式传输实现打字机效果。 diff --git a/relay/adaptor/deepseek/constants.go b/relay/adaptor/deepseek/constants.go new file mode 100644 index 00000000..ad840bc2 --- /dev/null +++ b/relay/adaptor/deepseek/constants.go @@ -0,0 +1,6 @@ +package deepseek + +var ModelList = []string{ + "deepseek-chat", + "deepseek-coder", +} diff --git a/relay/adaptor/openai/compatible.go b/relay/adaptor/openai/compatible.go index 200eac44..ae8449db 100644 --- a/relay/adaptor/openai/compatible.go +++ b/relay/adaptor/openai/compatible.go @@ -3,6 +3,7 @@ package openai import ( "github.com/songquanpeng/one-api/relay/adaptor/ai360" "github.com/songquanpeng/one-api/relay/adaptor/baichuan" + "github.com/songquanpeng/one-api/relay/adaptor/deepseek" "github.com/songquanpeng/one-api/relay/adaptor/groq" "github.com/songquanpeng/one-api/relay/adaptor/lingyiwanwu" "github.com/songquanpeng/one-api/relay/adaptor/minimax" @@ -22,6 +23,7 @@ var CompatibleChannels = []int{ channeltype.Groq, channeltype.LingYiWanWu, channeltype.StepFun, + channeltype.DeepSeek, } func GetCompatibleChannelMeta(channelType int) (string, []string) { @@ -44,6 +46,8 @@ func GetCompatibleChannelMeta(channelType int) (string, []string) { return "lingyiwanwu", lingyiwanwu.ModelList case channeltype.StepFun: return "stepfun", stepfun.ModelList + case channeltype.DeepSeek: + return "deepseek", deepseek.ModelList default: return "openai", ModelList } diff --git a/relay/billing/ratio/model.go b/relay/billing/ratio/model.go index 923d9c4f..c6fdf4b4 100644 --- a/relay/billing/ratio/model.go +++ b/relay/billing/ratio/model.go @@ -170,6 +170,9 @@ var ModelRatio = map[string]float64{ "command-light-nightly": 0.5, "command-r": 0.5 / 1000 * USD, "command-r-plus ": 3.0 / 1000 * USD, + // https://platform.deepseek.com/api-docs/pricing/ + "deepseek-chat": 1.0 / 1000 * RMB, + "deepseek-coder": 1.0 / 1000 * RMB, } var CompletionRatio = map[string]float64{} @@ -285,6 +288,9 @@ func GetCompletionRatio(name string) float64 { if strings.HasPrefix(name, "gemini-") { return 3 } + if strings.HasPrefix(name, "deepseek-") { + return 2 + } switch name { case "llama2-70b-4096": return 0.8 / 0.64 diff --git a/relay/channeltype/define.go b/relay/channeltype/define.go index 4b37e566..7f29afb3 100644 --- a/relay/channeltype/define.go +++ b/relay/channeltype/define.go @@ -37,6 +37,7 @@ const ( AwsClaude Coze Cohere + DeepSeek Dummy ) diff --git a/relay/channeltype/url.go b/relay/channeltype/url.go index 64fdcd0a..ea4dfb95 100644 --- a/relay/channeltype/url.go +++ b/relay/channeltype/url.go @@ -36,7 +36,8 @@ var ChannelBaseURLs = []string{ "https://api.stepfun.com", // 32 "", // 33 "https://api.coze.com", // 34 - "https://api.cohere.ai", //35 + "https://api.cohere.ai", // 35 + "https://api.deepseek.com", // 36 } func init() { diff --git a/web/berry/src/constants/ChannelConstants.js b/web/berry/src/constants/ChannelConstants.js index 7a96aa42..0ad94f04 100644 --- a/web/berry/src/constants/ChannelConstants.js +++ b/web/berry/src/constants/ChannelConstants.js @@ -131,6 +131,12 @@ export const CHANNEL_OPTIONS = { value: 35, color: 'primary' }, + 36: { + key: 36, + text: 'DeepSeek', + value: 36, + color: 'primary' + }, 8: { key: 8, text: '自定义渠道', diff --git a/web/default/src/constants/channel.constants.js b/web/default/src/constants/channel.constants.js index c21e19ed..ff124501 100644 --- a/web/default/src/constants/channel.constants.js +++ b/web/default/src/constants/channel.constants.js @@ -21,6 +21,7 @@ export const CHANNEL_OPTIONS = [ { key: 32, text: '阶跃星辰', value: 32, color: 'blue' }, { key: 34, text: 'Coze', value: 34, color: 'blue' }, { key: 35, text: 'Cohere', value: 35, color: 'blue' }, + { key: 36, text: 'DeepSeek', value: 36, color: 'black' }, { key: 8, text: '自定义渠道', value: 8, color: 'pink' }, { key: 22, text: '知识库:FastGPT', value: 22, color: 'blue' }, { key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple' }, From 9026ec7510f6dd7500e55567ce9de5f28f71b259 Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 26 Apr 2024 23:05:48 +0800 Subject: [PATCH 106/121] feat: support cloudflare now --- README.md | 1 + common/ctxkey/config.go | 13 -- common/ctxkey/key.go | 1 + common/helper/helper.go | 6 + common/helper/key.go | 5 + common/logger/constants.go | 4 - common/logger/logger.go | 2 +- controller/channel-test.go | 2 + controller/relay.go | 2 +- middleware/distributor.go | 26 +-- middleware/logger.go | 4 +- middleware/request-id.go | 7 +- middleware/utils.go | 2 +- model/channel.go | 18 ++- relay/adaptor.go | 3 + relay/adaptor/aiproxy/adaptor.go | 6 +- relay/adaptor/ali/adaptor.go | 8 +- relay/adaptor/aws/adapter.go | 16 +- relay/adaptor/aws/main.go | 27 +--- relay/adaptor/azure/helper.go | 15 -- relay/adaptor/cloudflare/adaptor.go | 66 ++++++++ relay/adaptor/cloudflare/constant.go | 36 +++++ relay/adaptor/cloudflare/main.go | 152 ++++++++++++++++++ relay/adaptor/cloudflare/model.go | 25 +++ relay/adaptor/coze/adaptor.go | 6 +- relay/adaptor/gemini/adaptor.go | 2 +- relay/adaptor/openai/adaptor.go | 4 +- relay/adaptor/openai/model.go | 2 +- relay/adaptor/xunfei/adaptor.go | 15 +- relay/adaptor/xunfei/main.go | 32 ++-- relay/apitype/define.go | 1 + relay/channeltype/define.go | 1 + relay/channeltype/helper.go | 2 + relay/channeltype/url.go | 1 + relay/controller/audio.go | 5 +- relay/controller/image.go | 1 + relay/controller/text.go | 1 + relay/meta/relay_meta.go | 35 ++-- .../src/constants/channel.constants.js | 73 ++++----- web/default/src/pages/Channel/EditChannel.js | 15 ++ 40 files changed, 464 insertions(+), 179 deletions(-) delete mode 100644 common/ctxkey/config.go create mode 100644 common/helper/key.go delete mode 100644 relay/adaptor/azure/helper.go create mode 100644 relay/adaptor/cloudflare/adaptor.go create mode 100644 relay/adaptor/cloudflare/constant.go create mode 100644 relay/adaptor/cloudflare/main.go create mode 100644 relay/adaptor/cloudflare/model.go diff --git a/README.md b/README.md index 01236a43..62834fb8 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 + [x] [Coze](https://www.coze.com/) + [x] [Cohere](https://cohere.com/) + [x] [DeepSeek](https://www.deepseek.com/) + + [x] [Cloudflare Workers AI](https://developers.cloudflare.com/workers-ai/) 2. 支持配置镜像以及众多[第三方代理服务](https://iamazing.cn/page/openai-api-third-party-services)。 3. 支持通过**负载均衡**的方式访问多个渠道。 4. 支持 **stream 模式**,可以通过流式传输实现打字机效果。 diff --git a/common/ctxkey/config.go b/common/ctxkey/config.go deleted file mode 100644 index 69e8a27a..00000000 --- a/common/ctxkey/config.go +++ /dev/null @@ -1,13 +0,0 @@ -package ctxkey - -const ( - ConfigPrefix = "cfg_" - - ConfigAPIVersion = ConfigPrefix + "api_version" - ConfigLibraryID = ConfigPrefix + "library_id" - ConfigPlugin = ConfigPrefix + "plugin" - ConfigSK = ConfigPrefix + "sk" - ConfigAK = ConfigPrefix + "ak" - ConfigRegion = ConfigPrefix + "region" - ConfigUserID = ConfigPrefix + "user_id" -) diff --git a/common/ctxkey/key.go b/common/ctxkey/key.go index 568cb095..6c640870 100644 --- a/common/ctxkey/key.go +++ b/common/ctxkey/key.go @@ -1,6 +1,7 @@ package ctxkey const ( + Config = "config" Id = "id" Username = "username" Role = "role" diff --git a/common/helper/helper.go b/common/helper/helper.go index cf2e1635..e06dfb6e 100644 --- a/common/helper/helper.go +++ b/common/helper/helper.go @@ -2,6 +2,7 @@ package helper import ( "fmt" + "github.com/gin-gonic/gin" "github.com/songquanpeng/one-api/common/random" "html/template" "log" @@ -105,6 +106,11 @@ func GenRequestID() string { return GetTimeString() + random.GetRandomNumberString(8) } +func GetResponseID(c *gin.Context) string { + logID := c.GetString(RequestIdKey) + return fmt.Sprintf("chatcmpl-%s", logID) +} + func Max(a int, b int) int { if a >= b { return a diff --git a/common/helper/key.go b/common/helper/key.go new file mode 100644 index 00000000..17aee2e0 --- /dev/null +++ b/common/helper/key.go @@ -0,0 +1,5 @@ +package helper + +const ( + RequestIdKey = "X-Oneapi-Request-Id" +) diff --git a/common/logger/constants.go b/common/logger/constants.go index 78d32062..49df31ec 100644 --- a/common/logger/constants.go +++ b/common/logger/constants.go @@ -1,7 +1,3 @@ package logger -const ( - RequestIdKey = "X-Oneapi-Request-Id" -) - var LogDir string diff --git a/common/logger/logger.go b/common/logger/logger.go index 858e33e2..c3dcd89d 100644 --- a/common/logger/logger.go +++ b/common/logger/logger.go @@ -87,7 +87,7 @@ func logHelper(ctx context.Context, level string, msg string) { if level == loggerINFO { writer = gin.DefaultWriter } - id := ctx.Value(RequestIdKey) + id := ctx.Value(helper.RequestIdKey) if id == nil { id = helper.GenRequestID() } diff --git a/controller/channel-test.go b/controller/channel-test.go index a84dc797..a9f03c45 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -57,6 +57,8 @@ func testChannel(channel *model.Channel) (err error, openaiErr *relaymodel.Error c.Request.Header.Set("Content-Type", "application/json") c.Set(ctxkey.Channel, channel.Type) c.Set(ctxkey.BaseURL, channel.GetBaseURL()) + cfg, _ := channel.LoadConfig() + c.Set(ctxkey.Config, cfg) middleware.SetupContextForSelectedChannel(c, channel, "") meta := meta.GetByContext(c) apiType := channeltype.ToAPIType(channel.Type) diff --git a/controller/relay.go b/controller/relay.go index 5fd22f85..aba4cd94 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -57,7 +57,7 @@ func Relay(c *gin.Context) { group := c.GetString(ctxkey.Group) originalModel := c.GetString(ctxkey.OriginalModel) go processChannelRelayError(ctx, channelId, channelName, bizErr) - requestId := c.GetString(logger.RequestIdKey) + requestId := c.GetString(helper.RequestIdKey) retryTimes := config.RetryTimes if !shouldRetry(c, bizErr.StatusCode) { logger.Errorf(ctx, "relay error happen, status code is %d, won't retry in this case", bizErr.StatusCode) diff --git a/middleware/distributor.go b/middleware/distributor.go index a4c34085..d0fd7ba5 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -65,21 +65,29 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode c.Set(ctxkey.OriginalModel, modelName) // for retry c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key)) c.Set(ctxkey.BaseURL, channel.GetBaseURL()) + cfg, _ := channel.LoadConfig() // this is for backward compatibility switch channel.Type { case channeltype.Azure: - c.Set(ctxkey.ConfigAPIVersion, channel.Other) + if cfg.APIVersion == "" { + cfg.APIVersion = channel.Other + } case channeltype.Xunfei: - c.Set(ctxkey.ConfigAPIVersion, channel.Other) + if cfg.APIVersion == "" { + cfg.APIVersion = channel.Other + } case channeltype.Gemini: - c.Set(ctxkey.ConfigAPIVersion, channel.Other) + if cfg.APIVersion == "" { + cfg.APIVersion = channel.Other + } case channeltype.AIProxyLibrary: - c.Set(ctxkey.ConfigLibraryID, channel.Other) + if cfg.LibraryID == "" { + cfg.LibraryID = channel.Other + } case channeltype.Ali: - c.Set(ctxkey.ConfigPlugin, channel.Other) - } - cfg, _ := channel.LoadConfig() - for k, v := range cfg { - c.Set(ctxkey.ConfigPrefix+k, v) + if cfg.Plugin == "" { + cfg.Plugin = channel.Other + } } + c.Set(ctxkey.Config, cfg) } diff --git a/middleware/logger.go b/middleware/logger.go index 6aae4f23..191364f8 100644 --- a/middleware/logger.go +++ b/middleware/logger.go @@ -3,14 +3,14 @@ package middleware import ( "fmt" "github.com/gin-gonic/gin" - "github.com/songquanpeng/one-api/common/logger" + "github.com/songquanpeng/one-api/common/helper" ) func SetUpLogger(server *gin.Engine) { server.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { var requestID string if param.Keys != nil { - requestID = param.Keys[logger.RequestIdKey].(string) + requestID = param.Keys[helper.RequestIdKey].(string) } return fmt.Sprintf("[GIN] %s | %s | %3d | %13v | %15s | %7s %s\n", param.TimeStamp.Format("2006/01/02 - 15:04:05"), diff --git a/middleware/request-id.go b/middleware/request-id.go index a4c49ddb..bef09e32 100644 --- a/middleware/request-id.go +++ b/middleware/request-id.go @@ -4,16 +4,15 @@ import ( "context" "github.com/gin-gonic/gin" "github.com/songquanpeng/one-api/common/helper" - "github.com/songquanpeng/one-api/common/logger" ) func RequestId() func(c *gin.Context) { return func(c *gin.Context) { id := helper.GenRequestID() - c.Set(logger.RequestIdKey, id) - ctx := context.WithValue(c.Request.Context(), logger.RequestIdKey, id) + c.Set(helper.RequestIdKey, id) + ctx := context.WithValue(c.Request.Context(), helper.RequestIdKey, id) c.Request = c.Request.WithContext(ctx) - c.Header(logger.RequestIdKey, id) + c.Header(helper.RequestIdKey, id) c.Next() } } diff --git a/middleware/utils.go b/middleware/utils.go index b65b018b..4d2f8092 100644 --- a/middleware/utils.go +++ b/middleware/utils.go @@ -12,7 +12,7 @@ import ( func abortWithMessage(c *gin.Context, statusCode int, message string) { c.JSON(statusCode, gin.H{ "error": gin.H{ - "message": helper.MessageWithRequestId(message, c.GetString(logger.RequestIdKey)), + "message": helper.MessageWithRequestId(message, c.GetString(helper.RequestIdKey)), "type": "one_api_error", }, }) diff --git a/model/channel.go b/model/channel.go index e667f7e7..ec52683e 100644 --- a/model/channel.go +++ b/model/channel.go @@ -38,6 +38,16 @@ type Channel struct { Config string `json:"config"` } +type ChannelConfig struct { + Region string `json:"region,omitempty"` + SK string `json:"sk,omitempty"` + AK string `json:"ak,omitempty"` + UserID string `json:"user_id,omitempty"` + APIVersion string `json:"api_version,omitempty"` + LibraryID string `json:"library_id,omitempty"` + Plugin string `json:"plugin,omitempty"` +} + func GetAllChannels(startIdx int, num int, scope string) ([]*Channel, error) { var channels []*Channel var err error @@ -161,14 +171,14 @@ func (channel *Channel) Delete() error { return err } -func (channel *Channel) LoadConfig() (map[string]string, error) { +func (channel *Channel) LoadConfig() (ChannelConfig, error) { + var cfg ChannelConfig if channel.Config == "" { - return nil, nil + return cfg, nil } - cfg := make(map[string]string) err := json.Unmarshal([]byte(channel.Config), &cfg) if err != nil { - return nil, err + return cfg, err } return cfg, nil } diff --git a/relay/adaptor.go b/relay/adaptor.go index 293b6d79..87021a04 100644 --- a/relay/adaptor.go +++ b/relay/adaptor.go @@ -7,6 +7,7 @@ import ( "github.com/songquanpeng/one-api/relay/adaptor/anthropic" "github.com/songquanpeng/one-api/relay/adaptor/aws" "github.com/songquanpeng/one-api/relay/adaptor/baidu" + "github.com/songquanpeng/one-api/relay/adaptor/cloudflare" "github.com/songquanpeng/one-api/relay/adaptor/cohere" "github.com/songquanpeng/one-api/relay/adaptor/coze" "github.com/songquanpeng/one-api/relay/adaptor/gemini" @@ -49,6 +50,8 @@ func GetAdaptor(apiType int) adaptor.Adaptor { return &coze.Adaptor{} case apitype.Cohere: return &cohere.Adaptor{} + case apitype.Cloudflare: + return &cloudflare.Adaptor{} } return nil } diff --git a/relay/adaptor/aiproxy/adaptor.go b/relay/adaptor/aiproxy/adaptor.go index a446f026..42d49c0a 100644 --- a/relay/adaptor/aiproxy/adaptor.go +++ b/relay/adaptor/aiproxy/adaptor.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "github.com/gin-gonic/gin" - "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/relay/adaptor" "github.com/songquanpeng/one-api/relay/meta" "github.com/songquanpeng/one-api/relay/model" @@ -13,10 +12,11 @@ import ( ) type Adaptor struct { + meta *meta.Meta } func (a *Adaptor) Init(meta *meta.Meta) { - + a.meta = meta } func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { @@ -34,7 +34,7 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.G return nil, errors.New("request is nil") } aiProxyLibraryRequest := ConvertRequest(*request) - aiProxyLibraryRequest.LibraryId = c.GetString(ctxkey.ConfigLibraryID) + aiProxyLibraryRequest.LibraryId = a.meta.Config.LibraryID return aiProxyLibraryRequest, nil } diff --git a/relay/adaptor/ali/adaptor.go b/relay/adaptor/ali/adaptor.go index 8e7220ff..4aa8a11a 100644 --- a/relay/adaptor/ali/adaptor.go +++ b/relay/adaptor/ali/adaptor.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "github.com/gin-gonic/gin" - "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/relay/adaptor" "github.com/songquanpeng/one-api/relay/meta" "github.com/songquanpeng/one-api/relay/model" @@ -16,10 +15,11 @@ import ( // https://help.aliyun.com/zh/dashscope/developer-reference/api-details type Adaptor struct { + meta *meta.Meta } func (a *Adaptor) Init(meta *meta.Meta) { - + a.meta = meta } func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { @@ -47,8 +47,8 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *me if meta.Mode == relaymode.ImagesGenerations { req.Header.Set("X-DashScope-Async", "enable") } - if c.GetString(ctxkey.ConfigPlugin) != "" { - req.Header.Set("X-DashScope-Plugin", c.GetString(ctxkey.ConfigPlugin)) + if a.meta.Config.Plugin != "" { + req.Header.Set("X-DashScope-Plugin", a.meta.Config.Plugin) } return nil } diff --git a/relay/adaptor/aws/adapter.go b/relay/adaptor/aws/adapter.go index 7f064efe..7245d3d9 100644 --- a/relay/adaptor/aws/adapter.go +++ b/relay/adaptor/aws/adapter.go @@ -1,6 +1,9 @@ package aws import ( + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/bedrockruntime" "github.com/songquanpeng/one-api/common/ctxkey" "io" "net/http" @@ -16,10 +19,16 @@ import ( var _ adaptor.Adaptor = new(Adaptor) type Adaptor struct { + meta *meta.Meta + awsClient *bedrockruntime.Client } func (a *Adaptor) Init(meta *meta.Meta) { - + a.meta = meta + a.awsClient = bedrockruntime.New(bedrockruntime.Options{ + Region: meta.Config.Region, + Credentials: aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(meta.Config.AK, meta.Config.SK, "")), + }) } func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { @@ -54,9 +63,9 @@ func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Read func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { if meta.IsStream { - err, usage = StreamHandler(c, resp) + err, usage = StreamHandler(c, a.awsClient) } else { - err, usage = Handler(c, resp, meta.PromptTokens, meta.ActualModelName) + err, usage = Handler(c, a.awsClient, meta.ActualModelName) } return } @@ -65,7 +74,6 @@ func (a *Adaptor) GetModelList() (models []string) { for n := range awsModelIDMap { models = append(models, n) } - return } diff --git a/relay/adaptor/aws/main.go b/relay/adaptor/aws/main.go index 3db38d22..0776f985 100644 --- a/relay/adaptor/aws/main.go +++ b/relay/adaptor/aws/main.go @@ -10,7 +10,6 @@ import ( "net/http" "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/bedrockruntime" "github.com/aws/aws-sdk-go-v2/service/bedrockruntime/types" "github.com/gin-gonic/gin" @@ -23,18 +22,6 @@ import ( relaymodel "github.com/songquanpeng/one-api/relay/model" ) -func newAwsClient(c *gin.Context) (*bedrockruntime.Client, error) { - ak := c.GetString(ctxkey.ConfigAK) - sk := c.GetString(ctxkey.ConfigSK) - region := c.GetString(ctxkey.ConfigRegion) - client := bedrockruntime.New(bedrockruntime.Options{ - Region: region, - Credentials: aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(ak, sk, "")), - }) - - return client, nil -} - func wrapErr(err error) *relaymodel.ErrorWithStatusCode { return &relaymodel.ErrorWithStatusCode{ StatusCode: http.StatusInternalServerError, @@ -62,12 +49,7 @@ func awsModelID(requestModel string) (string, error) { return "", errors.Errorf("model %s not found", requestModel) } -func Handler(c *gin.Context, resp *http.Response, promptTokens int, modelName string) (*relaymodel.ErrorWithStatusCode, *relaymodel.Usage) { - awsCli, err := newAwsClient(c) - if err != nil { - return wrapErr(errors.Wrap(err, "newAwsClient")), nil - } - +func Handler(c *gin.Context, awsCli *bedrockruntime.Client, modelName string) (*relaymodel.ErrorWithStatusCode, *relaymodel.Usage) { awsModelId, err := awsModelID(c.GetString(ctxkey.RequestModel)) if err != nil { return wrapErr(errors.Wrap(err, "awsModelID")), nil @@ -120,13 +102,8 @@ func Handler(c *gin.Context, resp *http.Response, promptTokens int, modelName st return nil, &usage } -func StreamHandler(c *gin.Context, resp *http.Response) (*relaymodel.ErrorWithStatusCode, *relaymodel.Usage) { +func StreamHandler(c *gin.Context, awsCli *bedrockruntime.Client) (*relaymodel.ErrorWithStatusCode, *relaymodel.Usage) { createdTime := helper.GetTimestamp() - awsCli, err := newAwsClient(c) - if err != nil { - return wrapErr(errors.Wrap(err, "newAwsClient")), nil - } - awsModelId, err := awsModelID(c.GetString(ctxkey.RequestModel)) if err != nil { return wrapErr(errors.Wrap(err, "awsModelID")), nil diff --git a/relay/adaptor/azure/helper.go b/relay/adaptor/azure/helper.go deleted file mode 100644 index 26443bc4..00000000 --- a/relay/adaptor/azure/helper.go +++ /dev/null @@ -1,15 +0,0 @@ -package azure - -import ( - "github.com/gin-gonic/gin" - "github.com/songquanpeng/one-api/common/ctxkey" -) - -func GetAPIVersion(c *gin.Context) string { - query := c.Request.URL.Query() - apiVersion := query.Get("api-version") - if apiVersion == "" { - apiVersion = c.GetString(ctxkey.ConfigAPIVersion) - } - return apiVersion -} diff --git a/relay/adaptor/cloudflare/adaptor.go b/relay/adaptor/cloudflare/adaptor.go new file mode 100644 index 00000000..6ff6b0d3 --- /dev/null +++ b/relay/adaptor/cloudflare/adaptor.go @@ -0,0 +1,66 @@ +package cloudflare + +import ( + "errors" + "fmt" + "io" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/songquanpeng/one-api/relay/adaptor" + "github.com/songquanpeng/one-api/relay/meta" + "github.com/songquanpeng/one-api/relay/model" +) + +type Adaptor struct { + meta *meta.Meta +} + +// ConvertImageRequest implements adaptor.Adaptor. +func (*Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { + return nil, errors.New("not implemented") +} + +// ConvertImageRequest implements adaptor.Adaptor. + +func (a *Adaptor) Init(meta *meta.Meta) { + a.meta = meta +} + +func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { + return fmt.Sprintf("%s/client/v4/accounts/%s/ai/run/%s", meta.BaseURL, meta.Config.UserID, meta.ActualModelName), nil +} + +func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { + adaptor.SetupCommonRequestHeader(c, req, meta) + req.Header.Set("Authorization", "Bearer "+meta.APIKey) + return nil +} + +func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) { + if request == nil { + return nil, errors.New("request is nil") + } + return ConvertRequest(*request), nil +} + +func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { + return adaptor.DoRequestHelper(a, c, meta, requestBody) +} + +func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { + if meta.IsStream { + err, usage = StreamHandler(c, resp, meta.PromptTokens, meta.ActualModelName) + } else { + err, usage = Handler(c, resp, meta.PromptTokens, meta.ActualModelName) + } + return +} + +func (a *Adaptor) GetModelList() []string { + return ModelList +} + +func (a *Adaptor) GetChannelName() string { + return "cloudflare" +} diff --git a/relay/adaptor/cloudflare/constant.go b/relay/adaptor/cloudflare/constant.go new file mode 100644 index 00000000..dee79a76 --- /dev/null +++ b/relay/adaptor/cloudflare/constant.go @@ -0,0 +1,36 @@ +package cloudflare + +var ModelList = []string{ + "@cf/meta/llama-2-7b-chat-fp16", + "@cf/meta/llama-2-7b-chat-int8", + "@cf/mistral/mistral-7b-instruct-v0.1", + "@hf/thebloke/deepseek-coder-6.7b-base-awq", + "@hf/thebloke/deepseek-coder-6.7b-instruct-awq", + "@cf/deepseek-ai/deepseek-math-7b-base", + "@cf/deepseek-ai/deepseek-math-7b-instruct", + "@cf/thebloke/discolm-german-7b-v1-awq", + "@cf/tiiuae/falcon-7b-instruct", + "@cf/google/gemma-2b-it-lora", + "@hf/google/gemma-7b-it", + "@cf/google/gemma-7b-it-lora", + "@hf/nousresearch/hermes-2-pro-mistral-7b", + "@hf/thebloke/llama-2-13b-chat-awq", + "@cf/meta-llama/llama-2-7b-chat-hf-lora", + "@cf/meta/llama-3-8b-instruct", + "@hf/thebloke/llamaguard-7b-awq", + "@hf/thebloke/mistral-7b-instruct-v0.1-awq", + "@hf/mistralai/mistral-7b-instruct-v0.2", + "@cf/mistral/mistral-7b-instruct-v0.2-lora", + "@hf/thebloke/neural-chat-7b-v3-1-awq", + "@cf/openchat/openchat-3.5-0106", + "@hf/thebloke/openhermes-2.5-mistral-7b-awq", + "@cf/microsoft/phi-2", + "@cf/qwen/qwen1.5-0.5b-chat", + "@cf/qwen/qwen1.5-1.8b-chat", + "@cf/qwen/qwen1.5-14b-chat-awq", + "@cf/qwen/qwen1.5-7b-chat-awq", + "@cf/defog/sqlcoder-7b-2", + "@hf/nexusflow/starling-lm-7b-beta", + "@cf/tinyllama/tinyllama-1.1b-chat-v1.0", + "@hf/thebloke/zephyr-7b-beta-awq", +} diff --git a/relay/adaptor/cloudflare/main.go b/relay/adaptor/cloudflare/main.go new file mode 100644 index 00000000..e85bbc25 --- /dev/null +++ b/relay/adaptor/cloudflare/main.go @@ -0,0 +1,152 @@ +package cloudflare + +import ( + "bufio" + "bytes" + "encoding/json" + "io" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/songquanpeng/one-api/common" + "github.com/songquanpeng/one-api/common/helper" + "github.com/songquanpeng/one-api/common/logger" + "github.com/songquanpeng/one-api/relay/adaptor/openai" + "github.com/songquanpeng/one-api/relay/model" +) + +func ConvertRequest(textRequest model.GeneralOpenAIRequest) *Request { + lastMessage := textRequest.Messages[len(textRequest.Messages)-1] + return &Request{ + MaxTokens: textRequest.MaxTokens, + Prompt: lastMessage.StringContent(), + Stream: textRequest.Stream, + Temperature: textRequest.Temperature, + } +} + +func ResponseCloudflare2OpenAI(cloudflareResponse *Response) *openai.TextResponse { + choice := openai.TextResponseChoice{ + Index: 0, + Message: model.Message{ + Role: "assistant", + Content: cloudflareResponse.Result.Response, + }, + FinishReason: "stop", + } + fullTextResponse := openai.TextResponse{ + Object: "chat.completion", + Created: helper.GetTimestamp(), + Choices: []openai.TextResponseChoice{choice}, + } + return &fullTextResponse +} + +func StreamResponseCloudflare2OpenAI(cloudflareResponse *StreamResponse) *openai.ChatCompletionsStreamResponse { + var choice openai.ChatCompletionsStreamResponseChoice + choice.Delta.Content = cloudflareResponse.Response + choice.Delta.Role = "assistant" + openaiResponse := openai.ChatCompletionsStreamResponse{ + Object: "chat.completion.chunk", + Choices: []openai.ChatCompletionsStreamResponseChoice{choice}, + Created: helper.GetTimestamp(), + } + return &openaiResponse +} + +func StreamHandler(c *gin.Context, resp *http.Response, promptTokens int, modelName string) (*model.ErrorWithStatusCode, *model.Usage) { + scanner := bufio.NewScanner(resp.Body) + scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + if i := bytes.IndexByte(data, '\n'); i >= 0 { + return i + 1, data[0:i], nil + } + if atEOF { + return len(data), data, nil + } + return 0, nil, nil + }) + + dataChan := make(chan string) + stopChan := make(chan bool) + go func() { + for scanner.Scan() { + data := scanner.Text() + if len(data) < len("data: ") { + continue + } + data = strings.TrimPrefix(data, "data: ") + dataChan <- data + } + stopChan <- true + }() + common.SetEventStreamHeaders(c) + id := helper.GetResponseID(c) + responseModel := c.GetString("original_model") + var responseText string + c.Stream(func(w io.Writer) bool { + select { + case data := <-dataChan: + // some implementations may add \r at the end of data + data = strings.TrimSuffix(data, "\r") + var cloudflareResponse StreamResponse + err := json.Unmarshal([]byte(data), &cloudflareResponse) + if err != nil { + logger.SysError("error unmarshalling stream response: " + err.Error()) + return true + } + response := StreamResponseCloudflare2OpenAI(&cloudflareResponse) + if response == nil { + return true + } + responseText += cloudflareResponse.Response + response.Id = id + response.Model = responseModel + jsonStr, err := json.Marshal(response) + if err != nil { + logger.SysError("error marshalling stream response: " + err.Error()) + return true + } + c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonStr)}) + return true + case <-stopChan: + c.Render(-1, common.CustomEvent{Data: "data: [DONE]"}) + return false + } + }) + _ = resp.Body.Close() + usage := openai.ResponseText2Usage(responseText, responseModel, promptTokens) + return nil, usage +} + +func Handler(c *gin.Context, resp *http.Response, promptTokens int, modelName string) (*model.ErrorWithStatusCode, *model.Usage) { + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return openai.ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil + } + err = resp.Body.Close() + if err != nil { + return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil + } + var cloudflareResponse Response + err = json.Unmarshal(responseBody, &cloudflareResponse) + if err != nil { + return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil + } + fullTextResponse := ResponseCloudflare2OpenAI(&cloudflareResponse) + fullTextResponse.Model = modelName + usage := openai.ResponseText2Usage(cloudflareResponse.Result.Response, modelName, promptTokens) + fullTextResponse.Usage = *usage + fullTextResponse.Id = helper.GetResponseID(c) + 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, usage +} diff --git a/relay/adaptor/cloudflare/model.go b/relay/adaptor/cloudflare/model.go new file mode 100644 index 00000000..0664ecd1 --- /dev/null +++ b/relay/adaptor/cloudflare/model.go @@ -0,0 +1,25 @@ +package cloudflare + +type Request struct { + Lora string `json:"lora,omitempty"` + MaxTokens int `json:"max_tokens,omitempty"` + Prompt string `json:"prompt,omitempty"` + Raw bool `json:"raw,omitempty"` + Stream bool `json:"stream,omitempty"` + Temperature float64 `json:"temperature,omitempty"` +} + +type Result struct { + Response string `json:"response"` +} + +type Response struct { + Result Result `json:"result"` + Success bool `json:"success"` + Errors []string `json:"errors"` + Messages []string `json:"messages"` +} + +type StreamResponse struct { + Response string `json:"response"` +} diff --git a/relay/adaptor/coze/adaptor.go b/relay/adaptor/coze/adaptor.go index 49979ef6..44f560e8 100644 --- a/relay/adaptor/coze/adaptor.go +++ b/relay/adaptor/coze/adaptor.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "github.com/gin-gonic/gin" - "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/relay/adaptor" "github.com/songquanpeng/one-api/relay/adaptor/openai" "github.com/songquanpeng/one-api/relay/meta" @@ -14,10 +13,11 @@ import ( ) type Adaptor struct { + meta *meta.Meta } func (a *Adaptor) Init(meta *meta.Meta) { - + a.meta = meta } func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { @@ -34,7 +34,7 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.G if request == nil { return nil, errors.New("request is nil") } - request.User = c.GetString(ctxkey.ConfigUserID) + request.User = a.meta.Config.UserID return ConvertRequest(*request), nil } diff --git a/relay/adaptor/gemini/adaptor.go b/relay/adaptor/gemini/adaptor.go index 6a2867e4..839e45d6 100644 --- a/relay/adaptor/gemini/adaptor.go +++ b/relay/adaptor/gemini/adaptor.go @@ -22,7 +22,7 @@ func (a *Adaptor) Init(meta *meta.Meta) { } func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { - version := helper.AssignOrDefault(meta.APIVersion, config.GeminiVersion) + version := helper.AssignOrDefault(meta.Config.APIVersion, config.GeminiVersion) action := "generateContent" if meta.IsStream { action = "streamGenerateContent" diff --git a/relay/adaptor/openai/adaptor.go b/relay/adaptor/openai/adaptor.go index 4bb2384e..57940558 100644 --- a/relay/adaptor/openai/adaptor.go +++ b/relay/adaptor/openai/adaptor.go @@ -29,13 +29,13 @@ func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { if meta.Mode == relaymode.ImagesGenerations { // https://learn.microsoft.com/en-us/azure/ai-services/openai/dall-e-quickstart?tabs=dalle3%2Ccommand-line&pivots=rest-api // 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, meta.ActualModelName, meta.APIVersion) + fullRequestURL := fmt.Sprintf("%s/openai/deployments/%s/images/generations?api-version=%s", meta.BaseURL, meta.ActualModelName, meta.Config.APIVersion) return fullRequestURL, nil } // https://learn.microsoft.com/en-us/azure/cognitive-services/openai/chatgpt-quickstart?pivots=rest-api&tabs=command-line#rest-api requestURL := strings.Split(meta.RequestURLPath, "?")[0] - requestURL = fmt.Sprintf("%s?api-version=%s", requestURL, meta.APIVersion) + requestURL = fmt.Sprintf("%s?api-version=%s", requestURL, meta.Config.APIVersion) task := strings.TrimPrefix(requestURL, "/v1/") model_ := meta.ActualModelName model_ = strings.Replace(model_, ".", "", -1) diff --git a/relay/adaptor/openai/model.go b/relay/adaptor/openai/model.go index ce252ff6..4c974de4 100644 --- a/relay/adaptor/openai/model.go +++ b/relay/adaptor/openai/model.go @@ -134,7 +134,7 @@ type ChatCompletionsStreamResponse struct { Created int64 `json:"created"` Model string `json:"model"` Choices []ChatCompletionsStreamResponseChoice `json:"choices"` - Usage *model.Usage `json:"usage"` + Usage *model.Usage `json:"usage,omitempty"` } type CompletionsStreamResponse struct { diff --git a/relay/adaptor/xunfei/adaptor.go b/relay/adaptor/xunfei/adaptor.go index edcd719f..3af97831 100644 --- a/relay/adaptor/xunfei/adaptor.go +++ b/relay/adaptor/xunfei/adaptor.go @@ -14,10 +14,11 @@ import ( type Adaptor struct { request *model.GeneralOpenAIRequest + meta *meta.Meta } func (a *Adaptor) Init(meta *meta.Meta) { - + a.meta = meta } func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { @@ -26,6 +27,14 @@ func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { adaptor.SetupCommonRequestHeader(c, req, meta) + version := parseAPIVersionByModelName(meta.ActualModelName) + if version == "" { + version = a.meta.Config.APIVersion + } + if version == "" { + version = "v1.1" + } + a.meta.Config.APIVersion = version // check DoResponse for auth part return nil } @@ -61,9 +70,9 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Met return nil, openai.ErrorWrapper(errors.New("request is nil"), "request_is_nil", http.StatusBadRequest) } if meta.IsStream { - err, usage = StreamHandler(c, *a.request, splits[0], splits[1], splits[2]) + err, usage = StreamHandler(c, meta, *a.request, splits[0], splits[1], splits[2]) } else { - err, usage = Handler(c, *a.request, splits[0], splits[1], splits[2]) + err, usage = Handler(c, meta, *a.request, splits[0], splits[1], splits[2]) } return } diff --git a/relay/adaptor/xunfei/main.go b/relay/adaptor/xunfei/main.go index 70a926fd..c3e768b7 100644 --- a/relay/adaptor/xunfei/main.go +++ b/relay/adaptor/xunfei/main.go @@ -9,12 +9,12 @@ import ( "github.com/gin-gonic/gin" "github.com/gorilla/websocket" "github.com/songquanpeng/one-api/common" - "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/common/helper" "github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/common/random" "github.com/songquanpeng/one-api/relay/adaptor/openai" "github.com/songquanpeng/one-api/relay/constant" + "github.com/songquanpeng/one-api/relay/meta" "github.com/songquanpeng/one-api/relay/model" "io" "net/http" @@ -149,8 +149,8 @@ func buildXunfeiAuthUrl(hostUrl string, apiKey, apiSecret string) string { return callUrl } -func StreamHandler(c *gin.Context, textRequest model.GeneralOpenAIRequest, appId string, apiSecret string, apiKey string) (*model.ErrorWithStatusCode, *model.Usage) { - domain, authUrl := getXunfeiAuthUrl(c, apiKey, apiSecret, textRequest.Model) +func StreamHandler(c *gin.Context, meta *meta.Meta, textRequest model.GeneralOpenAIRequest, appId string, apiSecret string, apiKey string) (*model.ErrorWithStatusCode, *model.Usage) { + domain, authUrl := getXunfeiAuthUrl(meta.Config.APIVersion, apiKey, apiSecret) dataChan, stopChan, err := xunfeiMakeRequest(textRequest, domain, authUrl, appId) if err != nil { return openai.ErrorWrapper(err, "xunfei_request_failed", http.StatusInternalServerError), nil @@ -179,8 +179,8 @@ func StreamHandler(c *gin.Context, textRequest model.GeneralOpenAIRequest, appId return nil, &usage } -func Handler(c *gin.Context, textRequest model.GeneralOpenAIRequest, appId string, apiSecret string, apiKey string) (*model.ErrorWithStatusCode, *model.Usage) { - domain, authUrl := getXunfeiAuthUrl(c, apiKey, apiSecret, textRequest.Model) +func Handler(c *gin.Context, meta *meta.Meta, textRequest model.GeneralOpenAIRequest, appId string, apiSecret string, apiKey string) (*model.ErrorWithStatusCode, *model.Usage) { + domain, authUrl := getXunfeiAuthUrl(meta.Config.APIVersion, apiKey, apiSecret) dataChan, stopChan, err := xunfeiMakeRequest(textRequest, domain, authUrl, appId) if err != nil { return openai.ErrorWrapper(err, "xunfei_request_failed", http.StatusInternalServerError), nil @@ -268,25 +268,12 @@ func xunfeiMakeRequest(textRequest model.GeneralOpenAIRequest, domain, authUrl, return dataChan, stopChan, nil } -func getAPIVersion(c *gin.Context, modelName string) string { - query := c.Request.URL.Query() - apiVersion := query.Get("api-version") - if apiVersion != "" { - return apiVersion - } +func parseAPIVersionByModelName(modelName string) string { parts := strings.Split(modelName, "-") if len(parts) == 2 { - apiVersion = parts[1] - return apiVersion - + return parts[1] } - apiVersion = c.GetString(ctxkey.ConfigAPIVersion) - if apiVersion != "" { - return apiVersion - } - apiVersion = "v1.1" - logger.SysLog("api_version not found, using default: " + apiVersion) - return apiVersion + return "" } // https://www.xfyun.cn/doc/spark/Web.html#_1-%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E @@ -304,8 +291,7 @@ func apiVersion2domain(apiVersion string) string { return "general" + apiVersion } -func getXunfeiAuthUrl(c *gin.Context, apiKey string, apiSecret string, modelName string) (string, string) { - apiVersion := getAPIVersion(c, modelName) +func getXunfeiAuthUrl(apiVersion string, apiKey string, apiSecret string) (string, string) { domain := apiVersion2domain(apiVersion) authUrl := buildXunfeiAuthUrl(fmt.Sprintf("wss://spark-api.xf-yun.com/%s/chat", apiVersion), apiKey, apiSecret) return domain, authUrl diff --git a/relay/apitype/define.go b/relay/apitype/define.go index a1c8e6e1..e38eff7e 100644 --- a/relay/apitype/define.go +++ b/relay/apitype/define.go @@ -15,6 +15,7 @@ const ( AwsClaude Coze Cohere + Cloudflare Dummy // this one is only for count, do not add any channel after this ) diff --git a/relay/channeltype/define.go b/relay/channeltype/define.go index 7f29afb3..3aa585a9 100644 --- a/relay/channeltype/define.go +++ b/relay/channeltype/define.go @@ -38,6 +38,7 @@ const ( Coze Cohere DeepSeek + Cloudflare Dummy ) diff --git a/relay/channeltype/helper.go b/relay/channeltype/helper.go index 42b77891..a608c80e 100644 --- a/relay/channeltype/helper.go +++ b/relay/channeltype/helper.go @@ -31,6 +31,8 @@ func ToAPIType(channelType int) int { apiType = apitype.Coze case Cohere: apiType = apitype.Cohere + case Cloudflare: + apiType = apitype.Cloudflare } return apiType diff --git a/relay/channeltype/url.go b/relay/channeltype/url.go index ea4dfb95..657b677e 100644 --- a/relay/channeltype/url.go +++ b/relay/channeltype/url.go @@ -38,6 +38,7 @@ var ChannelBaseURLs = []string{ "https://api.coze.com", // 34 "https://api.cohere.ai", // 35 "https://api.deepseek.com", // 36 + "https://api.cloudflare.com", // 37 } func init() { diff --git a/relay/controller/audio.go b/relay/controller/audio.go index db543318..15e74290 100644 --- a/relay/controller/audio.go +++ b/relay/controller/audio.go @@ -13,12 +13,12 @@ import ( "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/model" - "github.com/songquanpeng/one-api/relay/adaptor/azure" "github.com/songquanpeng/one-api/relay/adaptor/openai" "github.com/songquanpeng/one-api/relay/billing" billingratio "github.com/songquanpeng/one-api/relay/billing/ratio" "github.com/songquanpeng/one-api/relay/channeltype" "github.com/songquanpeng/one-api/relay/client" + "github.com/songquanpeng/one-api/relay/meta" relaymodel "github.com/songquanpeng/one-api/relay/model" "github.com/songquanpeng/one-api/relay/relaymode" "io" @@ -28,6 +28,7 @@ import ( func RelayAudioHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatusCode { ctx := c.Request.Context() + meta := meta.GetByContext(c) audioModel := "whisper-1" tokenId := c.GetInt(ctxkey.TokenId) @@ -128,7 +129,7 @@ func RelayAudioHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus fullRequestURL := openai.GetFullRequestURL(baseURL, requestURL, channelType) if channelType == channeltype.Azure { - apiVersion := azure.GetAPIVersion(c) + apiVersion := meta.Config.APIVersion if relayMode == relaymode.AudioTranscription { // 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) diff --git a/relay/controller/image.go b/relay/controller/image.go index 216e4700..6620bef5 100644 --- a/relay/controller/image.go +++ b/relay/controller/image.go @@ -70,6 +70,7 @@ func RelayImageHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus if adaptor == nil { return openai.ErrorWrapper(fmt.Errorf("invalid api type: %d", meta.APIType), "invalid_api_type", http.StatusBadRequest) } + adaptor.Init(meta) switch meta.ChannelType { case channeltype.Ali: diff --git a/relay/controller/text.go b/relay/controller/text.go index 23e94234..9bfd3e76 100644 --- a/relay/controller/text.go +++ b/relay/controller/text.go @@ -53,6 +53,7 @@ func RelayTextHelper(c *gin.Context) *model.ErrorWithStatusCode { if adaptor == nil { return openai.ErrorWrapper(fmt.Errorf("invalid api type: %d", meta.APIType), "invalid_api_type", http.StatusBadRequest) } + adaptor.Init(meta) // get request body var requestBody io.Reader diff --git a/relay/meta/relay_meta.go b/relay/meta/relay_meta.go index 0e8f72fe..9714ebb5 100644 --- a/relay/meta/relay_meta.go +++ b/relay/meta/relay_meta.go @@ -3,7 +3,7 @@ package meta import ( "github.com/gin-gonic/gin" "github.com/songquanpeng/one-api/common/ctxkey" - "github.com/songquanpeng/one-api/relay/adaptor/azure" + "github.com/songquanpeng/one-api/model" "github.com/songquanpeng/one-api/relay/channeltype" "github.com/songquanpeng/one-api/relay/relaymode" "strings" @@ -19,10 +19,9 @@ type Meta struct { Group string ModelMapping map[string]string BaseURL string - APIVersion string APIKey string APIType int - Config map[string]string + Config model.ChannelConfig IsStream bool OriginModelName string ActualModelName string @@ -32,22 +31,22 @@ type Meta struct { func GetByContext(c *gin.Context) *Meta { meta := Meta{ - Mode: relaymode.GetByPath(c.Request.URL.Path), - ChannelType: c.GetInt(ctxkey.Channel), - ChannelId: c.GetInt(ctxkey.ChannelId), - TokenId: c.GetInt(ctxkey.TokenId), - TokenName: c.GetString(ctxkey.TokenName), - UserId: c.GetInt(ctxkey.Id), - Group: c.GetString(ctxkey.Group), - ModelMapping: c.GetStringMapString(ctxkey.ModelMapping), - BaseURL: c.GetString(ctxkey.BaseURL), - APIVersion: c.GetString(ctxkey.ConfigAPIVersion), - APIKey: strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "), - Config: nil, - RequestURLPath: c.Request.URL.String(), + Mode: relaymode.GetByPath(c.Request.URL.Path), + ChannelType: c.GetInt(ctxkey.Channel), + ChannelId: c.GetInt(ctxkey.ChannelId), + TokenId: c.GetInt(ctxkey.TokenId), + TokenName: c.GetString(ctxkey.TokenName), + UserId: c.GetInt(ctxkey.Id), + Group: c.GetString(ctxkey.Group), + ModelMapping: c.GetStringMapString(ctxkey.ModelMapping), + OriginModelName: c.GetString(ctxkey.RequestModel), + BaseURL: c.GetString(ctxkey.BaseURL), + APIKey: strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "), + RequestURLPath: c.Request.URL.String(), } - if meta.ChannelType == channeltype.Azure { - meta.APIVersion = azure.GetAPIVersion(c) + cfg, ok := c.Get(ctxkey.Config) + if ok { + meta.Config = cfg.(model.ChannelConfig) } if meta.BaseURL == "" { meta.BaseURL = channeltype.ChannelBaseURLs[meta.ChannelType] diff --git a/web/default/src/constants/channel.constants.js b/web/default/src/constants/channel.constants.js index ff124501..a689ef27 100644 --- a/web/default/src/constants/channel.constants.js +++ b/web/default/src/constants/channel.constants.js @@ -1,38 +1,39 @@ export const CHANNEL_OPTIONS = [ - { key: 1, text: 'OpenAI', value: 1, color: 'green' }, - { key: 14, text: 'Anthropic Claude', value: 14, color: 'black' }, - { key: 33, text: 'AWS Claude', value: 33, color: 'black' }, - { key: 3, text: 'Azure OpenAI', value: 3, color: 'olive' }, - { key: 11, text: 'Google PaLM2', value: 11, color: 'orange' }, - { key: 24, text: 'Google Gemini', value: 24, color: 'orange' }, - { key: 28, text: 'Mistral AI', value: 28, color: 'orange' }, - { key: 15, text: '百度文心千帆', value: 15, color: 'blue' }, - { key: 17, text: '阿里通义千问', value: 17, color: 'orange' }, - { key: 18, text: '讯飞星火认知', value: 18, color: 'blue' }, - { key: 16, text: '智谱 ChatGLM', value: 16, color: 'violet' }, - { key: 19, text: '360 智脑', value: 19, color: 'blue' }, - { key: 25, text: 'Moonshot AI', value: 25, color: 'black' }, - { key: 23, text: '腾讯混元', value: 23, color: 'teal' }, - { key: 26, text: '百川大模型', value: 26, color: 'orange' }, - { key: 27, text: 'MiniMax', value: 27, color: 'red' }, - { key: 29, text: 'Groq', value: 29, color: 'orange' }, - { key: 30, text: 'Ollama', value: 30, color: 'black' }, - { key: 31, text: '零一万物', value: 31, color: 'green' }, - { key: 32, text: '阶跃星辰', value: 32, color: 'blue' }, - { key: 34, text: 'Coze', value: 34, color: 'blue' }, - { key: 35, text: 'Cohere', value: 35, color: 'blue' }, - { key: 36, text: 'DeepSeek', value: 36, color: 'black' }, - { key: 8, text: '自定义渠道', value: 8, color: 'pink' }, - { key: 22, text: '知识库:FastGPT', value: 22, color: 'blue' }, - { key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple' }, - { key: 20, text: '代理:OpenRouter', value: 20, color: 'black' }, - { key: 2, text: '代理:API2D', value: 2, color: 'blue' }, - { key: 5, text: '代理:OpenAI-SB', value: 5, color: 'brown' }, - { key: 7, text: '代理:OhMyGPT', value: 7, color: 'purple' }, - { key: 10, text: '代理:AI Proxy', value: 10, color: 'purple' }, - { key: 4, text: '代理:CloseAI', value: 4, color: 'teal' }, - { key: 6, text: '代理:OpenAI Max', value: 6, color: 'violet' }, - { key: 9, text: '代理:AI.LS', value: 9, color: 'yellow' }, - { key: 12, text: '代理:API2GPT', value: 12, color: 'blue' }, - { key: 13, text: '代理:AIGC2D', value: 13, color: 'purple' } + {key: 1, text: 'OpenAI', value: 1, color: 'green'}, + {key: 14, text: 'Anthropic Claude', value: 14, color: 'black'}, + {key: 33, text: 'AWS Claude', value: 33, color: 'black'}, + {key: 3, text: 'Azure OpenAI', value: 3, color: 'olive'}, + {key: 11, text: 'Google PaLM2', value: 11, color: 'orange'}, + {key: 24, text: 'Google Gemini', value: 24, color: 'orange'}, + {key: 28, text: 'Mistral AI', value: 28, color: 'orange'}, + {key: 15, text: '百度文心千帆', value: 15, color: 'blue'}, + {key: 17, text: '阿里通义千问', value: 17, color: 'orange'}, + {key: 18, text: '讯飞星火认知', value: 18, color: 'blue'}, + {key: 16, text: '智谱 ChatGLM', value: 16, color: 'violet'}, + {key: 19, text: '360 智脑', value: 19, color: 'blue'}, + {key: 25, text: 'Moonshot AI', value: 25, color: 'black'}, + {key: 23, text: '腾讯混元', value: 23, color: 'teal'}, + {key: 26, text: '百川大模型', value: 26, color: 'orange'}, + {key: 27, text: 'MiniMax', value: 27, color: 'red'}, + {key: 29, text: 'Groq', value: 29, color: 'orange'}, + {key: 30, text: 'Ollama', value: 30, color: 'black'}, + {key: 31, text: '零一万物', value: 31, color: 'green'}, + {key: 32, text: '阶跃星辰', value: 32, color: 'blue'}, + {key: 34, text: 'Coze', value: 34, color: 'blue'}, + {key: 35, text: 'Cohere', value: 35, color: 'blue'}, + {key: 36, text: 'DeepSeek', value: 36, color: 'black'}, + {key: 37, text: 'Cloudflare', value: 37, color: 'orange'}, + {key: 8, text: '自定义渠道', value: 8, color: 'pink'}, + {key: 22, text: '知识库:FastGPT', value: 22, color: 'blue'}, + {key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple'}, + {key: 20, text: '代理:OpenRouter', value: 20, color: 'black'}, + {key: 2, text: '代理:API2D', value: 2, color: 'blue'}, + {key: 5, text: '代理:OpenAI-SB', value: 5, color: 'brown'}, + {key: 7, text: '代理:OhMyGPT', value: 7, color: 'purple'}, + {key: 10, text: '代理:AI Proxy', value: 10, color: 'purple'}, + {key: 4, text: '代理:CloseAI', value: 4, color: 'teal'}, + {key: 6, text: '代理:OpenAI Max', value: 6, color: 'violet'}, + {key: 9, text: '代理:AI.LS', value: 9, color: 'yellow'}, + {key: 12, text: '代理:API2GPT', value: 12, color: 'blue'}, + {key: 13, text: '代理:AIGC2D', value: 13, color: 'purple'} ]; diff --git a/web/default/src/pages/Channel/EditChannel.js b/web/default/src/pages/Channel/EditChannel.js index ffc9fc5f..5c7f13ff 100644 --- a/web/default/src/pages/Channel/EditChannel.js +++ b/web/default/src/pages/Channel/EditChannel.js @@ -488,6 +488,21 @@ const EditChannel = () => { /> ) } + { + inputs.type === 37 && ( + + + + ) + } { inputs.type !== 33 && !isEdit && ( Date: Fri, 26 Apr 2024 23:12:39 +0800 Subject: [PATCH 107/121] chore: update ollama models --- relay/adaptor/ollama/constants.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/relay/adaptor/ollama/constants.go b/relay/adaptor/ollama/constants.go index 32f82b2a..d9dc72a8 100644 --- a/relay/adaptor/ollama/constants.go +++ b/relay/adaptor/ollama/constants.go @@ -1,5 +1,11 @@ package ollama var ModelList = []string{ + "codellama:7b-instruct", + "llama2:7b", + "llama2:latest", + "llama3:latest", + "phi3:latest", "qwen:0.5b-chat", + "qwen:7b", } From e64e7707a0193f764eefbcd498069c80c8f3a51f Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 27 Apr 2024 00:06:43 +0800 Subject: [PATCH 108/121] feat: support cohere's web search --- relay/adaptor/cohere/constant.go | 7 +++++++ relay/adaptor/cohere/main.go | 8 ++++++++ relay/billing/ratio/model.go | 3 +++ 3 files changed, 18 insertions(+) diff --git a/relay/adaptor/cohere/constant.go b/relay/adaptor/cohere/constant.go index 3ff4d655..9e70652c 100644 --- a/relay/adaptor/cohere/constant.go +++ b/relay/adaptor/cohere/constant.go @@ -5,3 +5,10 @@ var ModelList = []string{ "command-light", "command-light-nightly", "command-r", "command-r-plus", } + +func init() { + num := len(ModelList) + for i := 0; i < num; i++ { + ModelList = append(ModelList, ModelList[i]+"-internet") + } +} diff --git a/relay/adaptor/cohere/main.go b/relay/adaptor/cohere/main.go index 81277b07..4bc3fa8d 100644 --- a/relay/adaptor/cohere/main.go +++ b/relay/adaptor/cohere/main.go @@ -17,6 +17,10 @@ import ( "github.com/songquanpeng/one-api/relay/model" ) +var ( + WebSearchConnector = Connector{ID: "web-search"} +) + func stopReasonCohere2OpenAI(reason *string) string { if reason == nil { return "" @@ -45,6 +49,10 @@ func ConvertRequest(textRequest model.GeneralOpenAIRequest) *Request { if cohereRequest.Model == "" { cohereRequest.Model = "command-r" } + if strings.HasSuffix(cohereRequest.Model, "-internet") { + cohereRequest.Model = strings.TrimSuffix(cohereRequest.Model, "-internet") + cohereRequest.Connectors = append(cohereRequest.Connectors, WebSearchConnector) + } for _, message := range textRequest.Messages { if message.Role == "user" { cohereRequest.Message = message.Content.(string) diff --git a/relay/billing/ratio/model.go b/relay/billing/ratio/model.go index c6fdf4b4..f6cc233a 100644 --- a/relay/billing/ratio/model.go +++ b/relay/billing/ratio/model.go @@ -228,6 +228,9 @@ func GetModelRatio(name string) float64 { if strings.HasPrefix(name, "qwen-") && strings.HasSuffix(name, "-internet") { name = strings.TrimSuffix(name, "-internet") } + if strings.HasPrefix(name, "command-") && strings.HasSuffix(name, "-internet") { + name = strings.TrimSuffix(name, "-internet") + } ratio, ok := ModelRatio[name] if !ok { ratio, ok = DefaultModelRatio[name] From 007906216de1f122cae137aacb6c4cab3c87caef Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 27 Apr 2024 13:37:22 +0800 Subject: [PATCH 109/121] feat: support DeepL's model (close #1126) --- README.md | 1 + relay/adaptor.go | 3 + relay/adaptor/deepl/adaptor.go | 73 ++++++++++ relay/adaptor/deepl/constants.go | 9 ++ relay/adaptor/deepl/helper.go | 11 ++ relay/adaptor/deepl/main.go | 137 ++++++++++++++++++ relay/adaptor/deepl/model.go | 16 ++ relay/adaptor/openai/token.go | 4 + relay/apitype/define.go | 1 + relay/billing/ratio/model.go | 4 + relay/channeltype/define.go | 1 + relay/channeltype/helper.go | 2 + relay/channeltype/url.go | 1 + relay/constant/common.go | 2 + relay/constant/finishreason/define.go | 5 + relay/constant/role/define.go | 5 + relay/controller/helper.go | 18 +++ relay/controller/text.go | 15 +- web/berry/src/constants/ChannelConstants.js | 6 + .../src/constants/channel.constants.js | 1 + 20 files changed, 305 insertions(+), 10 deletions(-) create mode 100644 relay/adaptor/deepl/adaptor.go create mode 100644 relay/adaptor/deepl/constants.go create mode 100644 relay/adaptor/deepl/helper.go create mode 100644 relay/adaptor/deepl/main.go create mode 100644 relay/adaptor/deepl/model.go create mode 100644 relay/constant/finishreason/define.go create mode 100644 relay/constant/role/define.go diff --git a/README.md b/README.md index 62834fb8..e57dd11b 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 + [x] [Cohere](https://cohere.com/) + [x] [DeepSeek](https://www.deepseek.com/) + [x] [Cloudflare Workers AI](https://developers.cloudflare.com/workers-ai/) + + [x] [DeepL](https://www.deepl.com/) 2. 支持配置镜像以及众多[第三方代理服务](https://iamazing.cn/page/openai-api-third-party-services)。 3. 支持通过**负载均衡**的方式访问多个渠道。 4. 支持 **stream 模式**,可以通过流式传输实现打字机效果。 diff --git a/relay/adaptor.go b/relay/adaptor.go index 87021a04..794a84a6 100644 --- a/relay/adaptor.go +++ b/relay/adaptor.go @@ -10,6 +10,7 @@ import ( "github.com/songquanpeng/one-api/relay/adaptor/cloudflare" "github.com/songquanpeng/one-api/relay/adaptor/cohere" "github.com/songquanpeng/one-api/relay/adaptor/coze" + "github.com/songquanpeng/one-api/relay/adaptor/deepl" "github.com/songquanpeng/one-api/relay/adaptor/gemini" "github.com/songquanpeng/one-api/relay/adaptor/ollama" "github.com/songquanpeng/one-api/relay/adaptor/openai" @@ -52,6 +53,8 @@ func GetAdaptor(apiType int) adaptor.Adaptor { return &cohere.Adaptor{} case apitype.Cloudflare: return &cloudflare.Adaptor{} + case apitype.DeepL: + return &deepl.Adaptor{} } return nil } diff --git a/relay/adaptor/deepl/adaptor.go b/relay/adaptor/deepl/adaptor.go new file mode 100644 index 00000000..d018a096 --- /dev/null +++ b/relay/adaptor/deepl/adaptor.go @@ -0,0 +1,73 @@ +package deepl + +import ( + "errors" + "fmt" + "github.com/gin-gonic/gin" + "github.com/songquanpeng/one-api/relay/adaptor" + "github.com/songquanpeng/one-api/relay/meta" + "github.com/songquanpeng/one-api/relay/model" + "io" + "net/http" +) + +type Adaptor struct { + meta *meta.Meta + promptText string +} + +func (a *Adaptor) Init(meta *meta.Meta) { + a.meta = meta +} + +func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { + return fmt.Sprintf("%s/v2/translate", meta.BaseURL), nil +} + +func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, meta *meta.Meta) error { + adaptor.SetupCommonRequestHeader(c, req, meta) + req.Header.Set("Authorization", "DeepL-Auth-Key "+meta.APIKey) + return nil +} + +func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) { + if request == nil { + return nil, errors.New("request is nil") + } + convertedRequest, text := ConvertRequest(*request) + a.promptText = text + return convertedRequest, nil +} + +func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) { + if request == nil { + return nil, errors.New("request is nil") + } + return request, nil +} + +func (a *Adaptor) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { + return adaptor.DoRequestHelper(a, c, meta, requestBody) +} + +func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { + if meta.IsStream { + err = StreamHandler(c, resp, meta.ActualModelName) + } else { + err = Handler(c, resp, meta.ActualModelName) + } + promptTokens := len(a.promptText) + usage = &model.Usage{ + PromptTokens: promptTokens, + TotalTokens: promptTokens, + } + return +} + +func (a *Adaptor) GetModelList() []string { + return ModelList +} + +func (a *Adaptor) GetChannelName() string { + return "deepl" +} diff --git a/relay/adaptor/deepl/constants.go b/relay/adaptor/deepl/constants.go new file mode 100644 index 00000000..6a4f2545 --- /dev/null +++ b/relay/adaptor/deepl/constants.go @@ -0,0 +1,9 @@ +package deepl + +// https://developers.deepl.com/docs/api-reference/glossaries + +var ModelList = []string{ + "deepl-zh", + "deepl-en", + "deepl-ja", +} diff --git a/relay/adaptor/deepl/helper.go b/relay/adaptor/deepl/helper.go new file mode 100644 index 00000000..6d3a914b --- /dev/null +++ b/relay/adaptor/deepl/helper.go @@ -0,0 +1,11 @@ +package deepl + +import "strings" + +func parseLangFromModelName(modelName string) string { + parts := strings.Split(modelName, "-") + if len(parts) == 1 { + return "ZH" + } + return parts[1] +} diff --git a/relay/adaptor/deepl/main.go b/relay/adaptor/deepl/main.go new file mode 100644 index 00000000..f8bbae14 --- /dev/null +++ b/relay/adaptor/deepl/main.go @@ -0,0 +1,137 @@ +package deepl + +import ( + "encoding/json" + "github.com/gin-gonic/gin" + "github.com/songquanpeng/one-api/common" + "github.com/songquanpeng/one-api/common/helper" + "github.com/songquanpeng/one-api/relay/adaptor/openai" + "github.com/songquanpeng/one-api/relay/constant" + "github.com/songquanpeng/one-api/relay/constant/finishreason" + "github.com/songquanpeng/one-api/relay/constant/role" + "github.com/songquanpeng/one-api/relay/model" + "io" + "net/http" +) + +// https://developers.deepl.com/docs/getting-started/your-first-api-request + +func ConvertRequest(textRequest model.GeneralOpenAIRequest) (*Request, string) { + var text string + if len(textRequest.Messages) != 0 { + text = textRequest.Messages[len(textRequest.Messages)-1].StringContent() + } + deeplRequest := Request{ + TargetLang: parseLangFromModelName(textRequest.Model), + Text: []string{text}, + } + return &deeplRequest, text +} + +func StreamResponseDeepL2OpenAI(deeplResponse *Response) *openai.ChatCompletionsStreamResponse { + var choice openai.ChatCompletionsStreamResponseChoice + if len(deeplResponse.Translations) != 0 { + choice.Delta.Content = deeplResponse.Translations[0].Text + } + choice.Delta.Role = role.Assistant + choice.FinishReason = &constant.StopFinishReason + openaiResponse := openai.ChatCompletionsStreamResponse{ + Object: constant.StreamObject, + Created: helper.GetTimestamp(), + Choices: []openai.ChatCompletionsStreamResponseChoice{choice}, + } + return &openaiResponse +} + +func ResponseDeepL2OpenAI(deeplResponse *Response) *openai.TextResponse { + var responseText string + if len(deeplResponse.Translations) != 0 { + responseText = deeplResponse.Translations[0].Text + } + choice := openai.TextResponseChoice{ + Index: 0, + Message: model.Message{ + Role: role.Assistant, + Content: responseText, + Name: nil, + }, + FinishReason: finishreason.Stop, + } + fullTextResponse := openai.TextResponse{ + Object: constant.NonStreamObject, + Created: helper.GetTimestamp(), + Choices: []openai.TextResponseChoice{choice}, + } + return &fullTextResponse +} + +func StreamHandler(c *gin.Context, resp *http.Response, modelName string) *model.ErrorWithStatusCode { + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return openai.ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) + } + err = resp.Body.Close() + if err != nil { + return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError) + } + var deeplResponse Response + err = json.Unmarshal(responseBody, &deeplResponse) + if err != nil { + return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError) + } + fullTextResponse := StreamResponseDeepL2OpenAI(&deeplResponse) + fullTextResponse.Model = modelName + fullTextResponse.Id = helper.GetResponseID(c) + jsonData, err := json.Marshal(fullTextResponse) + if err != nil { + return openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError) + } + common.SetEventStreamHeaders(c) + c.Stream(func(w io.Writer) bool { + if jsonData != nil { + c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonData)}) + jsonData = nil + return true + } + c.Render(-1, common.CustomEvent{Data: "data: [DONE]"}) + return false + }) + _ = resp.Body.Close() + return nil +} + +func Handler(c *gin.Context, resp *http.Response, modelName string) *model.ErrorWithStatusCode { + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return openai.ErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) + } + err = resp.Body.Close() + if err != nil { + return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError) + } + var deeplResponse Response + err = json.Unmarshal(responseBody, &deeplResponse) + if err != nil { + return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError) + } + if deeplResponse.Message != "" { + return &model.ErrorWithStatusCode{ + Error: model.Error{ + Message: deeplResponse.Message, + Code: "deepl_error", + }, + StatusCode: resp.StatusCode, + } + } + fullTextResponse := ResponseDeepL2OpenAI(&deeplResponse) + fullTextResponse.Model = modelName + fullTextResponse.Id = helper.GetResponseID(c) + jsonResponse, err := json.Marshal(fullTextResponse) + if err != nil { + return openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError) + } + c.Writer.Header().Set("Content-Type", "application/json") + c.Writer.WriteHeader(resp.StatusCode) + _, err = c.Writer.Write(jsonResponse) + return nil +} diff --git a/relay/adaptor/deepl/model.go b/relay/adaptor/deepl/model.go new file mode 100644 index 00000000..3f823d21 --- /dev/null +++ b/relay/adaptor/deepl/model.go @@ -0,0 +1,16 @@ +package deepl + +type Request struct { + Text []string `json:"text"` + TargetLang string `json:"target_lang"` +} + +type Translation struct { + DetectedSourceLanguage string `json:"detected_source_language,omitempty"` + Text string `json:"text,omitempty"` +} + +type Response struct { + Translations []Translation `json:"translations,omitempty"` + Message string `json:"message,omitempty"` +} diff --git a/relay/adaptor/openai/token.go b/relay/adaptor/openai/token.go index c95a7b5e..bb9c38a9 100644 --- a/relay/adaptor/openai/token.go +++ b/relay/adaptor/openai/token.go @@ -206,3 +206,7 @@ func CountTokenText(text string, model string) int { tokenEncoder := getTokenEncoder(model) return getTokenNum(tokenEncoder, text) } + +func CountToken(text string) int { + return CountTokenInput(text, "gpt-3.5-turbo") +} diff --git a/relay/apitype/define.go b/relay/apitype/define.go index e38eff7e..cf1df694 100644 --- a/relay/apitype/define.go +++ b/relay/apitype/define.go @@ -16,6 +16,7 @@ const ( Coze Cohere Cloudflare + DeepL Dummy // this one is only for count, do not add any channel after this ) diff --git a/relay/billing/ratio/model.go b/relay/billing/ratio/model.go index f6cc233a..fd9f2a4b 100644 --- a/relay/billing/ratio/model.go +++ b/relay/billing/ratio/model.go @@ -173,6 +173,10 @@ var ModelRatio = map[string]float64{ // https://platform.deepseek.com/api-docs/pricing/ "deepseek-chat": 1.0 / 1000 * RMB, "deepseek-coder": 1.0 / 1000 * RMB, + // https://www.deepl.com/pro?cta=header-prices + "deepl-zh": 25.0 / 1000 * USD, + "deepl-en": 25.0 / 1000 * USD, + "deepl-ja": 25.0 / 1000 * USD, } var CompletionRatio = map[string]float64{} diff --git a/relay/channeltype/define.go b/relay/channeltype/define.go index 3aa585a9..84d074a6 100644 --- a/relay/channeltype/define.go +++ b/relay/channeltype/define.go @@ -39,6 +39,7 @@ const ( Cohere DeepSeek Cloudflare + DeepL Dummy ) diff --git a/relay/channeltype/helper.go b/relay/channeltype/helper.go index a608c80e..1bb71402 100644 --- a/relay/channeltype/helper.go +++ b/relay/channeltype/helper.go @@ -33,6 +33,8 @@ func ToAPIType(channelType int) int { apiType = apitype.Cohere case Cloudflare: apiType = apitype.Cloudflare + case DeepL: + apiType = apitype.DeepL } return apiType diff --git a/relay/channeltype/url.go b/relay/channeltype/url.go index 657b677e..e4d7fbc9 100644 --- a/relay/channeltype/url.go +++ b/relay/channeltype/url.go @@ -39,6 +39,7 @@ var ChannelBaseURLs = []string{ "https://api.cohere.ai", // 35 "https://api.deepseek.com", // 36 "https://api.cloudflare.com", // 37 + "https://api-free.deepl.com", // 38 } func init() { diff --git a/relay/constant/common.go b/relay/constant/common.go index b6606cc6..f31477ca 100644 --- a/relay/constant/common.go +++ b/relay/constant/common.go @@ -1,3 +1,5 @@ package constant var StopFinishReason = "stop" +var StreamObject = "chat.completion.chunk" +var NonStreamObject = "chat.completion" diff --git a/relay/constant/finishreason/define.go b/relay/constant/finishreason/define.go new file mode 100644 index 00000000..1ed9c425 --- /dev/null +++ b/relay/constant/finishreason/define.go @@ -0,0 +1,5 @@ +package finishreason + +const ( + Stop = "stop" +) diff --git a/relay/constant/role/define.go b/relay/constant/role/define.go new file mode 100644 index 00000000..972488c5 --- /dev/null +++ b/relay/constant/role/define.go @@ -0,0 +1,5 @@ +package role + +const ( + Assistant = "assistant" +) diff --git a/relay/controller/helper.go b/relay/controller/helper.go index f1b40bef..8cc7657d 100644 --- a/relay/controller/helper.go +++ b/relay/controller/helper.go @@ -18,6 +18,7 @@ import ( "github.com/songquanpeng/one-api/relay/relaymode" "math" "net/http" + "strings" ) func getAndValidateTextRequest(c *gin.Context, relayMode int) (*relaymodel.GeneralOpenAIRequest, error) { @@ -204,3 +205,20 @@ func getMappedModelName(modelName string, mapping map[string]string) (string, bo } return modelName, false } + +func isErrorHappened(meta *meta.Meta, resp *http.Response) bool { + if resp == nil { + return true + } + if resp.StatusCode != http.StatusOK { + return true + } + if meta.ChannelType == channeltype.DeepL { + // skip stream check for deepl + return false + } + if meta.IsStream && strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") { + return true + } + return false +} diff --git a/relay/controller/text.go b/relay/controller/text.go index 9bfd3e76..6ed19b1d 100644 --- a/relay/controller/text.go +++ b/relay/controller/text.go @@ -4,10 +4,6 @@ import ( "bytes" "encoding/json" "fmt" - "io" - "net/http" - "strings" - "github.com/gin-gonic/gin" "github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/relay" @@ -18,6 +14,8 @@ import ( "github.com/songquanpeng/one-api/relay/channeltype" "github.com/songquanpeng/one-api/relay/meta" "github.com/songquanpeng/one-api/relay/model" + "io" + "net/http" ) func RelayTextHelper(c *gin.Context) *model.ErrorWithStatusCode { @@ -88,12 +86,9 @@ func RelayTextHelper(c *gin.Context) *model.ErrorWithStatusCode { logger.Errorf(ctx, "DoRequest failed: %s", err.Error()) return openai.ErrorWrapper(err, "do_request_failed", http.StatusInternalServerError) } - if resp != nil { - errorHappened := (resp.StatusCode != http.StatusOK) || (meta.IsStream && strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json")) - if errorHappened { - billing.ReturnPreConsumedQuota(ctx, preConsumedQuota, meta.TokenId) - return RelayErrorHandler(resp) - } + if isErrorHappened(meta, resp) { + billing.ReturnPreConsumedQuota(ctx, preConsumedQuota, meta.TokenId) + return RelayErrorHandler(resp) } // do response diff --git a/web/berry/src/constants/ChannelConstants.js b/web/berry/src/constants/ChannelConstants.js index 0ad94f04..b7a720ef 100644 --- a/web/berry/src/constants/ChannelConstants.js +++ b/web/berry/src/constants/ChannelConstants.js @@ -137,6 +137,12 @@ export const CHANNEL_OPTIONS = { value: 36, color: 'primary' }, + 38: { + key: 38, + text: 'DeepL', + value: 38, + color: 'primary' + }, 8: { key: 8, text: '自定义渠道', diff --git a/web/default/src/constants/channel.constants.js b/web/default/src/constants/channel.constants.js index a689ef27..9d7c7fa0 100644 --- a/web/default/src/constants/channel.constants.js +++ b/web/default/src/constants/channel.constants.js @@ -23,6 +23,7 @@ export const CHANNEL_OPTIONS = [ {key: 35, text: 'Cohere', value: 35, color: 'blue'}, {key: 36, text: 'DeepSeek', value: 36, color: 'black'}, {key: 37, text: 'Cloudflare', value: 37, color: 'orange'}, + {key: 38, text: 'DeepL', value: 38, color: 'black'}, {key: 8, text: '自定义渠道', value: 8, color: 'pink'}, {key: 22, text: '知识库:FastGPT', value: 22, color: 'blue'}, {key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple'}, From ef88497f25ad07e40240eeec8de26322e7fa87ab Mon Sep 17 00:00:00 2001 From: Wei Tingjiang Date: Sat, 27 Apr 2024 15:39:59 +0800 Subject: [PATCH 110/121] fix: refactor Gemini adaptor to support streaming content generation (#1382) --- relay/adaptor/gemini/adaptor.go | 7 ++++--- relay/adaptor/gemini/main.go | 32 +++++++++++++------------------- 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/relay/adaptor/gemini/adaptor.go b/relay/adaptor/gemini/adaptor.go index 839e45d6..a4dcae93 100644 --- a/relay/adaptor/gemini/adaptor.go +++ b/relay/adaptor/gemini/adaptor.go @@ -3,6 +3,9 @@ package gemini import ( "errors" "fmt" + "io" + "net/http" + "github.com/gin-gonic/gin" "github.com/songquanpeng/one-api/common/config" "github.com/songquanpeng/one-api/common/helper" @@ -10,8 +13,6 @@ import ( "github.com/songquanpeng/one-api/relay/adaptor/openai" "github.com/songquanpeng/one-api/relay/meta" "github.com/songquanpeng/one-api/relay/model" - "io" - "net/http" ) type Adaptor struct { @@ -25,7 +26,7 @@ func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { version := helper.AssignOrDefault(meta.Config.APIVersion, config.GeminiVersion) action := "generateContent" if meta.IsStream { - action = "streamGenerateContent" + action = "streamGenerateContent?alt=sse" } return fmt.Sprintf("%s/%s/models/%s:%s", meta.BaseURL, version, meta.ActualModelName, action), nil } diff --git a/relay/adaptor/gemini/main.go b/relay/adaptor/gemini/main.go index 8b934d30..f1b48551 100644 --- a/relay/adaptor/gemini/main.go +++ b/relay/adaptor/gemini/main.go @@ -232,8 +232,6 @@ func streamResponseGeminiChat2OpenAI(geminiResponse *ChatResponse) *openai.ChatC func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, string) { responseText := "" - dataChan := make(chan string) - stopChan := make(chan bool) scanner := bufio.NewScanner(resp.Body) scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { if atEOF && len(data) == 0 { @@ -247,14 +245,16 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC } return 0, nil, nil }) + dataChan := make(chan string) + stopChan := make(chan bool) go func() { for scanner.Scan() { data := scanner.Text() data = strings.TrimSpace(data) - if !strings.HasPrefix(data, "\"text\": \"") { + if !strings.HasPrefix(data, "data: ") { continue } - data = strings.TrimPrefix(data, "\"text\": \"") + data = strings.TrimPrefix(data, "data: ") data = strings.TrimSuffix(data, "\"") dataChan <- data } @@ -264,23 +264,17 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC c.Stream(func(w io.Writer) bool { select { case data := <-dataChan: - // this is used to prevent annoying \ related format bug - data = fmt.Sprintf("{\"content\": \"%s\"}", data) - type dummyStruct struct { - Content string `json:"content"` + var geminiResponse ChatResponse + err := json.Unmarshal([]byte(data), &geminiResponse) + if err != nil { + logger.SysError("error unmarshalling stream response: " + err.Error()) + return true } - var dummy dummyStruct - err := json.Unmarshal([]byte(data), &dummy) - responseText += dummy.Content - var choice openai.ChatCompletionsStreamResponseChoice - choice.Delta.Content = dummy.Content - response := openai.ChatCompletionsStreamResponse{ - Id: fmt.Sprintf("chatcmpl-%s", random.GetUUID()), - Object: "chat.completion.chunk", - Created: helper.GetTimestamp(), - Model: "gemini-pro", - Choices: []openai.ChatCompletionsStreamResponseChoice{choice}, + response := streamResponseGeminiChat2OpenAI(&geminiResponse) + if response == nil { + return true } + responseText += fmt.Sprintf("%v", response.Choices[0].Delta.Content) jsonResponse, err := json.Marshal(response) if err != nil { logger.SysError("error marshalling stream response: " + err.Error()) From 04b49aa0ec4ede0bec7f4c57ae78e8bc84bec672 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 27 Apr 2024 15:41:02 +0800 Subject: [PATCH 111/121] chore: use StringContent() to convert response to text --- relay/adaptor/gemini/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/relay/adaptor/gemini/main.go b/relay/adaptor/gemini/main.go index f1b48551..faccc4cb 100644 --- a/relay/adaptor/gemini/main.go +++ b/relay/adaptor/gemini/main.go @@ -274,7 +274,7 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC if response == nil { return true } - responseText += fmt.Sprintf("%v", response.Choices[0].Delta.Content) + responseText += response.Choices[0].Delta.StringContent() jsonResponse, err := json.Marshal(response) if err != nil { logger.SysError("error marshalling stream response: " + err.Error()) From 6170b91d1c6705f8937ca05b580cb77139a2e8ec Mon Sep 17 00:00:00 2001 From: NongMO <31816449+nongmo677@users.noreply.github.com> Date: Sat, 27 Apr 2024 15:47:27 +0800 Subject: [PATCH 112/121] feat: support for the ollama vision model (#1376) * feat: support for the ollama vision model `llava` model, pass test * Update main.go format code * chore: remove useless log --------- Co-authored-by: nongqiqin Co-authored-by: JustSong --- relay/adaptor/ollama/main.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/relay/adaptor/ollama/main.go b/relay/adaptor/ollama/main.go index b08eb0ca..c5fe08e6 100644 --- a/relay/adaptor/ollama/main.go +++ b/relay/adaptor/ollama/main.go @@ -13,6 +13,7 @@ import ( "github.com/gin-gonic/gin" "github.com/songquanpeng/one-api/common" + "github.com/songquanpeng/one-api/common/image" "github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/relay/adaptor/openai" "github.com/songquanpeng/one-api/relay/constant" @@ -32,9 +33,22 @@ func ConvertRequest(request model.GeneralOpenAIRequest) *ChatRequest { Stream: request.Stream, } for _, message := range request.Messages { + openaiContent := message.ParseContent() + var imageUrls []string + var contentText string + for _, part := range openaiContent { + switch part.Type { + case model.ContentTypeText: + contentText = part.Text + case model.ContentTypeImageURL: + _, data, _ := image.GetImageFromUrl(part.ImageURL.Url) + imageUrls = append(imageUrls, data) + } + } ollamaRequest.Messages = append(ollamaRequest.Messages, Message{ Role: message.Role, - Content: message.StringContent(), + Content: contentText, + Images: imageUrls, }) } return &ollamaRequest From 1bd14af47b3ad3059539c6dbd41960cf8ebbd0a9 Mon Sep 17 00:00:00 2001 From: tylinux Date: Sat, 27 Apr 2024 15:53:20 +0800 Subject: [PATCH 113/121] feat: use mapped model name to test (#1370) --- controller/channel-test.go | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/controller/channel-test.go b/controller/channel-test.go index a9f03c45..b8c41819 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -5,6 +5,15 @@ import ( "encoding/json" "errors" "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "strings" + "sync" + "time" + "github.com/songquanpeng/one-api/common/config" "github.com/songquanpeng/one-api/common/ctxkey" "github.com/songquanpeng/one-api/common/logger" @@ -18,14 +27,6 @@ import ( "github.com/songquanpeng/one-api/relay/meta" relaymodel "github.com/songquanpeng/one-api/relay/model" "github.com/songquanpeng/one-api/relay/relaymode" - "io" - "net/http" - "net/http/httptest" - "net/url" - "strconv" - "strings" - "sync" - "time" "github.com/gin-gonic/gin" ) @@ -69,6 +70,7 @@ func testChannel(channel *model.Channel) (err error, openaiErr *relaymodel.Error adaptor.Init(meta) var modelName string modelList := adaptor.GetModelList() + modelMap := channel.GetModelMapping() if len(modelList) != 0 { modelName = modelList[0] } @@ -77,6 +79,9 @@ func testChannel(channel *model.Channel) (err error, openaiErr *relaymodel.Error if len(modelNames) > 0 { modelName = modelNames[0] } + if modelMap != nil && modelMap[modelName] != "" { + modelName = modelMap[modelName] + } } request := buildTestRequest() request.Model = modelName From a84c7b38b7f8689104eb6486c9d83f812789208d Mon Sep 17 00:00:00 2001 From: Qiying Wang <781345688@qq.com> Date: Sat, 27 Apr 2024 15:58:07 +0800 Subject: [PATCH 114/121] fix: claude stream response parse (#1334) --- relay/adaptor/anthropic/main.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/relay/adaptor/anthropic/main.go b/relay/adaptor/anthropic/main.go index aa9e754f..a8de185c 100644 --- a/relay/adaptor/anthropic/main.go +++ b/relay/adaptor/anthropic/main.go @@ -4,6 +4,10 @@ import ( "bufio" "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" @@ -11,9 +15,6 @@ import ( "github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/relay/adaptor/openai" "github.com/songquanpeng/one-api/relay/model" - "io" - "net/http" - "strings" ) func stopReasonClaude2OpenAI(reason *string) string { @@ -176,10 +177,10 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC if len(data) < 6 { continue } - if !strings.HasPrefix(data, "data: ") { + if !strings.HasPrefix(data, "data:") { continue } - data = strings.TrimPrefix(data, "data: ") + data = strings.TrimPrefix(data, "data:") dataChan <- data } stopChan <- true @@ -192,7 +193,7 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC select { case data := <-dataChan: // some implementations may add \r at the end of data - data = strings.TrimSuffix(data, "\r") + data = strings.TrimSpace(data) var claudeResponse StreamResponse err := json.Unmarshal([]byte(data), &claudeResponse) if err != nil { From 6cffb116b7c1e2e5f4a681bf2440feef4a5ef265 Mon Sep 17 00:00:00 2001 From: caixinjiang Date: Sat, 27 Apr 2024 16:05:14 +0800 Subject: [PATCH 115/121] fix: fix zhipu embedding error when input is array but not string (#1306) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix zhipu embedding error when input is array but not string * fix: only use the first one --------- Co-authored-by: 蔡新疆 Co-authored-by: JustSong --- relay/adaptor/zhipu/adaptor.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/relay/adaptor/zhipu/adaptor.go b/relay/adaptor/zhipu/adaptor.go index 5ebafbb3..78b01fb3 100644 --- a/relay/adaptor/zhipu/adaptor.go +++ b/relay/adaptor/zhipu/adaptor.go @@ -62,8 +62,8 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.G } switch relayMode { case relaymode.Embeddings: - baiduEmbeddingRequest := ConvertEmbeddingRequest(*request) - return baiduEmbeddingRequest, nil + baiduEmbeddingRequest, err := ConvertEmbeddingRequest(*request) + return baiduEmbeddingRequest, err default: // TopP (0.0, 1.0) request.TopP = math.Min(0.99, request.TopP) @@ -129,11 +129,15 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Met return } -func ConvertEmbeddingRequest(request model.GeneralOpenAIRequest) *EmbeddingRequest { - return &EmbeddingRequest{ - Model: "embedding-2", - Input: request.Input.(string), +func ConvertEmbeddingRequest(request model.GeneralOpenAIRequest) (*EmbeddingRequest, error) { + inputs := request.ParseInput() + if len(inputs) != 1 { + return nil, errors.New("invalid input length, zhipu only support one input") } + return &EmbeddingRequest{ + Model: request.Model, + Input: inputs[0], + }, nil } func (a *Adaptor) GetModelList() []string { From 1c2654320e5b6268b13b6efca40ce37a523d032b Mon Sep 17 00:00:00 2001 From: plusye Date: Sat, 27 Apr 2024 16:07:06 +0800 Subject: [PATCH 116/121] fix: fix getPreConsumedQuota (#1312) --- relay/controller/helper.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/relay/controller/helper.go b/relay/controller/helper.go index 8cc7657d..2489652c 100644 --- a/relay/controller/helper.go +++ b/relay/controller/helper.go @@ -125,9 +125,9 @@ func getPromptTokens(textRequest *relaymodel.GeneralOpenAIRequest, relayMode int } func getPreConsumedQuota(textRequest *relaymodel.GeneralOpenAIRequest, promptTokens int, ratio float64) int64 { - preConsumedTokens := config.PreConsumedQuota + preConsumedTokens := config.PreConsumedQuota + int64(promptTokens) if textRequest.MaxTokens != 0 { - preConsumedTokens = int64(promptTokens) + int64(textRequest.MaxTokens) + preConsumedTokens += int64(textRequest.MaxTokens) } return int64(float64(preConsumedTokens) * ratio) } From 30f373b62344614198e97dc9c5da9664a07c1d30 Mon Sep 17 00:00:00 2001 From: JustSong Date: Mon, 29 Apr 2024 22:29:13 +0800 Subject: [PATCH 117/121] fix: fix usage is empty (close #1391) --- relay/adaptor/openai/adaptor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/relay/adaptor/openai/adaptor.go b/relay/adaptor/openai/adaptor.go index 57940558..55c05e0c 100644 --- a/relay/adaptor/openai/adaptor.go +++ b/relay/adaptor/openai/adaptor.go @@ -86,7 +86,7 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Met if meta.IsStream { var responseText string err, responseText, usage = StreamHandler(c, resp, meta.Mode) - if usage == nil { + if usage == nil || usage.TotalTokens == 0 { usage = ResponseText2Usage(responseText, meta.ActualModelName, meta.PromptTokens) } } else { From 7e027d2bd04759b69b49ea17aa20d2a764abdb2a Mon Sep 17 00:00:00 2001 From: JustSong Date: Mon, 29 Apr 2024 22:35:47 +0800 Subject: [PATCH 118/121] fix: fix minimax prompt & completion tokens is empty (#1391) --- relay/adaptor/openai/adaptor.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/relay/adaptor/openai/adaptor.go b/relay/adaptor/openai/adaptor.go index 55c05e0c..2e2e4100 100644 --- a/relay/adaptor/openai/adaptor.go +++ b/relay/adaptor/openai/adaptor.go @@ -89,6 +89,10 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Met if usage == nil || usage.TotalTokens == 0 { usage = ResponseText2Usage(responseText, meta.ActualModelName, meta.PromptTokens) } + if usage.TotalTokens != 0 && usage.PromptTokens == 0 { // some channels don't return prompt tokens & completion tokens + usage.PromptTokens = meta.PromptTokens + usage.CompletionTokens = usage.TotalTokens - meta.PromptTokens + } } else { switch meta.Mode { case relaymode.ImagesGenerations: From 1f76c8055310e79cb54c85ff9665c685f3596f16 Mon Sep 17 00:00:00 2001 From: JustSong Date: Mon, 29 Apr 2024 22:49:06 +0800 Subject: [PATCH 119/121] fix: fix aws claude panic (#1384) --- relay/controller/error.go | 10 ++++++++++ relay/controller/helper.go | 3 +++ 2 files changed, 13 insertions(+) diff --git a/relay/controller/error.go b/relay/controller/error.go index 69ece3ec..29d4f125 100644 --- a/relay/controller/error.go +++ b/relay/controller/error.go @@ -53,6 +53,16 @@ func (e GeneralErrorResponse) ToMessage() string { } func RelayErrorHandler(resp *http.Response) (ErrorWithStatusCode *model.ErrorWithStatusCode) { + if resp == nil { + return &model.ErrorWithStatusCode{ + StatusCode: 500, + Error: model.Error{ + Message: "resp is nil", + Type: "upstream_error", + Code: "bad_response", + }, + } + } ErrorWithStatusCode = &model.ErrorWithStatusCode{ StatusCode: resp.StatusCode, Error: model.Error{ diff --git a/relay/controller/helper.go b/relay/controller/helper.go index 2489652c..dccff486 100644 --- a/relay/controller/helper.go +++ b/relay/controller/helper.go @@ -208,6 +208,9 @@ func getMappedModelName(modelName string, mapping map[string]string) (string, bo func isErrorHappened(meta *meta.Meta, resp *http.Response) bool { if resp == nil { + if meta.ChannelType == channeltype.AwsClaude { + return false + } return true } if resp.StatusCode != http.StatusOK { From 71f4403fd51d0a83cb6e8fb792fcaee562173199 Mon Sep 17 00:00:00 2001 From: JustSong Date: Tue, 30 Apr 2024 02:16:53 +0800 Subject: [PATCH 120/121] feat: add together.ai support (#1298) --- README.md | 1 + relay/adaptor/openai/compatible.go | 4 ++++ relay/adaptor/togetherai/constants.go | 10 ++++++++++ relay/channeltype/define.go | 1 + relay/channeltype/url.go | 1 + web/berry/src/constants/ChannelConstants.js | 6 ++++++ web/default/src/constants/channel.constants.js | 1 + 7 files changed, 24 insertions(+) create mode 100644 relay/adaptor/togetherai/constants.go diff --git a/README.md b/README.md index e57dd11b..40f6e4e0 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用 + [x] [DeepSeek](https://www.deepseek.com/) + [x] [Cloudflare Workers AI](https://developers.cloudflare.com/workers-ai/) + [x] [DeepL](https://www.deepl.com/) + + [x] [together.ai](https://www.together.ai/) 2. 支持配置镜像以及众多[第三方代理服务](https://iamazing.cn/page/openai-api-third-party-services)。 3. 支持通过**负载均衡**的方式访问多个渠道。 4. 支持 **stream 模式**,可以通过流式传输实现打字机效果。 diff --git a/relay/adaptor/openai/compatible.go b/relay/adaptor/openai/compatible.go index ae8449db..0116a2eb 100644 --- a/relay/adaptor/openai/compatible.go +++ b/relay/adaptor/openai/compatible.go @@ -10,6 +10,7 @@ import ( "github.com/songquanpeng/one-api/relay/adaptor/mistral" "github.com/songquanpeng/one-api/relay/adaptor/moonshot" "github.com/songquanpeng/one-api/relay/adaptor/stepfun" + "github.com/songquanpeng/one-api/relay/adaptor/togetherai" "github.com/songquanpeng/one-api/relay/channeltype" ) @@ -24,6 +25,7 @@ var CompatibleChannels = []int{ channeltype.LingYiWanWu, channeltype.StepFun, channeltype.DeepSeek, + channeltype.TogetherAI, } func GetCompatibleChannelMeta(channelType int) (string, []string) { @@ -48,6 +50,8 @@ func GetCompatibleChannelMeta(channelType int) (string, []string) { return "stepfun", stepfun.ModelList case channeltype.DeepSeek: return "deepseek", deepseek.ModelList + case channeltype.TogetherAI: + return "together.ai", togetherai.ModelList default: return "openai", ModelList } diff --git a/relay/adaptor/togetherai/constants.go b/relay/adaptor/togetherai/constants.go new file mode 100644 index 00000000..0a79fbdc --- /dev/null +++ b/relay/adaptor/togetherai/constants.go @@ -0,0 +1,10 @@ +package togetherai + +// https://docs.together.ai/docs/inference-models + +var ModelList = []string{ + "meta-llama/Llama-3-70b-chat-hf", + "deepseek-ai/deepseek-coder-33b-instruct", + "mistralai/Mixtral-8x22B-Instruct-v0.1", + "Qwen/Qwen1.5-72B-Chat", +} diff --git a/relay/channeltype/define.go b/relay/channeltype/define.go index 84d074a6..60964565 100644 --- a/relay/channeltype/define.go +++ b/relay/channeltype/define.go @@ -40,6 +40,7 @@ const ( DeepSeek Cloudflare DeepL + TogetherAI Dummy ) diff --git a/relay/channeltype/url.go b/relay/channeltype/url.go index e4d7fbc9..f5767f47 100644 --- a/relay/channeltype/url.go +++ b/relay/channeltype/url.go @@ -40,6 +40,7 @@ var ChannelBaseURLs = []string{ "https://api.deepseek.com", // 36 "https://api.cloudflare.com", // 37 "https://api-free.deepl.com", // 38 + "https://api.together.xyz", // 39 } func init() { diff --git a/web/berry/src/constants/ChannelConstants.js b/web/berry/src/constants/ChannelConstants.js index b7a720ef..e6b0aed5 100644 --- a/web/berry/src/constants/ChannelConstants.js +++ b/web/berry/src/constants/ChannelConstants.js @@ -143,6 +143,12 @@ export const CHANNEL_OPTIONS = { value: 38, color: 'primary' }, + 39: { + key: 39, + text: 'together.ai', + value: 39, + color: 'primary' + }, 8: { key: 8, text: '自定义渠道', diff --git a/web/default/src/constants/channel.constants.js b/web/default/src/constants/channel.constants.js index 9d7c7fa0..43ee51b7 100644 --- a/web/default/src/constants/channel.constants.js +++ b/web/default/src/constants/channel.constants.js @@ -24,6 +24,7 @@ export const CHANNEL_OPTIONS = [ {key: 36, text: 'DeepSeek', value: 36, color: 'black'}, {key: 37, text: 'Cloudflare', value: 37, color: 'orange'}, {key: 38, text: 'DeepL', value: 38, color: 'black'}, + {key: 39, text: 'together.ai', value: 39, color: 'blue'}, {key: 8, text: '自定义渠道', value: 8, color: 'pink'}, {key: 22, text: '知识库:FastGPT', value: 22, color: 'blue'}, {key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple'}, From 2720e1a3583219743a557610ca38d8f32592bc66 Mon Sep 17 00:00:00 2001 From: JustSong Date: Tue, 30 Apr 2024 02:23:14 +0800 Subject: [PATCH 121/121] feat: support minimax's 6.5 models (close #1395) --- relay/adaptor/minimax/constants.go | 8 ++++++-- relay/billing/ratio/model.go | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/relay/adaptor/minimax/constants.go b/relay/adaptor/minimax/constants.go index c3da5b2d..1b2fc104 100644 --- a/relay/adaptor/minimax/constants.go +++ b/relay/adaptor/minimax/constants.go @@ -1,7 +1,11 @@ package minimax +// https://www.minimaxi.com/document/guides/chat-model/V2?id=65e0736ab2845de20908e2dd + var ModelList = []string{ - "abab5.5s-chat", - "abab5.5-chat", + "abab6.5-chat", + "abab6.5s-chat", "abab6-chat", + "abab5.5-chat", + "abab5.5s-chat", } diff --git a/relay/billing/ratio/model.go b/relay/billing/ratio/model.go index fd9f2a4b..f87e7742 100644 --- a/relay/billing/ratio/model.go +++ b/relay/billing/ratio/model.go @@ -138,6 +138,8 @@ var ModelRatio = map[string]float64{ "Baichuan2-Turbo-192k": 0.016 * RMB, "Baichuan2-53B": 0.02 * RMB, // https://api.minimax.chat/document/price + "abab6.5-chat": 0.03 * RMB, + "abab6.5s-chat": 0.01 * RMB, "abab6-chat": 0.1 * RMB, "abab5.5-chat": 0.015 * RMB, "abab5.5s-chat": 0.005 * RMB,