From a9f42abb59461cfa2ad1bbdc6ece003922309466 Mon Sep 17 00:00:00 2001 From: suziheng Date: Mon, 2 Dec 2024 10:32:39 +0800 Subject: [PATCH 01/18] =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E5=BC=80=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- model/main.go | 60 ++++++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/model/main.go b/model/main.go index ff542faf..60b9d849 100644 --- a/model/main.go +++ b/model/main.go @@ -108,36 +108,38 @@ func InitDB(envName string) (db *gorm.DB, err error) { if common.UsingMySQL { _, _ = sqlDB.Exec("DROP INDEX idx_channels_key ON channels;") // TODO: delete this line when most users have upgraded } - logger.SysLog("database migration started") - err = db.AutoMigrate(&Channel{}) - if err != nil { - return nil, err + if env.Bool("StartSqlMigration", false) { + logger.SysLog("database migration started") + err = db.AutoMigrate(&Channel{}) + if err != nil { + return nil, err + } + err = db.AutoMigrate(&Token{}) + if err != nil { + return nil, err + } + err = db.AutoMigrate(&User{}) + if err != nil { + return nil, err + } + err = db.AutoMigrate(&Option{}) + if err != nil { + return nil, err + } + err = db.AutoMigrate(&Redemption{}) + if err != nil { + return nil, err + } + err = db.AutoMigrate(&Ability{}) + if err != nil { + return nil, err + } + err = db.AutoMigrate(&Log{}) + if err != nil { + return nil, err + } + logger.SysLog("database migrated") } - err = db.AutoMigrate(&Token{}) - if err != nil { - return nil, err - } - err = db.AutoMigrate(&User{}) - if err != nil { - return nil, err - } - err = db.AutoMigrate(&Option{}) - if err != nil { - return nil, err - } - err = db.AutoMigrate(&Redemption{}) - if err != nil { - return nil, err - } - err = db.AutoMigrate(&Ability{}) - if err != nil { - return nil, err - } - err = db.AutoMigrate(&Log{}) - if err != nil { - return nil, err - } - logger.SysLog("database migrated") return db, err } else { logger.FatalLog(err) From 4882fd60ab82a01628dfa01f130780741e6175fe Mon Sep 17 00:00:00 2001 From: suziheng Date: Mon, 2 Dec 2024 11:01:30 +0800 Subject: [PATCH 02/18] =?UTF-8?q?=E4=BF=AE=E6=94=B9dockerfile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6743b139..ba9dbe51 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,15 +5,15 @@ COPY ./VERSION . COPY ./web . WORKDIR /web/default -RUN npm install +RUN npm install --legacy-peer-deps RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build WORKDIR /web/berry -RUN npm install +RUN npm install --legacy-peer-deps RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build WORKDIR /web/air -RUN npm install +RUN npm install --legacy-peer-deps RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build FROM golang AS builder2 From 9c931b7d43d6eb8ba0dbbb21d1b380ccdfabd3ae Mon Sep 17 00:00:00 2001 From: suziheng Date: Mon, 2 Dec 2024 11:08:36 +0800 Subject: [PATCH 03/18] =?UTF-8?q?=E4=BF=AE=E6=94=B9dockerfile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index ba9dbe51..10301631 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,15 +5,15 @@ COPY ./VERSION . COPY ./web . WORKDIR /web/default -RUN npm install --legacy-peer-deps +RUN npm install --force RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build WORKDIR /web/berry -RUN npm install --legacy-peer-deps +RUN npm install --force RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build WORKDIR /web/air -RUN npm install --legacy-peer-deps +RUN npm install --force RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) npm run build FROM golang AS builder2 From 6eb4e788c7dda251e4cf70f934f692806572fb85 Mon Sep 17 00:00:00 2001 From: suziheng Date: Tue, 24 Dec 2024 10:08:38 +0800 Subject: [PATCH 04/18] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=E5=BC=80=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- model/main.go | 53 ++++++++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/model/main.go b/model/main.go index 6d567e17..c71243b6 100644 --- a/model/main.go +++ b/model/main.go @@ -116,7 +116,10 @@ func InitDB() { return } + sqlDB := setDBConns(DB) + if !config.IsMasterNode { + return } if common.UsingMySQL { @@ -132,30 +135,32 @@ func InitDB() { } func migrateDB() error { - var err error - if err = DB.AutoMigrate(&Channel{}); err != nil { - return err - } - if err = DB.AutoMigrate(&Token{}); err != nil { - return err - } - if err = DB.AutoMigrate(&User{}); err != nil { - return err - } - if err = DB.AutoMigrate(&Option{}); err != nil { - return err - } - if err = DB.AutoMigrate(&Redemption{}); err != nil { - return err - } - if err = DB.AutoMigrate(&Ability{}); err != nil { - return err - } - if err = DB.AutoMigrate(&Log{}); err != nil { - return err - } - if err = DB.AutoMigrate(&Channel{}); err != nil { - return err + if env.Bool("StartSqlMigration", false) { + var err error + if err = DB.AutoMigrate(&Channel{}); err != nil { + return err + } + if err = DB.AutoMigrate(&Token{}); err != nil { + return err + } + if err = DB.AutoMigrate(&User{}); err != nil { + return err + } + if err = DB.AutoMigrate(&Option{}); err != nil { + return err + } + if err = DB.AutoMigrate(&Redemption{}); err != nil { + return err + } + if err = DB.AutoMigrate(&Ability{}); err != nil { + return err + } + if err = DB.AutoMigrate(&Log{}); err != nil { + return err + } + if err = DB.AutoMigrate(&Channel{}); err != nil { + return err + } } return nil } From 533f9853ac571a90ef59d5fc883fa8d8de0a0234 Mon Sep 17 00:00:00 2001 From: suziheng Date: Wed, 22 Jan 2025 16:43:40 +0800 Subject: [PATCH 05/18] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81CozeV3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- model/redemption.go | 2 +- model/token.go | 2 +- model/user.go | 8 +- relay/adaptor.go | 3 + relay/adaptor/coze/adaptor_v3.go | 75 ++++++++++ relay/adaptor/coze/helper.go | 18 ++- relay/adaptor/coze/main.go | 151 +++++++++++++++++++- relay/adaptor/coze/model.go | 42 ++++++ relay/apitype/define.go | 1 + relay/model/message.go | 49 +++++++ web/air/src/constants/channel.constants.js | 1 + web/berry/src/constants/ChannelConstants.js | 8 +- web/berry/src/views/Channel/type/Config.js | 14 ++ 13 files changed, 359 insertions(+), 15 deletions(-) create mode 100644 relay/adaptor/coze/adaptor_v3.go diff --git a/model/redemption.go b/model/redemption.go index 45871a71..9117916f 100644 --- a/model/redemption.go +++ b/model/redemption.go @@ -17,7 +17,7 @@ const ( type Redemption struct { Id int `json:"id"` UserId int `json:"user_id"` - Key string `json:"key" gorm:"type:char(32);uniqueIndex"` + Key string `json:"key" gorm:"type:char(32)"` Status int `json:"status" gorm:"default:1"` Name string `json:"name" gorm:"index"` Quota int64 `json:"quota" gorm:"bigint;default:100"` diff --git a/model/token.go b/model/token.go index 91e72a82..2426b730 100644 --- a/model/token.go +++ b/model/token.go @@ -21,7 +21,7 @@ const ( type Token struct { Id int `json:"id"` UserId int `json:"user_id"` - Key string `json:"key" gorm:"type:char(48);uniqueIndex"` + Key string `json:"key" gorm:"type:char(48)"` Status int `json:"status" gorm:"default:1"` Name string `json:"name" gorm:"index" ` CreatedTime int64 `json:"created_time" gorm:"bigint"` diff --git a/model/user.go b/model/user.go index a964a0d7..1956d2ce 100644 --- a/model/user.go +++ b/model/user.go @@ -30,7 +30,7 @@ const ( // Otherwise, the sensitive information will be saved on local storage in plain text! type User struct { Id int `json:"id"` - Username string `json:"username" gorm:"unique;index" validate:"max=12"` + Username string `json:"username" gorm:"index" validate:"max=12"` Password string `json:"password" gorm:"not null;" validate:"min=8,max=20"` DisplayName string `json:"display_name" gorm:"index" validate:"max=20"` Role int `json:"role" gorm:"type:int;default:1"` // admin, util @@ -40,13 +40,13 @@ type User struct { WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"` LarkId string `json:"lark_id" gorm:"column:lark_id;index"` OidcId string `json:"oidc_id" gorm:"column:oidc_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 + 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"` // this token is for system management 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"` + AffCode string `json:"aff_code" gorm:"type:varchar(32);column:aff_code"` InviterId int `json:"inviter_id" gorm:"type:int;column:inviter_id;index"` } diff --git a/relay/adaptor.go b/relay/adaptor.go index 03e83903..ad69aa82 100644 --- a/relay/adaptor.go +++ b/relay/adaptor.go @@ -64,6 +64,9 @@ func GetAdaptor(apiType int) adaptor.Adaptor { return &proxy.Adaptor{} case apitype.Replicate: return &replicate.Adaptor{} + case apitype.CozeV3: + return &coze.AdaptorV3{} + } return nil } diff --git a/relay/adaptor/coze/adaptor_v3.go b/relay/adaptor/coze/adaptor_v3.go new file mode 100644 index 00000000..fe890a8d --- /dev/null +++ b/relay/adaptor/coze/adaptor_v3.go @@ -0,0 +1,75 @@ +package coze + +import ( + "errors" + "fmt" + "github.com/gin-gonic/gin" + "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 AdaptorV3 struct { + meta *meta.Meta +} + +func (a *AdaptorV3) Init(meta *meta.Meta) { + a.meta = meta +} + +func (a *AdaptorV3) GetRequestURL(meta *meta.Meta) (string, error) { + return fmt.Sprintf("%s/v3/chat", meta.BaseURL), nil +} + +func (a *AdaptorV3) 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 *AdaptorV3) ConvertRequest(c *gin.Context, relayMode int, request *model.GeneralOpenAIRequest) (any, error) { + if request == nil { + return nil, errors.New("request is nil") + } + request.User = a.meta.Config.UserID + return ConvertRequest(*request), nil +} + +func (a *AdaptorV3) ConvertImageRequest(request *model.ImageRequest) (any, error) { + if request == nil { + return nil, errors.New("request is nil") + } + return request, nil +} + +func (a *AdaptorV3) DoRequest(c *gin.Context, meta *meta.Meta, requestBody io.Reader) (*http.Response, error) { + return adaptor.DoRequestHelper(a, c, meta, requestBody) +} + +func (a *AdaptorV3) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Meta) (usage *model.Usage, err *model.ErrorWithStatusCode) { + var responseText *string + if meta.IsStream { + err, responseText = V3StreamHandler(c, resp) + } else { + err, responseText = V3Handler(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 *AdaptorV3) GetModelList() []string { + return ModelList +} + +func (a *AdaptorV3) GetChannelName() string { + return "CozeV3" +} diff --git a/relay/adaptor/coze/helper.go b/relay/adaptor/coze/helper.go index 0396afcb..3c818426 100644 --- a/relay/adaptor/coze/helper.go +++ b/relay/adaptor/coze/helper.go @@ -1,6 +1,9 @@ package coze -import "github.com/songquanpeng/one-api/relay/adaptor/coze/constant/event" +import ( + "github.com/songquanpeng/one-api/relay/adaptor/coze/constant/event" + "strings" +) func event2StopReason(e *string) string { if e == nil || *e == event.Message { @@ -8,3 +11,16 @@ func event2StopReason(e *string) string { } return "stop" } + +func splitOnDoubleNewline(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\n"); i >= 0 { + return i + 1, data[0:i], nil + } + if atEOF { + return len(data), data, nil + } + return 0, nil, nil +} diff --git a/relay/adaptor/coze/main.go b/relay/adaptor/coze/main.go index d0402a76..8d3789ec 100644 --- a/relay/adaptor/coze/main.go +++ b/relay/adaptor/coze/main.go @@ -4,19 +4,18 @@ import ( "bufio" "encoding/json" "fmt" - "github.com/songquanpeng/one-api/common/render" - "io" - "net/http" - "strings" - "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/common/render" "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 @@ -45,12 +44,12 @@ func ConvertRequest(textRequest model.GeneralOpenAIRequest) *Request { } for i, message := range textRequest.Messages { if i == len(textRequest.Messages)-1 { - cozeRequest.Query = message.StringContent() + cozeRequest.Query = message.CozeV3StringContent() continue } cozeMessage := Message{ Role: message.Role, - Content: message.StringContent(), + Content: message.CozeV3StringContent(), } cozeRequest.ChatHistory = append(cozeRequest.ChatHistory, cozeMessage) } @@ -80,6 +79,28 @@ func StreamResponseCoze2OpenAI(cozeResponse *StreamResponse) (*openai.ChatComple return &openaiResponse, response } +func V3StreamResponseCoze2OpenAI(cozeResponse *V3StreamResponse) (*openai.ChatCompletionsStreamResponse, *Response) { + var response *Response + var choice openai.ChatCompletionsStreamResponseChoice + + choice.Delta.Role = cozeResponse.Role + choice.Delta.Content = cozeResponse.Content + + var openaiResponse openai.ChatCompletionsStreamResponse + openaiResponse.Object = "chat.completion.chunk" + openaiResponse.Choices = []openai.ChatCompletionsStreamResponseChoice{choice} + openaiResponse.Id = cozeResponse.ConversationId + + if cozeResponse.Usage.TokenCount > 0 { + openaiResponse.Usage = &model.Usage{ + PromptTokens: cozeResponse.Usage.InputCount, + CompletionTokens: cozeResponse.Usage.OutputCount, + TotalTokens: cozeResponse.Usage.TokenCount, + } + } + return &openaiResponse, response +} + func ResponseCoze2OpenAI(cozeResponse *Response) *openai.TextResponse { var responseText string for _, message := range cozeResponse.Messages { @@ -107,6 +128,26 @@ func ResponseCoze2OpenAI(cozeResponse *Response) *openai.TextResponse { return &fullTextResponse } +func V3ResponseCoze2OpenAI(cozeResponse *V3Response) *openai.TextResponse { + choice := openai.TextResponseChoice{ + Index: 0, + Message: model.Message{ + Role: "assistant", + Content: cozeResponse.Data.Content, + Name: nil, + }, + FinishReason: "stop", + } + fullTextResponse := openai.TextResponse{ + Id: fmt.Sprintf("chatcmpl-%s", cozeResponse.Data.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() @@ -162,6 +203,63 @@ func StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusC return nil, &responseText } +func V3StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *string) { + var responseText string + createdTime := helper.GetTimestamp() + scanner := bufio.NewScanner(resp.Body) + scanner.Split(splitOnDoubleNewline) + common.SetEventStreamHeaders(c) + var modelName string + for scanner.Scan() { + part := scanner.Text() + part = strings.TrimPrefix(part, "\n") + parts := strings.Split(part, "\n") + if len(parts) != 2 { + continue + } + if strings.HasPrefix(parts[0], "event:") && strings.HasPrefix(parts[1], "data:") { + continue + } + event, data := strings.TrimSpace(parts[0][6:]), strings.TrimSpace(parts[1][5:]) + if event == "conversation.message.delta" || event == "conversation.chat.completed" { + data = strings.TrimSuffix(data, "\r") + var cozeResponse V3StreamResponse + err := json.Unmarshal([]byte(data), &cozeResponse) + if err != nil { + logger.SysError("error unmarshalling stream response: " + err.Error()) + continue + } + + response, _ := V3StreamResponseCoze2OpenAI(&cozeResponse) + if response == nil { + continue + } + + for _, choice := range response.Choices { + responseText += conv.AsString(choice.Delta.Content) + } + response.Model = modelName + response.Created = createdTime + + err = render.ObjectData(c, response) + if err != nil { + logger.SysError(err.Error()) + } + } + } + if err := scanner.Err(); err != nil { + logger.SysError("error reading stream: " + err.Error()) + } + + render.Done(c) + err := resp.Body.Close() + if err != nil { + return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil + } + + 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 { @@ -200,3 +298,42 @@ func Handler(c *gin.Context, resp *http.Response, promptTokens int, modelName st } return nil, &responseText } + +func V3Handler(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 V3Response + 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 := V3ResponseCoze2OpenAI(&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 index d0afecfe..6bb98cb3 100644 --- a/relay/adaptor/coze/model.go +++ b/relay/adaptor/coze/model.go @@ -36,3 +36,45 @@ type StreamResponse struct { ConversationId string `json:"conversation_id,omitempty"` ErrorInformation *ErrorInformation `json:"error_information,omitempty"` } + +type V3StreamResponse struct { + Id string `json:"id"` + ConversationId string `json:"conversation_id"` + BotId string `json:"bot_id"` + Role string `json:"role"` + Type string `json:"type"` + Content string `json:"content"` + ContentType string `json:"content_type"` + ChatId string `json:"chat_id"` + CreatedAt int `json:"created_at"` + CompletedAt int `json:"completed_at"` + LastError struct { + Code int `json:"code"` + Msg string `json:"msg"` + } `json:"last_error"` + Status string `json:"status"` + Usage struct { + TokenCount int `json:"token_count"` + OutputCount int `json:"output_count"` + InputCount int `json:"input_count"` + } `json:"usage"` + SectionId string `json:"section_id"` +} + +type V3Response struct { + Data struct { + Id string `json:"id"` + ConversationId string `json:"conversation_id"` + BotId string `json:"bot_id"` + Content string `json:"content"` + ContentType string `json:"content_type"` + CreatedAt int `json:"created_at"` + LastError struct { + Code int `json:"code"` + Msg string `json:"msg"` + } `json:"last_error"` + Status string `json:"status"` + } `json:"data"` + Code int `json:"code"` + Msg string `json:"msg"` +} diff --git a/relay/apitype/define.go b/relay/apitype/define.go index 0c6a5ff1..066c1b7f 100644 --- a/relay/apitype/define.go +++ b/relay/apitype/define.go @@ -22,4 +22,5 @@ const ( Replicate Dummy // this one is only for count, do not add any channel after this + CozeV3 ) diff --git a/relay/model/message.go b/relay/model/message.go index b908f989..0a437dd8 100644 --- a/relay/model/message.go +++ b/relay/model/message.go @@ -1,5 +1,7 @@ package model +import "encoding/json" + type Message struct { Role string `json:"role,omitempty"` Content any `json:"content,omitempty"` @@ -37,6 +39,53 @@ func (m Message) StringContent() string { return "" } +func (m Message) CozeV3StringContent() string { + content, ok := m.Content.(string) + if ok { + return content + } + contentList, ok := m.Content.([]any) + if ok { + contents := make([]map[string]any, 0) + var contentStr string + for _, contentItem := range contentList { + contentMap, ok := contentItem.(map[string]any) + if !ok { + continue + } + switch contentMap["type"] { + case "text": + if subStr, ok := contentMap["text"].(string); ok { + contents = append(contents, map[string]any{ + "type": "text", + "text": subStr, + }) + } + case "image_url": + if subStr, ok := contentMap["image_url"].(string); ok { + contents = append(contents, map[string]any{ + "type": "image", + "file_url": subStr, + }) + } + case "file": + if subStr, ok := contentMap["image_url"].(string); ok { + contents = append(contents, map[string]any{ + "type": "file", + "file_url": subStr, + }) + } + } + } + if len(contents) > 0 { + b, _ := json.Marshal(contents) + return string(b) + } + return contentStr + } + return "" +} + func (m Message) ParseContent() []MessageContent { var contentList []MessageContent content, ok := m.Content.(string) diff --git a/web/air/src/constants/channel.constants.js b/web/air/src/constants/channel.constants.js index e7b25399..ef1ad8ec 100644 --- a/web/air/src/constants/channel.constants.js +++ b/web/air/src/constants/channel.constants.js @@ -22,6 +22,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: 47, text: 'CozeV3', value: 47, 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' }, diff --git a/web/berry/src/constants/ChannelConstants.js b/web/berry/src/constants/ChannelConstants.js index 375adcd9..da3f6056 100644 --- a/web/berry/src/constants/ChannelConstants.js +++ b/web/berry/src/constants/ChannelConstants.js @@ -137,6 +137,12 @@ export const CHANNEL_OPTIONS = { value: 34, color: 'primary' }, + 47: { + key: 47, + text: 'CozeV3', + value: 47, + color: 'primary' + }, 35: { key: 35, text: 'Cohere', @@ -185,7 +191,7 @@ export const CHANNEL_OPTIONS = { value: 45, color: 'primary' }, - 45: { + 46: { key: 46, text: 'Replicate', value: 46, diff --git a/web/berry/src/views/Channel/type/Config.js b/web/berry/src/views/Channel/type/Config.js index 67b90733..4a05692d 100644 --- a/web/berry/src/views/Channel/type/Config.js +++ b/web/berry/src/views/Channel/type/Config.js @@ -206,6 +206,20 @@ const typeConfig = { }, modelGroup: 'Coze' }, + 47: { + inputLabel: { + config: { + user_id: 'User ID' + } + }, + prompt: { + models: '对于 CozeV3 而言,模型名称即 Bot ID,你可以添加一个前缀 `bot-`,例如:`bot-123456`', + config: { + user_id: '生成该密钥的用户 ID' + } + }, + modelGroup: 'CozeV3' + }, 42: { inputLabel: { key: '', From aca72dc9792bd054b0dcd298982cbf991931260b Mon Sep 17 00:00:00 2001 From: suziheng Date: Wed, 22 Jan 2025 18:10:25 +0800 Subject: [PATCH 06/18] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81CozeV3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/default/src/constants/channel.constants.js | 1 + 1 file changed, 1 insertion(+) diff --git a/web/default/src/constants/channel.constants.js b/web/default/src/constants/channel.constants.js index 61425508..aa31fb37 100644 --- a/web/default/src/constants/channel.constants.js +++ b/web/default/src/constants/channel.constants.js @@ -22,6 +22,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: 47, text: 'CozeV3', value: 47, 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' }, From dde3cff7086793958a30bc07fc2d1cf67d7f6d89 Mon Sep 17 00:00:00 2001 From: suziheng Date: Wed, 22 Jan 2025 19:49:24 +0800 Subject: [PATCH 07/18] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81CozeV3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- relay/channeltype/define.go | 1 + relay/channeltype/helper.go | 2 ++ relay/channeltype/url.go | 1 + 3 files changed, 4 insertions(+) diff --git a/relay/channeltype/define.go b/relay/channeltype/define.go index f54d0e30..07fc77f5 100644 --- a/relay/channeltype/define.go +++ b/relay/channeltype/define.go @@ -48,5 +48,6 @@ const ( SiliconFlow XAI Replicate + CozeV3 Dummy ) diff --git a/relay/channeltype/helper.go b/relay/channeltype/helper.go index 8839b30a..e8adb7df 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 CozeV3: + apiType = apitype.CozeV3 case Cohere: apiType = apitype.Cohere case Cloudflare: diff --git a/relay/channeltype/url.go b/relay/channeltype/url.go index 8e271f4e..15f8e0a0 100644 --- a/relay/channeltype/url.go +++ b/relay/channeltype/url.go @@ -48,6 +48,7 @@ var ChannelBaseURLs = []string{ "https://api.siliconflow.cn", // 44 "https://api.x.ai", // 45 "https://api.replicate.com/v1/models/", // 46 + "https://api.coze.cn", // 47 } func init() { From cbf8413a39bbc9cae15e7a377cf8b86263f3c9ad Mon Sep 17 00:00:00 2001 From: suziheng Date: Wed, 22 Jan 2025 21:37:17 +0800 Subject: [PATCH 08/18] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81CozeV3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- relay/adaptor/coze/adaptor_v3.go | 2 +- relay/adaptor/coze/main.go | 32 +++++++++++++++++++++++++++++--- relay/adaptor/coze/model.go | 11 +++++++++-- relay/apitype/define.go | 3 +-- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/relay/adaptor/coze/adaptor_v3.go b/relay/adaptor/coze/adaptor_v3.go index fe890a8d..947b3846 100644 --- a/relay/adaptor/coze/adaptor_v3.go +++ b/relay/adaptor/coze/adaptor_v3.go @@ -35,7 +35,7 @@ func (a *AdaptorV3) ConvertRequest(c *gin.Context, relayMode int, request *model return nil, errors.New("request is nil") } request.User = a.meta.Config.UserID - return ConvertRequest(*request), nil + return V3ConvertRequest(*request), nil } func (a *AdaptorV3) ConvertImageRequest(request *model.ImageRequest) (any, error) { diff --git a/relay/adaptor/coze/main.go b/relay/adaptor/coze/main.go index 8d3789ec..98587374 100644 --- a/relay/adaptor/coze/main.go +++ b/relay/adaptor/coze/main.go @@ -44,14 +44,40 @@ func ConvertRequest(textRequest model.GeneralOpenAIRequest) *Request { } for i, message := range textRequest.Messages { if i == len(textRequest.Messages)-1 { - cozeRequest.Query = message.CozeV3StringContent() + cozeRequest.Query = message.StringContent() + continue + } + cozeMessage := Message{ + Role: message.Role, + Content: message.StringContent(), + } + cozeRequest.ChatHistory = append(cozeRequest.ChatHistory, cozeMessage) + } + return &cozeRequest +} + +func V3ConvertRequest(textRequest model.GeneralOpenAIRequest) *V3Request { + cozeRequest := V3Request{ + UserId: textRequest.User, + Stream: textRequest.Stream, + BotId: strings.TrimPrefix(textRequest.Model, "bot-"), + } + if cozeRequest.UserId == "" { + cozeRequest.UserId = "any" + } + for i, message := range textRequest.Messages { + if i == len(textRequest.Messages)-1 { + cozeRequest.AdditionalMessages = append(cozeRequest.AdditionalMessages, Message{ + Role: "user", + Content: message.CozeV3StringContent(), + }) continue } cozeMessage := Message{ Role: message.Role, Content: message.CozeV3StringContent(), } - cozeRequest.ChatHistory = append(cozeRequest.ChatHistory, cozeMessage) + cozeRequest.AdditionalMessages = append(cozeRequest.AdditionalMessages, cozeMessage) } return &cozeRequest } @@ -217,7 +243,7 @@ func V3StreamHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatu if len(parts) != 2 { continue } - if strings.HasPrefix(parts[0], "event:") && strings.HasPrefix(parts[1], "data:") { + if !strings.HasPrefix(parts[0], "event:") || !strings.HasPrefix(parts[1], "data:") { continue } event, data := strings.TrimSpace(parts[0][6:]), strings.TrimSpace(parts[1][5:]) diff --git a/relay/adaptor/coze/model.go b/relay/adaptor/coze/model.go index 6bb98cb3..b86e2a67 100644 --- a/relay/adaptor/coze/model.go +++ b/relay/adaptor/coze/model.go @@ -2,9 +2,9 @@ package coze type Message struct { Role string `json:"role"` - Type string `json:"type"` + Type string `json:"type,omitempty"` Content string `json:"content"` - ContentType string `json:"content_type"` + ContentType string `json:"content_type,omitempty"` } type ErrorInformation struct { @@ -78,3 +78,10 @@ type V3Response struct { Code int `json:"code"` Msg string `json:"msg"` } + +type V3Request struct { + BotId string `json:"bot_id"` + UserId string `json:"user_id"` + AdditionalMessages []Message `json:"additional_messages"` + Stream bool `json:"stream"` +} diff --git a/relay/apitype/define.go b/relay/apitype/define.go index 066c1b7f..30a7a65b 100644 --- a/relay/apitype/define.go +++ b/relay/apitype/define.go @@ -20,7 +20,6 @@ const ( VertexAI Proxy Replicate - - Dummy // this one is only for count, do not add any channel after this CozeV3 + Dummy // this one is only for count, do not add any channel after this ) From c2bd301e0a4115519b4cfa5b343addd3f8433e0b Mon Sep 17 00:00:00 2001 From: suziheng Date: Mon, 21 Apr 2025 15:59:44 +0800 Subject: [PATCH 09/18] =?UTF-8?q?feat:=E4=BF=AE=E5=A4=8Dtranscribe=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- relay/controller/audio.go | 97 +++++++++++++++++++++++++++++++++++---- 1 file changed, 89 insertions(+), 8 deletions(-) diff --git a/relay/controller/audio.go b/relay/controller/audio.go index e3d57b1e..2f792ce3 100644 --- a/relay/controller/audio.go +++ b/relay/controller/audio.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "mime/multipart" "net/http" "strings" @@ -30,8 +31,7 @@ import ( func RelayAudioHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatusCode { ctx := c.Request.Context() meta := meta.GetByContext(c) - audioModel := "whisper-1" - + audioModel := "gpt-4o-transcribe" tokenId := c.GetInt(ctxkey.TokenId) channelType := c.GetInt(ctxkey.Channel) channelId := c.GetInt(ctxkey.ChannelId) @@ -124,12 +124,13 @@ func RelayAudioHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus fullRequestURL := openai.GetFullRequestURL(baseURL, requestURL, channelType) if channelType == channeltype.Azure { apiVersion := meta.Config.APIVersion + deploymentName := c.GetString(ctxkey.ChannelName) 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) + fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/audio/transcriptions?api-version=%s", baseURL, deploymentName, apiVersion) } else if relayMode == relaymode.AudioSpeech { // 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) + fullRequestURL = fmt.Sprintf("%s/openai/deployments/%s/audio/speech?api-version=%s", baseURL, deploymentName, apiVersion) } } @@ -138,8 +139,73 @@ func RelayAudioHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus if err != nil { return openai.ErrorWrapper(err, "new_request_body_failed", http.StatusInternalServerError) } - c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody.Bytes())) - responseFormat := c.DefaultPostForm("response_format", "json") + + // 处理表单数据 + contentType := c.Request.Header.Get("Content-Type") + responseFormat := "json" + var contentTypeWithBoundary string + + if strings.Contains(contentType, "multipart/form-data") { + originalBody := requestBody.Bytes() + c.Request.Body = io.NopCloser(bytes.NewBuffer(originalBody)) + err = c.Request.ParseMultipartForm(32 << 20) // 32MB 最大内存 + if err != nil { + return openai.ErrorWrapper(err, "parse_multipart_form_failed", http.StatusInternalServerError) + } + + // 获取响应格式 + if format := c.Request.FormValue("response_format"); format != "" { + responseFormat = format + } + + requestBody = &bytes.Buffer{} + writer := multipart.NewWriter(requestBody) + + // 复制表单字段 + for key, values := range c.Request.MultipartForm.Value { + for _, value := range values { + err = writer.WriteField(key, value) + if err != nil { + return openai.ErrorWrapper(err, "write_field_failed", http.StatusInternalServerError) + } + } + } + + // 复制文件 + for key, fileHeaders := range c.Request.MultipartForm.File { + for _, fileHeader := range fileHeaders { + file, err := fileHeader.Open() + if err != nil { + return openai.ErrorWrapper(err, "open_file_failed", http.StatusInternalServerError) + } + + part, err := writer.CreateFormFile(key, fileHeader.Filename) + if err != nil { + file.Close() + return openai.ErrorWrapper(err, "create_form_file_failed", http.StatusInternalServerError) + } + + _, err = io.Copy(part, file) + file.Close() + if err != nil { + return openai.ErrorWrapper(err, "copy_file_failed", http.StatusInternalServerError) + } + } + } + + // 完成multipart写入 + err = writer.Close() + if err != nil { + return openai.ErrorWrapper(err, "close_writer_failed", http.StatusInternalServerError) + } + + // 更新Content-Type + contentTypeWithBoundary = writer.FormDataContentType() + c.Request.Header.Set("Content-Type", contentTypeWithBoundary) + } else { + // 对于非表单请求,直接重置请求体 + c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody.Bytes())) + } req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody) if err != nil { @@ -151,11 +217,26 @@ func RelayAudioHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus apiKey := c.Request.Header.Get("Authorization") apiKey = strings.TrimPrefix(apiKey, "Bearer ") req.Header.Set("api-key", apiKey) - req.ContentLength = c.Request.ContentLength + // 确保请求体大小与Content-Length一致 + req.ContentLength = int64(requestBody.Len()) } else { req.Header.Set("Authorization", c.Request.Header.Get("Authorization")) + // 确保请求体大小与Content-Length一致 + req.ContentLength = int64(requestBody.Len()) + } + + // 确保Content-Type正确传递 + if strings.Contains(contentType, "multipart/form-data") && c.Request.MultipartForm != nil { + // 对于multipart请求,使用我们重建时生成的Content-Type + // 注意:此处必须使用writer生成的boundary + if contentTypeWithBoundary != "" { + req.Header.Set("Content-Type", contentTypeWithBoundary) + } else { + req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type")) + } + } else { + req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type")) } - req.Header.Set("Content-Type", c.Request.Header.Get("Content-Type")) req.Header.Set("Accept", c.Request.Header.Get("Accept")) resp, err := client.HTTPClient.Do(req) From 5f5521bc9a1a388e033dee186949df37d1a5de74 Mon Sep 17 00:00:00 2001 From: suziheng Date: Tue, 22 Apr 2025 09:27:58 +0800 Subject: [PATCH 10/18] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E5=80=8D=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- relay/billing/ratio/model.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/relay/billing/ratio/model.go b/relay/billing/ratio/model.go index e8b3b615..f25ada17 100644 --- a/relay/billing/ratio/model.go +++ b/relay/billing/ratio/model.go @@ -71,6 +71,8 @@ var ModelRatio = map[string]float64{ "text-davinci-edit-001": 10, "code-davinci-edit-001": 10, "whisper-1": 15, // $0.006 / minute -> $0.006 / 150 words -> $0.006 / 200 tokens -> $0.03 / 1k tokens + "gpt-4o-mini-transcribe": 15, // $0.006 / minute -> $0.006 / 150 words -> $0.006 / 200 tokens -> $0.03 / 1k tokens + "gpt-4o-transcribe": 15, // $0.006 / minute -> $0.006 / 150 words -> $0.006 / 200 tokens -> $0.03 / 1k tokens "tts-1": 7.5, // $0.015 / 1K characters "tts-1-1106": 7.5, "tts-1-hd": 15, // $0.030 / 1K characters @@ -626,7 +628,9 @@ var CompletionRatio = map[string]float64{ "llama3-8b-8192(33)": 0.0006 / 0.0003, "llama3-70b-8192(33)": 0.0035 / 0.00265, // whisper - "whisper-1": 0, // only count input tokens + "whisper-1": 0, // only count input tokens + "gpt-4o-mini-transcribe": 0, + "gpt-4o-transcribe": 0, // deepseek "deepseek-chat": 0.28 / 0.14, "deepseek-reasoner": 2.19 / 0.55, From abf9d113af20d64b06ab40ba8906c9d7a8cdbe42 Mon Sep 17 00:00:00 2001 From: suziheng Date: Tue, 22 Apr 2025 11:10:21 +0800 Subject: [PATCH 11/18] =?UTF-8?q?feat:=20=E6=81=A2=E5=A4=8D=E8=A1=A8?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- model/redemption.go | 2 +- model/token.go | 2 +- model/user.go | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/model/redemption.go b/model/redemption.go index ad2b512a..957a33be 100644 --- a/model/redemption.go +++ b/model/redemption.go @@ -20,7 +20,7 @@ const ( type Redemption struct { Id int `json:"id"` UserId int `json:"user_id"` - Key string `json:"key" gorm:"type:char(32)"` + 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:"bigint;default:100"` diff --git a/model/token.go b/model/token.go index 49394712..52ee63ef 100644 --- a/model/token.go +++ b/model/token.go @@ -23,7 +23,7 @@ const ( type Token struct { Id int `json:"id"` UserId int `json:"user_id"` - Key string `json:"key" gorm:"type:char(48)"` + 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"` diff --git a/model/user.go b/model/user.go index 773dad59..7b25c61b 100644 --- a/model/user.go +++ b/model/user.go @@ -33,7 +33,7 @@ const ( // Otherwise, the sensitive information will be saved on local storage in plain text! type User struct { Id int `json:"id"` - Username string `json:"username" gorm:"index" validate:"max=12"` + Username string `json:"username" gorm:"unique;index" validate:"max=12"` Password string `json:"password" gorm:"not null;" validate:"min=8,max=20"` DisplayName string `json:"display_name" gorm:"index" validate:"max=20"` Role int `json:"role" gorm:"type:int;default:1"` // admin, util @@ -43,8 +43,8 @@ type User struct { WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"` LarkId string `json:"lark_id" gorm:"column:lark_id;index"` OidcId string `json:"oidc_id" gorm:"column:oidc_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"` // this token is for system management + 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:"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 From a8a303b4eea83d0ad3aa18bb754e3468a536e73c Mon Sep 17 00:00:00 2001 From: suziheng Date: Tue, 22 Apr 2025 11:12:23 +0800 Subject: [PATCH 12/18] =?UTF-8?q?feat:=20=E6=81=A2=E5=A4=8D=E8=A1=A8?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- model/user.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model/user.go b/model/user.go index 7b25c61b..021810c0 100644 --- a/model/user.go +++ b/model/user.go @@ -49,7 +49,7 @@ type User struct { 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"` + 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 c7742de0fc214e4de47d690b1feb2add20f2c45e Mon Sep 17 00:00:00 2001 From: suziheng Date: Tue, 22 Apr 2025 11:52:57 +0800 Subject: [PATCH 13/18] =?UTF-8?q?build:=20=E5=B9=B6=E8=A1=8C=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E4=B8=B2=E8=A1=8C=E6=9E=84=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 346d9c5b..46ab475c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,15 +4,13 @@ WORKDIR /web COPY ./VERSION . COPY ./web . -RUN npm install --prefix /web/default & \ - npm install --prefix /web/berry & \ - npm install --prefix /web/air & \ - wait +RUN npm install --prefix /web/default && \ + npm install --prefix /web/berry && \ + npm install --prefix /web/air -RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat ./VERSION) npm run build --prefix /web/default & \ - DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat ./VERSION) npm run build --prefix /web/berry & \ - DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat ./VERSION) npm run build --prefix /web/air & \ - wait +RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat ./VERSION) npm run build --prefix /web/default && \ + DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat ./VERSION) npm run build --prefix /web/berry && \ + DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat ./VERSION) npm run build --prefix /web/air FROM golang:alpine AS builder2 From 9a7967e9bb0c83e34acb97fcbbed730650ef6646 Mon Sep 17 00:00:00 2001 From: suziheng Date: Wed, 23 Apr 2025 10:25:35 +0800 Subject: [PATCH 14/18] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4azure=20deploym?= =?UTF-8?q?ent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- relay/adaptor/openai/adaptor.go | 1 - 1 file changed, 1 deletion(-) diff --git a/relay/adaptor/openai/adaptor.go b/relay/adaptor/openai/adaptor.go index 8faf90a5..38a66d7a 100644 --- a/relay/adaptor/openai/adaptor.go +++ b/relay/adaptor/openai/adaptor.go @@ -45,7 +45,6 @@ func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { requestURL = fmt.Sprintf("%s?api-version=%s", requestURL, meta.Config.APIVersion) task := strings.TrimPrefix(requestURL, "/v1/") model_ := meta.ActualModelName - model_ = strings.Replace(model_, ".", "", -1) //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) From 93d54a7ef5ada3f9fcda7bd62a4ebcc77abe0c3c Mon Sep 17 00:00:00 2001 From: suziheng Date: Wed, 23 Apr 2025 10:27:45 +0800 Subject: [PATCH 15/18] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0gpt-4.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- relay/adaptor/openai/constants.go | 2 +- relay/billing/ratio/model.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/relay/adaptor/openai/constants.go b/relay/adaptor/openai/constants.go index 8a643bc6..a8854efe 100644 --- a/relay/adaptor/openai/constants.go +++ b/relay/adaptor/openai/constants.go @@ -4,7 +4,7 @@ var ModelList = []string{ "gpt-3.5-turbo", "gpt-3.5-turbo-0301", "gpt-3.5-turbo-0613", "gpt-3.5-turbo-1106", "gpt-3.5-turbo-0125", "gpt-3.5-turbo-16k", "gpt-3.5-turbo-16k-0613", "gpt-3.5-turbo-instruct", - "gpt-4", "gpt-4-0314", "gpt-4-0613", "gpt-4-1106-preview", "gpt-4-0125-preview", + "gpt-4", "gpt-4.1", "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", "gpt-4-turbo-2024-04-09", "gpt-4o", "gpt-4o-2024-05-13", diff --git a/relay/billing/ratio/model.go b/relay/billing/ratio/model.go index f25ada17..70a08d8f 100644 --- a/relay/billing/ratio/model.go +++ b/relay/billing/ratio/model.go @@ -27,6 +27,7 @@ var modelRatioLock sync.RWMutex var ModelRatio = map[string]float64{ // https://openai.com/pricing "gpt-4": 15, + "gpt-4.1": 15, "gpt-4-0314": 15, "gpt-4-0613": 15, "gpt-4-32k": 30, From 1e19c333c97a4d8571df3e81eefbea4c2674e6d4 Mon Sep 17 00:00:00 2001 From: suziheng Date: Fri, 16 May 2025 10:18:07 +0800 Subject: [PATCH 16/18] fix: get gemini adapter bug --- relay/adaptor/gemini/adaptor.go | 2 +- relay/channeltype/helper.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/relay/adaptor/gemini/adaptor.go b/relay/adaptor/gemini/adaptor.go index 84083f60..5d87d2d6 100644 --- a/relay/adaptor/gemini/adaptor.go +++ b/relay/adaptor/gemini/adaptor.go @@ -25,7 +25,7 @@ func (a *Adaptor) Init(meta *meta.Meta) { func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) { defaultVersion := config.GeminiVersion - if strings.Contains(meta.ActualModelName, "gemini-2.0") || + if strings.Contains(meta.ActualModelName, "gemini-2") || strings.Contains(meta.ActualModelName, "gemini-1.5") { defaultVersion = "v1beta" } diff --git a/relay/channeltype/helper.go b/relay/channeltype/helper.go index e8adb7df..ea3e6a55 100644 --- a/relay/channeltype/helper.go +++ b/relay/channeltype/helper.go @@ -23,6 +23,8 @@ func ToAPIType(channelType int) int { apiType = apitype.Tencent case Gemini: apiType = apitype.Gemini + case GeminiOpenAICompatible: + apiType = apitype.Gemini case Ollama: apiType = apitype.Ollama case AwsClaude: From e1ee4fe7d99b2414f656830e696c368bfd4827e8 Mon Sep 17 00:00:00 2001 From: suziheng Date: Wed, 21 May 2025 16:19:36 +0800 Subject: [PATCH 17/18] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81gemini=20?= =?UTF-8?q?=E6=80=9D=E8=80=83=E8=BF=87=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- relay/adaptor/gemini/main.go | 40 +++++++++++++++++++++++++++++++++-- relay/adaptor/gemini/model.go | 23 +++++++++++++------- 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/relay/adaptor/gemini/main.go b/relay/adaptor/gemini/main.go index 29637296..3d0fe901 100644 --- a/relay/adaptor/gemini/main.go +++ b/relay/adaptor/gemini/main.go @@ -66,6 +66,23 @@ func ConvertRequest(textRequest model.GeneralOpenAIRequest) *ChatRequest { MaxOutputTokens: textRequest.MaxTokens, }, } + + if textRequest.ReasoningEffort != nil { + var thinkBudget int + switch *textRequest.ReasoningEffort { + case "low": + thinkBudget = 1000 + case "medium": + thinkBudget = 8000 + case "high": + thinkBudget = 24000 + } + geminiRequest.GenerationConfig.ThinkingConfig = &ThinkingConfig{ + ThinkingBudget: thinkBudget, + IncludeThoughts: true, + } + } + if textRequest.ResponseFormat != nil { if mimeType, ok := mimeTypeMap[textRequest.ResponseFormat.Type]; ok { geminiRequest.GenerationConfig.ResponseMimeType = mimeType @@ -199,6 +216,21 @@ func (g *ChatResponse) GetResponseText() string { return "" } +func (g *ChatResponse) GetResponseTextAndThought() (content string, thought string) { + if g == nil { + return + } + if len(g.Candidates) > 0 && len(g.Candidates[0].Content.Parts) > 0 { + contentPart := g.Candidates[0].Content.Parts[0] + if contentPart.Thought { + thought = contentPart.Text + return + } + content = contentPart.Text + } + return +} + type ChatCandidate struct { Content ChatContent `json:"content"` FinishReason string `json:"finishReason"` @@ -263,7 +295,11 @@ func responseGeminiChat2OpenAI(response *ChatResponse) *openai.TextResponse { if i > 0 { builder.WriteString("\n") } - builder.WriteString(part.Text) + if part.Thought { + builder.WriteString(fmt.Sprintf("%s\n", part.Text)) + } else { + builder.WriteString(part.Text) + } } choice.Message.Content = builder.String() } @@ -278,7 +314,7 @@ func responseGeminiChat2OpenAI(response *ChatResponse) *openai.TextResponse { func streamResponseGeminiChat2OpenAI(geminiResponse *ChatResponse) *openai.ChatCompletionsStreamResponse { var choice openai.ChatCompletionsStreamResponseChoice - choice.Delta.Content = geminiResponse.GetResponseText() + choice.Delta.Content, choice.Delta.ReasoningContent = geminiResponse.GetResponseTextAndThought() //choice.FinishReason = &constant.StopFinishReason var response openai.ChatCompletionsStreamResponse response.Id = fmt.Sprintf("chatcmpl-%s", random.GetUUID()) diff --git a/relay/adaptor/gemini/model.go b/relay/adaptor/gemini/model.go index c3acae60..8fd96622 100644 --- a/relay/adaptor/gemini/model.go +++ b/relay/adaptor/gemini/model.go @@ -49,6 +49,7 @@ type Part struct { Text string `json:"text,omitempty"` InlineData *InlineData `json:"inlineData,omitempty"` FunctionCall *FunctionCall `json:"functionCall,omitempty"` + Thought bool `json:"thought,omitempty"` } type ChatContent struct { @@ -66,12 +67,18 @@ type ChatTools struct { } type ChatGenerationConfig struct { - ResponseMimeType string `json:"responseMimeType,omitempty"` - ResponseSchema any `json:"responseSchema,omitempty"` - Temperature *float64 `json:"temperature,omitempty"` - TopP *float64 `json:"topP,omitempty"` - TopK float64 `json:"topK,omitempty"` - MaxOutputTokens int `json:"maxOutputTokens,omitempty"` - CandidateCount int `json:"candidateCount,omitempty"` - StopSequences []string `json:"stopSequences,omitempty"` + ResponseMimeType string `json:"responseMimeType,omitempty"` + ResponseSchema any `json:"responseSchema,omitempty"` + Temperature *float64 `json:"temperature,omitempty"` + TopP *float64 `json:"topP,omitempty"` + TopK float64 `json:"topK,omitempty"` + MaxOutputTokens int `json:"maxOutputTokens,omitempty"` + CandidateCount int `json:"candidateCount,omitempty"` + StopSequences []string `json:"stopSequences,omitempty"` + ThinkingConfig *ThinkingConfig `json:"thinkingConfig,omitempty"` +} + +type ThinkingConfig struct { + ThinkingBudget int `json:"thinkingBudget"` + IncludeThoughts bool `json:"includeThoughts"` } From cf0ce425e608ed1a698f3cff5b655bacff0b6f1e Mon Sep 17 00:00:00 2001 From: suziheng Date: Wed, 28 May 2025 15:08:48 +0800 Subject: [PATCH 18/18] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4gemini=20tools?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- relay/adaptor/gemini/main.go | 21 ++++++--------------- relay/adaptor/gemini/model.go | 2 +- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/relay/adaptor/gemini/main.go b/relay/adaptor/gemini/main.go index 3d0fe901..9270e7f7 100644 --- a/relay/adaptor/gemini/main.go +++ b/relay/adaptor/gemini/main.go @@ -93,22 +93,13 @@ func ConvertRequest(textRequest model.GeneralOpenAIRequest) *ChatRequest { } } 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, - }, - } + geminiRequest.Tools = textRequest.Tools } + + if textRequest.Functions != nil { + geminiRequest.Tools = textRequest.Functions + } + shouldAddDummyModelMessage := false for _, message := range textRequest.Messages { content := ChatContent{ diff --git a/relay/adaptor/gemini/model.go b/relay/adaptor/gemini/model.go index 8fd96622..0218766a 100644 --- a/relay/adaptor/gemini/model.go +++ b/relay/adaptor/gemini/model.go @@ -4,7 +4,7 @@ type ChatRequest struct { Contents []ChatContent `json:"contents"` SafetySettings []ChatSafetySettings `json:"safety_settings,omitempty"` GenerationConfig ChatGenerationConfig `json:"generation_config,omitempty"` - Tools []ChatTools `json:"tools,omitempty"` + Tools interface{} `json:"tools,omitempty"` SystemInstruction *ChatContent `json:"system_instruction,omitempty"` }