Browse Source

Merge branch 'main' of https://github.com/Calcium-Ion/new-api

Xyfacai 1 year ago
parent
commit
5b2082ba58

+ 2 - 0
common/constants.go

@@ -213,6 +213,7 @@ const (
 	ChannelTypeDify           = 37
 	ChannelTypeJina           = 38
 	ChannelCloudflare         = 39
+	ChannelTypeSiliconFlow    = 40
 
 	ChannelTypeDummy // this one is only for count, do not add any channel after this
 
@@ -259,4 +260,5 @@ var ChannelBaseURLs = []string{
 	"",                                          //37
 	"https://api.jina.ai",                       //38
 	"https://api.cloudflare.com",                //39
+	"https://api.siliconflow.cn",                //40
 }

+ 8 - 0
common/email-outlook-auth.go

@@ -3,6 +3,7 @@ package common
 import (
 	"errors"
 	"net/smtp"
+	"strings"
 )
 
 type outlookAuth struct {
@@ -30,3 +31,10 @@ func (a *outlookAuth) Next(fromServer []byte, more bool) ([]byte, error) {
 	}
 	return nil, nil
 }
+
+func isOutlookServer(server string) bool {
+	// 兼容多地区的outlook邮箱和ofb邮箱
+	// 其实应该加一个Option来区分是否用LOGIN的方式登录
+	// 先临时兼容一下
+	return strings.Contains(server, "outlook") || strings.Contains(server, "onmicrosoft")
+}

+ 8 - 2
common/email.go

@@ -9,6 +9,11 @@ import (
 	"time"
 )
 
+func generateMessageID() string {
+	domain := strings.Split(SMTPFrom, "@")[1]
+	return fmt.Sprintf("<%d.%s@%s>", time.Now().UnixNano(), GetRandomString(12), domain)
+}
+
 func SendEmail(subject string, receiver string, content string) error {
 	if SMTPFrom == "" { // for compatibility
 		SMTPFrom = SMTPAccount
@@ -18,8 +23,9 @@ func SendEmail(subject string, receiver string, content string) error {
 		"From: %s<%s>\r\n"+
 		"Subject: %s\r\n"+
 		"Date: %s\r\n"+
+		"Message-ID: %s\r\n"+ // 添加 Message-ID 头
 		"Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n",
-		receiver, SystemName, SMTPFrom, encodedSubject, time.Now().Format(time.RFC1123Z), content))
+		receiver, SystemName, SMTPFrom, encodedSubject, time.Now().Format(time.RFC1123Z), generateMessageID(), content))
 	auth := smtp.PlainAuth("", SMTPAccount, SMTPToken, SMTPServer)
 	addr := fmt.Sprintf("%s:%d", SMTPServer, SMTPPort)
 	to := strings.Split(receiver, ";")
@@ -62,7 +68,7 @@ func SendEmail(subject string, receiver string, content string) error {
 		if err != nil {
 			return err
 		}
-	} else if strings.HasSuffix(SMTPAccount, "outlook.com") {
+	} else if isOutlookServer(SMTPAccount) {
 		auth = LoginAuth(SMTPAccount, SMTPToken)
 		err = smtp.SendMail(addr, auth, SMTPAccount, to, mail)
 	} else {

+ 20 - 8
common/model-ratio.go

@@ -23,10 +23,11 @@ const (
 
 var defaultModelRatio = map[string]float64{
 	//"midjourney":                50,
-	"gpt-4-gizmo-*": 15,
-	"gpt-4-all":     15,
-	"gpt-4o-all":    15,
-	"gpt-4":         15,
+	"gpt-4-gizmo-*":  15,
+	"gpt-4o-gizmo-*": 2.5,
+	"gpt-4-all":      15,
+	"gpt-4o-all":     15,
+	"gpt-4":          15,
 	//"gpt-4-0314":                   15, //deprecated
 	"gpt-4-0613": 15,
 	"gpt-4-32k":  30,
@@ -37,6 +38,7 @@ var defaultModelRatio = map[string]float64{
 	"gpt-4-turbo-preview":       5,    // $0.01 / 1K tokens
 	"gpt-4-vision-preview":      5,    // $0.01 / 1K tokens
 	"gpt-4-1106-vision-preview": 5,    // $0.01 / 1K tokens
+	"chatgpt-4o-latest":         2.5,  // $0.01 / 1K tokens
 	"gpt-4o":                    2.5,  // $0.01 / 1K tokens
 	"gpt-4o-2024-05-13":         2.5,  // $0.01 / 1K tokens
 	"gpt-4o-2024-08-06":         1.25, // $0.01 / 1K tokens
@@ -186,8 +188,8 @@ var defaultModelPrice = map[string]float64{
 }
 
 var (
-	modelPriceMap      = make(map[string]float64)
-	modelPriceMapMutex = sync.RWMutex{}
+	modelPriceMap      map[string]float64 = nil
+	modelPriceMapMutex                    = sync.RWMutex{}
 )
 var (
 	modelRatioMap      map[string]float64 = nil
@@ -196,8 +198,9 @@ var (
 
 var CompletionRatio map[string]float64 = nil
 var defaultCompletionRatio = map[string]float64{
-	"gpt-4-gizmo-*": 2,
-	"gpt-4-all":     2,
+	"gpt-4-gizmo-*":  2,
+	"gpt-4o-gizmo-*": 3,
+	"gpt-4-all":      2,
 }
 
 func GetModelPriceMap() map[string]float64 {
@@ -231,6 +234,9 @@ func GetModelPrice(name string, printErr bool) (float64, bool) {
 	if strings.HasPrefix(name, "gpt-4-gizmo") {
 		name = "gpt-4-gizmo-*"
 	}
+	if strings.HasPrefix(name, "gpt-4o-gizmo") {
+		name = "gpt-4o-gizmo-*"
+	}
 	price, ok := modelPriceMap[name]
 	if !ok {
 		if printErr {
@@ -311,6 +317,9 @@ func GetCompletionRatio(name string) float64 {
 	if strings.HasPrefix(name, "gpt-4-gizmo") {
 		name = "gpt-4-gizmo-*"
 	}
+	if strings.HasPrefix(name, "gpt-4o-gizmo") {
+		name = "gpt-4o-gizmo-*"
+	}
 	if strings.HasPrefix(name, "gpt-3.5") {
 		if name == "gpt-3.5-turbo" || strings.HasSuffix(name, "0125") {
 			// https://openai.com/blog/new-embedding-models-and-api-updates
@@ -334,6 +343,9 @@ func GetCompletionRatio(name string) float64 {
 		}
 		return 2
 	}
+	if name == "chatgpt-4o-latest" {
+		return 3
+	}
 	if strings.Contains(name, "claude-instant-1") {
 		return 3
 	} else if strings.Contains(name, "claude-2") {

+ 20 - 10
controller/log.go

@@ -1,18 +1,19 @@
 package controller
 
 import (
-	"github.com/gin-gonic/gin"
 	"net/http"
 	"one-api/common"
 	"one-api/model"
 	"strconv"
+
+	"github.com/gin-gonic/gin"
 )
 
 func GetAllLogs(c *gin.Context) {
 	p, _ := strconv.Atoi(c.Query("p"))
 	pageSize, _ := strconv.Atoi(c.Query("page_size"))
-	if p < 0 {
-		p = 0
+	if p < 1 {
+		p = 1
 	}
 	if pageSize < 0 {
 		pageSize = common.ItemsPerPage
@@ -24,7 +25,7 @@ func GetAllLogs(c *gin.Context) {
 	tokenName := c.Query("token_name")
 	modelName := c.Query("model_name")
 	channel, _ := strconv.Atoi(c.Query("channel"))
-	logs, err := model.GetAllLogs(logType, startTimestamp, endTimestamp, modelName, username, tokenName, p*pageSize, pageSize, channel)
+	logs, total, err := model.GetAllLogs(logType, startTimestamp, endTimestamp, modelName, username, tokenName, (p-1)*pageSize, pageSize, channel)
 	if err != nil {
 		c.JSON(http.StatusOK, gin.H{
 			"success": false,
@@ -35,16 +36,20 @@ func GetAllLogs(c *gin.Context) {
 	c.JSON(http.StatusOK, gin.H{
 		"success": true,
 		"message": "",
-		"data":    logs,
+		"data": map[string]any{
+			"items":     logs,
+			"total":     total,
+			"page":      p,
+			"page_size": pageSize,
+		},
 	})
-	return
 }
 
 func GetUserLogs(c *gin.Context) {
 	p, _ := strconv.Atoi(c.Query("p"))
 	pageSize, _ := strconv.Atoi(c.Query("page_size"))
-	if p < 0 {
-		p = 0
+	if p < 1 {
+		p = 1
 	}
 	if pageSize < 0 {
 		pageSize = common.ItemsPerPage
@@ -58,7 +63,7 @@ func GetUserLogs(c *gin.Context) {
 	endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64)
 	tokenName := c.Query("token_name")
 	modelName := c.Query("model_name")
-	logs, err := model.GetUserLogs(userId, logType, startTimestamp, endTimestamp, modelName, tokenName, p*pageSize, pageSize)
+	logs, total, err := model.GetUserLogs(userId, logType, startTimestamp, endTimestamp, modelName, tokenName, (p-1)*pageSize, pageSize)
 	if err != nil {
 		c.JSON(http.StatusOK, gin.H{
 			"success": false,
@@ -69,7 +74,12 @@ func GetUserLogs(c *gin.Context) {
 	c.JSON(http.StatusOK, gin.H{
 		"success": true,
 		"message": "",
-		"data":    logs,
+		"data": map[string]any{
+			"items":     logs,
+			"total":     total,
+			"page":      p,
+			"page_size": pageSize,
+		},
 	})
 	return
 }

+ 8 - 5
dto/rerank.go

@@ -1,14 +1,17 @@
 package dto
 
 type RerankRequest struct {
-	Documents []any  `json:"documents"`
-	Query     string `json:"query"`
-	Model     string `json:"model"`
-	TopN      int    `json:"top_n"`
+	Documents       []any  `json:"documents"`
+	Query           string `json:"query"`
+	Model           string `json:"model"`
+	TopN            int    `json:"top_n"`
+	ReturnDocuments bool   `json:"return_documents,omitempty"`
+	MaxChunkPerDoc  int    `json:"max_chunk_per_doc,omitempty"`
+	OverLapTokens   int    `json:"overlap_tokens,omitempty"`
 }
 
 type RerankResponseDocument struct {
-	Document       any     `json:"document"`
+	Document       any     `json:"document,omitempty"`
 	Index          int     `json:"index"`
 	RelevanceScore float64 `json:"relevance_score"`
 }

+ 5 - 0
main.go

@@ -42,6 +42,11 @@ func main() {
 	if err != nil {
 		common.FatalLog("failed to initialize database: " + err.Error())
 	}
+	// Initialize SQL Database
+	err = model.InitLogDB()
+	if err != nil {
+		common.FatalLog("failed to initialize database: " + err.Error())
+	}
 	defer func() {
 		err := model.CloseDB()
 		if err != nil {

+ 3 - 0
model/cache.go

@@ -270,6 +270,9 @@ func CacheGetRandomSatisfiedChannel(group string, model string, retry int) (*Cha
 	if strings.HasPrefix(model, "gpt-4-gizmo") {
 		model = "gpt-4-gizmo-*"
 	}
+	if strings.HasPrefix(model, "gpt-4o-gizmo") {
+		model = "gpt-4o-gizmo-*"
+	}
 
 	// if memory cache is disabled, get channel directly from database
 	if !common.MemoryCacheEnabled {

+ 31 - 19
model/log.go

@@ -3,11 +3,12 @@ package model
 import (
 	"context"
 	"fmt"
-	"github.com/bytedance/gopkg/util/gopool"
-	"gorm.io/gorm"
 	"one-api/common"
 	"strings"
 	"time"
+
+	"github.com/bytedance/gopkg/util/gopool"
+	"gorm.io/gorm"
 )
 
 type Log struct {
@@ -38,7 +39,7 @@ const (
 )
 
 func GetLogByKey(key string) (logs []*Log, err error) {
-	err = DB.Joins("left join tokens on tokens.id = logs.token_id").Where("tokens.key = ?", strings.TrimPrefix(key, "sk-")).Find(&logs).Error
+	err = LOG_DB.Joins("left join tokens on tokens.id = logs.token_id").Where("tokens.key = ?", strings.TrimPrefix(key, "sk-")).Find(&logs).Error
 	return logs, err
 }
 
@@ -54,7 +55,7 @@ func RecordLog(userId int, logType int, content string) {
 		Type:      logType,
 		Content:   content,
 	}
-	err := DB.Create(log).Error
+	err := LOG_DB.Create(log).Error
 	if err != nil {
 		common.SysError("failed to record log: " + err.Error())
 	}
@@ -84,7 +85,7 @@ func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptToke
 		IsStream:         isStream,
 		Other:            otherStr,
 	}
-	err := DB.Create(log).Error
+	err := LOG_DB.Create(log).Error
 	if err != nil {
 		common.LogError(ctx, "failed to record log: "+err.Error())
 	}
@@ -95,12 +96,12 @@ func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptToke
 	}
 }
 
-func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, startIdx int, num int, channel int) (logs []*Log, err error) {
+func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, startIdx int, num int, channel int) (logs []*Log, total int64, err error) {
 	var tx *gorm.DB
 	if logType == LogTypeUnknown {
-		tx = DB
+		tx = LOG_DB
 	} else {
-		tx = DB.Where("type = ?", logType)
+		tx = LOG_DB.Where("type = ?", logType)
 	}
 	if modelName != "" {
 		tx = tx.Where("model_name like ?", modelName)
@@ -120,16 +121,23 @@ func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName
 	if channel != 0 {
 		tx = tx.Where("channel_id = ?", channel)
 	}
+	err = tx.Model(&Log{}).Count(&total).Error
+	if err != nil {
+		return nil, 0, err
+	}
 	err = tx.Order("id desc").Limit(num).Offset(startIdx).Find(&logs).Error
-	return logs, err
+	if err != nil {
+		return nil, 0, err
+	}
+	return logs, total, err
 }
 
-func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int64, modelName string, tokenName string, startIdx int, num int) (logs []*Log, err error) {
+func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int64, modelName string, tokenName string, startIdx int, num int) (logs []*Log, total int64, err error) {
 	var tx *gorm.DB
 	if logType == LogTypeUnknown {
-		tx = DB.Where("user_id = ?", userId)
+		tx = LOG_DB.Where("user_id = ?", userId)
 	} else {
-		tx = DB.Where("user_id = ? and type = ?", userId, logType)
+		tx = LOG_DB.Where("user_id = ? and type = ?", userId, logType)
 	}
 	if modelName != "" {
 		tx = tx.Where("model_name like ?", modelName)
@@ -143,6 +151,10 @@ func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int
 	if endTimestamp != 0 {
 		tx = tx.Where("created_at <= ?", endTimestamp)
 	}
+	err = tx.Model(&Log{}).Count(&total).Error
+	if err != nil {
+		return nil, 0, err
+	}
 	err = tx.Order("id desc").Limit(num).Offset(startIdx).Omit("id").Find(&logs).Error
 	for i := range logs {
 		var otherMap map[string]interface{}
@@ -153,16 +165,16 @@ func GetUserLogs(userId int, logType int, startTimestamp int64, endTimestamp int
 		}
 		logs[i].Other = common.MapToJsonStr(otherMap)
 	}
-	return logs, err
+	return logs, total, err
 }
 
 func SearchAllLogs(keyword string) (logs []*Log, err error) {
-	err = DB.Where("type = ? or content LIKE ?", keyword, keyword+"%").Order("id desc").Limit(common.MaxRecentItems).Find(&logs).Error
+	err = LOG_DB.Where("type = ? or content LIKE ?", keyword, keyword+"%").Order("id desc").Limit(common.MaxRecentItems).Find(&logs).Error
 	return logs, err
 }
 
 func SearchUserLogs(userId int, keyword string) (logs []*Log, err error) {
-	err = DB.Where("user_id = ? and type = ?", userId, keyword).Order("id desc").Limit(common.MaxRecentItems).Omit("id").Find(&logs).Error
+	err = LOG_DB.Where("user_id = ? and type = ?", userId, keyword).Order("id desc").Limit(common.MaxRecentItems).Omit("id").Find(&logs).Error
 	return logs, err
 }
 
@@ -173,10 +185,10 @@ type Stat struct {
 }
 
 func SumUsedQuota(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, channel int) (stat Stat) {
-	tx := DB.Table("logs").Select("sum(quota) quota")
+	tx := LOG_DB.Table("logs").Select("sum(quota) quota")
 
 	// 为rpm和tpm创建单独的查询
-	rpmTpmQuery := DB.Table("logs").Select("count(*) rpm, sum(prompt_tokens) + sum(completion_tokens) tpm")
+	rpmTpmQuery := LOG_DB.Table("logs").Select("count(*) rpm, sum(prompt_tokens) + sum(completion_tokens) tpm")
 
 	if username != "" {
 		tx = tx.Where("username = ?", username)
@@ -215,7 +227,7 @@ func SumUsedQuota(logType int, startTimestamp int64, endTimestamp int64, modelNa
 }
 
 func SumUsedToken(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string) (token int) {
-	tx := DB.Table("logs").Select("ifnull(sum(prompt_tokens),0) + ifnull(sum(completion_tokens),0)")
+	tx := LOG_DB.Table("logs").Select("ifnull(sum(prompt_tokens),0) + ifnull(sum(completion_tokens),0)")
 	if username != "" {
 		tx = tx.Where("username = ?", username)
 	}
@@ -236,6 +248,6 @@ func SumUsedToken(logType int, startTimestamp int64, endTimestamp int64, modelNa
 }
 
 func DeleteOldLog(targetTimestamp int64) (int64, error) {
-	result := DB.Where("created_at < ?", targetTimestamp).Delete(&Log{})
+	result := LOG_DB.Where("created_at < ?", targetTimestamp).Delete(&Log{})
 	return result.RowsAffected, result.Error
 }

+ 116 - 47
model/main.go

@@ -15,6 +15,8 @@ import (
 
 var DB *gorm.DB
 
+var LOG_DB *gorm.DB
+
 func createRootAccountIfNeed() error {
 	var user User
 	//if user.Status != common.UserStatusEnabled {
@@ -38,9 +40,9 @@ func createRootAccountIfNeed() error {
 	return nil
 }
 
-func chooseDB() (*gorm.DB, error) {
-	if os.Getenv("SQL_DSN") != "" {
-		dsn := os.Getenv("SQL_DSN")
+func chooseDB(envName string) (*gorm.DB, error) {
+	dsn := os.Getenv(envName)
+	if dsn != "" {
 		if strings.HasPrefix(dsn, "postgres://") {
 			// Use PostgreSQL
 			common.SysLog("using PostgreSQL as database")
@@ -52,6 +54,13 @@ func chooseDB() (*gorm.DB, error) {
 				PrepareStmt: true, // precompile SQL
 			})
 		}
+		if strings.HasPrefix(dsn, "local") {
+			common.SysLog("SQL_DSN not set, using SQLite as database")
+			common.UsingSQLite = true
+			return gorm.Open(sqlite.Open(common.SQLitePath), &gorm.Config{
+				PrepareStmt: true, // precompile SQL
+			})
+		}
 		// Use MySQL
 		common.SysLog("using MySQL as database")
 		// check parseTime
@@ -76,7 +85,7 @@ func chooseDB() (*gorm.DB, error) {
 }
 
 func InitDB() (err error) {
-	db, err := chooseDB()
+	db, err := chooseDB("SQL_DSN")
 	if err == nil {
 		if common.DebugEnabled {
 			db = db.Debug()
@@ -100,52 +109,44 @@ func InitDB() (err error) {
 		//	_, _ = sqlDB.Exec("ALTER TABLE midjourneys MODIFY status VARCHAR(20);")   // TODO: delete this line when most users have upgraded
 		//}
 		common.SysLog("database migration started")
-		err = db.AutoMigrate(&Channel{})
-		if err != nil {
-			return err
-		}
-		err = db.AutoMigrate(&Token{})
-		if err != nil {
-			return err
-		}
-		err = db.AutoMigrate(&User{})
-		if err != nil {
-			return err
-		}
-		err = db.AutoMigrate(&Option{})
-		if err != nil {
-			return err
-		}
-		err = db.AutoMigrate(&Redemption{})
-		if err != nil {
-			return err
-		}
-		err = db.AutoMigrate(&Ability{})
-		if err != nil {
-			return err
-		}
-		err = db.AutoMigrate(&Log{})
-		if err != nil {
-			return err
-		}
-		err = db.AutoMigrate(&Midjourney{})
-		if err != nil {
-			return err
-		}
-		err = db.AutoMigrate(&TopUp{})
-		if err != nil {
-			return err
+		err = migrateDB()
+		return err
+	} else {
+		common.FatalLog(err)
+	}
+	return err
+}
+
+func InitLogDB() (err error) {
+	if os.Getenv("LOG_SQL_DSN") == "" {
+		LOG_DB = DB
+		return
+	}
+	db, err := chooseDB("LOG_SQL_DSN")
+	if err == nil {
+		if common.DebugEnabled {
+			db = db.Debug()
 		}
-		err = db.AutoMigrate(&QuotaData{})
+		LOG_DB = db
+		sqlDB, err := LOG_DB.DB()
 		if err != nil {
 			return err
 		}
-		err = db.AutoMigrate(&Task{})
-		if err != nil {
-			return err
+		sqlDB.SetMaxIdleConns(common.GetEnvOrDefault("SQL_MAX_IDLE_CONNS", 100))
+		sqlDB.SetMaxOpenConns(common.GetEnvOrDefault("SQL_MAX_OPEN_CONNS", 1000))
+		sqlDB.SetConnMaxLifetime(time.Second * time.Duration(common.GetEnvOrDefault("SQL_MAX_LIFETIME", 60)))
+
+		if !common.IsMasterNode {
+			return nil
 		}
-		common.SysLog("database migrated")
-		err = createRootAccountIfNeed()
+		//if common.UsingMySQL {
+		//	_, _ = sqlDB.Exec("DROP INDEX idx_channels_key ON channels;")             // TODO: delete this line when most users have upgraded
+		//	_, _ = sqlDB.Exec("ALTER TABLE midjourneys MODIFY action VARCHAR(40);")   // TODO: delete this line when most users have upgraded
+		//	_, _ = sqlDB.Exec("ALTER TABLE midjourneys MODIFY progress VARCHAR(30);") // TODO: delete this line when most users have upgraded
+		//	_, _ = sqlDB.Exec("ALTER TABLE midjourneys MODIFY status VARCHAR(20);")   // TODO: delete this line when most users have upgraded
+		//}
+		common.SysLog("database migration started")
+		err = migrateLOGDB()
 		return err
 	} else {
 		common.FatalLog(err)
@@ -153,8 +154,66 @@ func InitDB() (err error) {
 	return err
 }
 
-func CloseDB() error {
-	sqlDB, err := DB.DB()
+func migrateDB() error {
+	err := DB.AutoMigrate(&Channel{})
+	if err != nil {
+		return err
+	}
+	err = DB.AutoMigrate(&Token{})
+	if err != nil {
+		return err
+	}
+	err = DB.AutoMigrate(&User{})
+	if err != nil {
+		return err
+	}
+	err = DB.AutoMigrate(&Option{})
+	if err != nil {
+		return err
+	}
+	err = DB.AutoMigrate(&Redemption{})
+	if err != nil {
+		return err
+	}
+	err = DB.AutoMigrate(&Ability{})
+	if err != nil {
+		return err
+	}
+	err = DB.AutoMigrate(&Log{})
+	if err != nil {
+		return err
+	}
+	err = DB.AutoMigrate(&Midjourney{})
+	if err != nil {
+		return err
+	}
+	err = DB.AutoMigrate(&TopUp{})
+	if err != nil {
+		return err
+	}
+	err = DB.AutoMigrate(&QuotaData{})
+	if err != nil {
+		return err
+	}
+	err = DB.AutoMigrate(&Task{})
+	if err != nil {
+		return err
+	}
+	common.SysLog("database migrated")
+	err = createRootAccountIfNeed()
+	return err
+}
+
+func migrateLOGDB() error {
+	var err error
+	if err = LOG_DB.AutoMigrate(&Log{}); err != nil {
+		return err
+	}
+	return nil
+}
+
+func closeDB(db *gorm.DB) error {
+	sqlDB, err := db.DB()
 	if err != nil {
 		return err
 	}
@@ -162,6 +221,16 @@ func CloseDB() error {
 	return err
 }
 
+func CloseDB() error {
+	if LOG_DB != DB {
+		err := closeDB(LOG_DB)
+		if err != nil {
+			return err
+		}
+	}
+	return closeDB(DB)
+}
+
 var (
 	lastPingTime time.Time
 	pingMutex    sync.Mutex

+ 3 - 3
relay/channel/claude/dto.go

@@ -31,9 +31,9 @@ type ClaudeMessage struct {
 }
 
 type Tool struct {
-	Name        string      `json:"name"`
-	Description string      `json:"description,omitempty"`
-	InputSchema InputSchema `json:"input_schema"`
+	Name        string                 `json:"name"`
+	Description string                 `json:"description,omitempty"`
+	InputSchema map[string]interface{} `json:"input_schema"`
 }
 
 type InputSchema struct {

+ 13 - 7
relay/channel/claude/relay-claude.go

@@ -63,15 +63,21 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*ClaudeR
 
 	for _, tool := range textRequest.Tools {
 		if params, ok := tool.Function.Parameters.(map[string]any); ok {
-			claudeTools = append(claudeTools, Tool{
+			claudeTool := Tool{
 				Name:        tool.Function.Name,
 				Description: tool.Function.Description,
-				InputSchema: InputSchema{
-					Type:       params["type"].(string),
-					Properties: params["properties"],
-					Required:   params["required"],
-				},
-			})
+			}
+			claudeTool.InputSchema = make(map[string]interface{})
+			claudeTool.InputSchema["type"] = params["type"].(string)
+			claudeTool.InputSchema["properties"] = params["properties"]
+			claudeTool.InputSchema["required"] = params["required"]
+			for s, a := range params {
+				if s == "type" || s == "properties" || s == "required" {
+					continue
+				}
+				claudeTool.InputSchema[s] = a
+			}
+			claudeTools = append(claudeTools, claudeTool)
 		}
 	}
 

+ 1 - 0
relay/channel/openai/constant.go

@@ -8,6 +8,7 @@ var ModelList = []string{
 	"gpt-4-32k", "gpt-4-32k-0613",
 	"gpt-4-turbo-preview", "gpt-4-turbo", "gpt-4-turbo-2024-04-09",
 	"gpt-4-vision-preview",
+	"chatgpt-4o-latest",
 	"gpt-4o", "gpt-4o-2024-05-13", "gpt-4o-2024-08-06",
 	"gpt-4o-mini", "gpt-4o-mini-2024-07-18",
 	"text-embedding-ada-002", "text-embedding-3-small", "text-embedding-3-large",

+ 80 - 0
relay/channel/siliconflow/adaptor.go

@@ -0,0 +1,80 @@
+package siliconflow
+
+import (
+	"errors"
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"io"
+	"net/http"
+	"one-api/dto"
+	"one-api/relay/channel"
+	"one-api/relay/channel/openai"
+	relaycommon "one-api/relay/common"
+	"one-api/relay/constant"
+)
+
+type Adaptor struct {
+}
+
+func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) {
+	//TODO implement me
+	return nil, errors.New("not implemented")
+}
+
+func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) {
+	//TODO implement me
+	return nil, errors.New("not implemented")
+}
+
+func (a *Adaptor) Init(info *relaycommon.RelayInfo) {
+}
+
+func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
+	if info.RelayMode == constant.RelayModeRerank {
+		return fmt.Sprintf("%s/v1/rerank", info.BaseUrl), nil
+	} else if info.RelayMode == constant.RelayModeEmbeddings {
+		return fmt.Sprintf("%s/v1/embeddings ", info.BaseUrl), nil
+	} else if info.RelayMode == constant.RelayModeChatCompletions {
+		return fmt.Sprintf("%s/v1/chat/completions", info.BaseUrl), nil
+	}
+	return "", errors.New("invalid relay mode")
+}
+
+func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, info *relaycommon.RelayInfo) error {
+	channel.SetupApiRequestHeader(info, c, req)
+	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", info.ApiKey))
+	return nil
+}
+
+func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) {
+	return request, nil
+}
+
+func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (*http.Response, error) {
+	return channel.DoApiRequest(a, c, info, requestBody)
+}
+
+func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) {
+	return request, nil
+}
+
+func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage *dto.Usage, err *dto.OpenAIErrorWithStatusCode) {
+	if info.RelayMode == constant.RelayModeRerank {
+		err, usage = siliconflowRerankHandler(c, resp)
+	} else if info.RelayMode == constant.RelayModeChatCompletions {
+		if info.IsStream {
+			err, usage = openai.OaiStreamHandler(c, resp, info)
+		} else {
+			err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName)
+		}
+	}
+	return
+}
+
+func (a *Adaptor) GetModelList() []string {
+	return ModelList
+}
+
+func (a *Adaptor) GetChannelName() string {
+	return ChannelName
+}

+ 51 - 0
relay/channel/siliconflow/constant.go

@@ -0,0 +1,51 @@
+package siliconflow
+
+var ModelList = []string{
+	"THUDM/glm-4-9b-chat",
+	//"stabilityai/stable-diffusion-xl-base-1.0",
+	//"TencentARC/PhotoMaker",
+	"InstantX/InstantID",
+	//"stabilityai/stable-diffusion-2-1",
+	//"stabilityai/sd-turbo",
+	//"stabilityai/sdxl-turbo",
+	"ByteDance/SDXL-Lightning",
+	"deepseek-ai/deepseek-llm-67b-chat",
+	"Qwen/Qwen1.5-14B-Chat",
+	"Qwen/Qwen1.5-7B-Chat",
+	"Qwen/Qwen1.5-110B-Chat",
+	"Qwen/Qwen1.5-32B-Chat",
+	"01-ai/Yi-1.5-6B-Chat",
+	"01-ai/Yi-1.5-9B-Chat-16K",
+	"01-ai/Yi-1.5-34B-Chat-16K",
+	"THUDM/chatglm3-6b",
+	"deepseek-ai/DeepSeek-V2-Chat",
+	"Qwen/Qwen2-72B-Instruct",
+	"Qwen/Qwen2-7B-Instruct",
+	"Qwen/Qwen2-57B-A14B-Instruct",
+	//"stabilityai/stable-diffusion-3-medium",
+	"deepseek-ai/DeepSeek-Coder-V2-Instruct",
+	"Qwen/Qwen2-1.5B-Instruct",
+	"internlm/internlm2_5-7b-chat",
+	"BAAI/bge-large-en-v1.5",
+	"BAAI/bge-large-zh-v1.5",
+	"Pro/Qwen/Qwen2-7B-Instruct",
+	"Pro/Qwen/Qwen2-1.5B-Instruct",
+	"Pro/Qwen/Qwen1.5-7B-Chat",
+	"Pro/THUDM/glm-4-9b-chat",
+	"Pro/THUDM/chatglm3-6b",
+	"Pro/01-ai/Yi-1.5-9B-Chat-16K",
+	"Pro/01-ai/Yi-1.5-6B-Chat",
+	"Pro/google/gemma-2-9b-it",
+	"Pro/internlm/internlm2_5-7b-chat",
+	"Pro/meta-llama/Meta-Llama-3-8B-Instruct",
+	"Pro/mistralai/Mistral-7B-Instruct-v0.2",
+	"black-forest-labs/FLUX.1-schnell",
+	"iic/SenseVoiceSmall",
+	"netease-youdao/bce-embedding-base_v1",
+	"BAAI/bge-m3",
+	"internlm/internlm2_5-20b-chat",
+	"Qwen/Qwen2-Math-72B-Instruct",
+	"netease-youdao/bce-reranker-base_v1",
+	"BAAI/bge-reranker-v2-m3",
+}
+var ChannelName = "siliconflow"

+ 17 - 0
relay/channel/siliconflow/dto.go

@@ -0,0 +1,17 @@
+package siliconflow
+
+import "one-api/dto"
+
+type SFTokens struct {
+	InputTokens  int `json:"input_tokens"`
+	OutputTokens int `json:"output_tokens"`
+}
+
+type SFMeta struct {
+	Tokens SFTokens `json:"tokens"`
+}
+
+type SFRerankResponse struct {
+	Results []dto.RerankResponseDocument `json:"results"`
+	Meta    SFMeta                       `json:"meta"`
+}

+ 44 - 0
relay/channel/siliconflow/relay-siliconflow.go

@@ -0,0 +1,44 @@
+package siliconflow
+
+import (
+	"encoding/json"
+	"github.com/gin-gonic/gin"
+	"io"
+	"net/http"
+	"one-api/dto"
+	"one-api/service"
+)
+
+func siliconflowRerankHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
+	responseBody, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil
+	}
+	err = resp.Body.Close()
+	if err != nil {
+		return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
+	}
+	var siliconflowResp SFRerankResponse
+	err = json.Unmarshal(responseBody, &siliconflowResp)
+	if err != nil {
+		return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
+	}
+	usage := &dto.Usage{
+		PromptTokens:     siliconflowResp.Meta.Tokens.InputTokens,
+		CompletionTokens: siliconflowResp.Meta.Tokens.OutputTokens,
+		TotalTokens:      siliconflowResp.Meta.Tokens.InputTokens + siliconflowResp.Meta.Tokens.OutputTokens,
+	}
+	rerankResp := &dto.RerankResponse{
+		Results: siliconflowResp.Results,
+		Usage:   *usage,
+	}
+
+	jsonResponse, err := json.Marshal(rerankResp)
+	if err != nil {
+		return service.OpenAIErrorWrapper(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
+}

+ 3 - 0
relay/constant/api_type.go

@@ -23,6 +23,7 @@ const (
 	APITypeDify
 	APITypeJina
 	APITypeCloudflare
+	APITypeSiliconFlow
 
 	APITypeDummy // this one is only for count, do not add any channel after this
 )
@@ -66,6 +67,8 @@ func ChannelType2APIType(channelType int) (int, bool) {
 		apiType = APITypeJina
 	case common.ChannelCloudflare:
 		apiType = APITypeCloudflare
+	case common.ChannelTypeSiliconFlow:
+		apiType = APITypeSiliconFlow
 	}
 	if apiType == -1 {
 		return APITypeOpenAI, false

+ 5 - 1
relay/relay-text.go

@@ -317,7 +317,7 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, modelN
 	totalTokens := promptTokens + completionTokens
 	var logContent string
 	if !usePrice {
-		logContent = fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f,补全倍率 %.2f", modelRatio, groupRatio, completionRatio)
+		logContent = fmt.Sprintf("模型倍率 %.2f,补全倍率 %.2f,分组倍率 %.2f", modelRatio, completionRatio, groupRatio)
 	} else {
 		logContent = fmt.Sprintf("模型价格 %.2f,分组倍率 %.2f", modelPrice, groupRatio)
 	}
@@ -354,6 +354,10 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, modelN
 		logModel = "gpt-4-gizmo-*"
 		logContent += fmt.Sprintf(",模型 %s", modelName)
 	}
+	if strings.HasPrefix(logModel, "gpt-4o-gizmo") {
+		logModel = "gpt-4o-gizmo-*"
+		logContent += fmt.Sprintf(",模型 %s", modelName)
+	}
 	if extraContent != "" {
 		logContent += ", " + extraContent
 	}

+ 3 - 0
relay/relay_adaptor.go

@@ -16,6 +16,7 @@ import (
 	"one-api/relay/channel/openai"
 	"one-api/relay/channel/palm"
 	"one-api/relay/channel/perplexity"
+	"one-api/relay/channel/siliconflow"
 	"one-api/relay/channel/task/suno"
 	"one-api/relay/channel/tencent"
 	"one-api/relay/channel/xunfei"
@@ -62,6 +63,8 @@ func GetAdaptor(apiType int) channel.Adaptor {
 		return &jina.Adaptor{}
 	case constant.APITypeCloudflare:
 		return &cloudflare.Adaptor{}
+	case constant.APITypeSiliconFlow:
+		return &siliconflow.Adaptor{}
 	}
 	return nil
 }

+ 17 - 0
relay/relay_rerank.go

@@ -38,6 +38,23 @@ func RerankHelper(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusCode
 	if len(rerankRequest.Documents) == 0 {
 		return service.OpenAIErrorWrapperLocal(fmt.Errorf("documents is empty"), "invalid_documents", http.StatusBadRequest)
 	}
+
+	// map model name
+	modelMapping := c.GetString("model_mapping")
+	//isModelMapped := false
+	if modelMapping != "" && modelMapping != "{}" {
+		modelMap := make(map[string]string)
+		err := json.Unmarshal([]byte(modelMapping), &modelMap)
+		if err != nil {
+			return service.OpenAIErrorWrapperLocal(err, "unmarshal_model_mapping_failed", http.StatusInternalServerError)
+		}
+		if modelMap[rerankRequest.Model] != "" {
+			rerankRequest.Model = modelMap[rerankRequest.Model]
+			// set upstream model name
+			//isModelMapped = true
+		}
+	}
+
 	relayInfo.UpstreamModelName = rerankRequest.Model
 	modelPrice, success := common.GetModelPrice(rerankRequest.Model, false)
 	groupRatio := common.GetGroupRatio(relayInfo.Group)

+ 4 - 8
service/error.go

@@ -28,13 +28,11 @@ func MidjourneyErrorWithStatusCodeWrapper(code int, desc string, statusCode int)
 // OpenAIErrorWrapper wraps an error into an OpenAIErrorWithStatusCode
 func OpenAIErrorWrapper(err error, code string, statusCode int) *dto.OpenAIErrorWithStatusCode {
 	text := err.Error()
-	// 定义一个正则表达式匹配URL
-	if strings.Contains(text, "Post") || strings.Contains(text, "dial") {
+	lowerText := strings.ToLower(text)
+	if strings.Contains(lowerText, "post") || strings.Contains(lowerText, "dial") || strings.Contains(lowerText, "http") {
 		common.SysLog(fmt.Sprintf("error: %s", text))
 		text = "请求上游地址失败"
 	}
-	//避免暴露内部错误
-
 	openAIError := dto.OpenAIError{
 		Message: text,
 		Type:    "new_api_error",
@@ -113,14 +111,12 @@ func TaskErrorWrapperLocal(err error, code string, statusCode int) *dto.TaskErro
 
 func TaskErrorWrapper(err error, code string, statusCode int) *dto.TaskError {
 	text := err.Error()
-
-	// 定义一个正则表达式匹配URL
-	if strings.Contains(text, "Post") || strings.Contains(text, "dial") {
+	lowerText := strings.ToLower(text)
+	if strings.Contains(lowerText, "post") || strings.Contains(lowerText, "dial") || strings.Contains(lowerText, "http") {
 		common.SysLog(fmt.Sprintf("error: %s", text))
 		text = "请求上游地址失败"
 	}
 	//避免暴露内部错误
-
 	taskError := &dto.TaskError{
 		Code:       code,
 		Message:    text,

+ 1 - 1
web/package.json

@@ -50,7 +50,7 @@
     ]
   },
   "devDependencies": {
-    "@so1ve/prettier-config": "^2.0.0",
+    "@so1ve/prettier-config": "^3.1.0",
     "@vitejs/plugin-react": "^4.2.1",
     "prettier": "^3.0.0",
     "typescript": "4.4.2",

+ 52 - 52
web/pnpm-lock.yaml

@@ -13,10 +13,10 @@ importers:
         version: 2.53.2([email protected])
       '@douyinfe/semi-ui':
         specifier: ^2.55.3
-        version: 2.55.3([email protected])([email protected])
+        version: 2.55.3([email protected]([email protected]))([email protected])
       '@visactor/react-vchart':
         specifier: ~1.8.8
-        version: 1.8.11([email protected])([email protected])
+        version: 1.8.11([email protected]([email protected]))([email protected])
       '@visactor/vchart':
         specifier: ~1.8.8
         version: 1.8.11
@@ -49,26 +49,26 @@ importers:
         version: 1.0.4
       react-router-dom:
         specifier: ^6.3.0
-        version: 6.22.2([email protected])([email protected])
+        version: 6.22.2([email protected]([email protected]))([email protected])
       react-telegram-login:
         specifier: ^1.1.2
         version: 1.1.2([email protected])
       react-toastify:
         specifier: ^9.0.8
-        version: 9.1.3([email protected])([email protected])
+        version: 9.1.3([email protected]([email protected]))([email protected])
       react-turnstile:
         specifier: ^1.0.5
-        version: 1.1.3([email protected])([email protected])
+        version: 1.1.3([email protected]([email protected]))([email protected])
       semantic-ui-offline:
         specifier: ^2.5.0
         version: 2.5.0
       semantic-ui-react:
         specifier: ^2.1.3
-        version: 2.1.5([email protected])([email protected])
+        version: 2.1.5([email protected]([email protected]))([email protected])
     devDependencies:
       '@so1ve/prettier-config':
-        specifier: ^2.0.0
-        version: 2.0.0([email protected])
+        specifier: ^3.1.0
+        version: 3.1.0([email protected])
       '@vitejs/plugin-react':
         specifier: ^4.2.1
         version: 4.2.1([email protected])
@@ -88,8 +88,8 @@ packages:
     resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
     engines: {node: '>=6.0.0'}
 
-  '@astrojs/compiler@1.8.2':
-    resolution: {integrity: sha512-o/ObKgtMzl8SlpIdzaxFnt7SATKPxu4oIP/1NL+HDJRzxfJcAkOTAb/ZKMRyULbz4q+1t2/DAebs2Z1QairkZw==}
+  '@astrojs/compiler@2.10.2':
+    resolution: {integrity: sha512-bvH+v8AirwpRWCkYJEyWYdc5Cs/BjG2ZTxIJzttHilXgfKJAdW2496KsUQKzf5j2tOHtaHXKKn9hb9WZiBGpEg==}
 
   '@babel/[email protected]':
     resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==}
@@ -578,13 +578,13 @@ packages:
       react: ^16.0.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
 
-  '@so1ve/prettier-config@2.0.0':
-    resolution: {integrity: sha512-s6qsH5Rf4Bl+J0LU9rKmSWe/rYRdsYw0ELyXhDDDqEaTWtah4NpHKJuVWARuKqj0TWLBeWmyWUoIH/Bkp/DHaw==}
+  '@so1ve/prettier-config@3.1.0':
+    resolution: {integrity: sha512-9GJ1yXKBC4DzqCTTaZoBf8zw7WWkVuXcccZt1Aqk4lj6ab/GiNUnjPGajUVYLjaqAEOKqM7jUSUfTjk2JTjCAg==}
     peerDependencies:
       prettier: ^3.0.0
 
-  '@so1ve/prettier-plugin-toml@2.0.0':
-    resolution: {integrity: sha512-GvuFdTqhs3qxbhKTiCXWMXITmNLSdndUp7ql1yJbzzWaGqAdb3UH+R+0ZhtAEctBSx90MWAWW3kkW/Iba02tCg==}
+  '@so1ve/prettier-plugin-toml@3.1.0':
+    resolution: {integrity: sha512-8WZAGjAVNIJlkfWL6wHKxlUuEBY45fdd5qY5bR/Z6r/txgzKXk/r9qi1DTwc17gi/WcNuRrcRugecRT+mWbIYg==}
     peerDependencies:
       prettier: ^3.0.0
 
@@ -1127,12 +1127,12 @@ packages:
     resolution: {integrity: sha512-WxtodH/wWavfw3MR7yK/GrS4pASEQ+iSTkdtSxPJWvqzG55ir5nvbLt9rw5AOiEcqqPCRM92WCtR1rk3TG3JSQ==}
     hasBin: true
 
-  [email protected]3.0:
-    resolution: {integrity: sha512-5HrJNnPmZqTUNoA97zn4gNQv9BgVhv+et03314WpQ9H9N8m2L9OSV798olwmG2YLXPl1iSstlJCR1zB3x5xG4g==}
+  [email protected]4.1:
+    resolution: {integrity: sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==}
     engines: {node: ^14.15.0 || >=16.0.0}
 
-  prettier-plugin-curly-and-jsdoc@2.0.0:
-    resolution: {integrity: sha512-uSjWOWmX8+yrCrfhJSI58ODqtX7lXx07M8JYeOC1hfRv+vCttfiDlZoM27mNChGitJNKI+pCBvMMBYh8JiV0HQ==}
+  prettier-plugin-curly-and-jsdoc@3.1.0:
+    resolution: {integrity: sha512-4QMOHnLlkP2jTRWS0MFH6j+cuOiXLvXOqCLKbtwwVd8PPyq8NenW5AAwfwqiTNHBQG/DmzViPphRrwgN0XkUVQ==}
     peerDependencies:
       prettier: ^3.0.0
 
@@ -1442,7 +1442,7 @@ snapshots:
       '@jridgewell/gen-mapping': 0.3.5
       '@jridgewell/trace-mapping': 0.3.24
 
-  '@astrojs/compiler@1.8.2': {}
+  '@astrojs/compiler@2.10.2': {}
 
   '@babel/[email protected]':
     dependencies:
@@ -1590,7 +1590,7 @@ snapshots:
       react: 18.2.0
       tslib: 2.6.2
 
-  '@dnd-kit/[email protected]([email protected])([email protected])':
+  '@dnd-kit/[email protected]([email protected]([email protected]))([email protected])':
     dependencies:
       '@dnd-kit/accessibility': 3.1.0([email protected])
       '@dnd-kit/utilities': 3.2.2([email protected])
@@ -1598,9 +1598,9 @@ snapshots:
       react-dom: 18.2.0([email protected])
       tslib: 2.6.2
 
-  '@dnd-kit/[email protected](@dnd-kit/[email protected])([email protected])':
+  '@dnd-kit/[email protected](@dnd-kit/[email protected]([email protected]([email protected]))([email protected]))([email protected])':
     dependencies:
-      '@dnd-kit/core': 6.1.0([email protected])([email protected])
+      '@dnd-kit/core': 6.1.0([email protected]([email protected]))([email protected])
       '@dnd-kit/utilities': 3.2.2([email protected])
       react: 18.2.0
       tslib: 2.6.2
@@ -1652,10 +1652,10 @@ snapshots:
     dependencies:
       glob: 7.2.3
 
-  '@douyinfe/[email protected]([email protected])([email protected])':
+  '@douyinfe/[email protected]([email protected]([email protected]))([email protected])':
     dependencies:
-      '@dnd-kit/core': 6.1.0([email protected])([email protected])
-      '@dnd-kit/sortable': 7.0.2(@dnd-kit/[email protected])([email protected])
+      '@dnd-kit/core': 6.1.0([email protected]([email protected]))([email protected])
+      '@dnd-kit/sortable': 7.0.2(@dnd-kit/[email protected]([email protected]([email protected]))([email protected]))([email protected])
       '@dnd-kit/utilities': 3.2.2([email protected])
       '@douyinfe/semi-animation': 2.55.3
       '@douyinfe/semi-animation-react': 2.55.3
@@ -1673,8 +1673,8 @@ snapshots:
       prop-types: 15.8.1
       react: 18.2.0
       react-dom: 18.2.0([email protected])
-      react-resizable: 3.0.5([email protected])([email protected])
-      react-window: 1.8.10([email protected])([email protected])
+      react-resizable: 3.0.5([email protected]([email protected]))([email protected])
+      react-window: 1.8.10([email protected]([email protected]))([email protected])
       scroll-into-view-if-needed: 2.2.31
       utility-types: 3.11.0
 
@@ -1747,13 +1747,13 @@ snapshots:
   '@esbuild/[email protected]':
     optional: true
 
-  '@fluentui/[email protected]([email protected])([email protected])':
+  '@fluentui/[email protected]([email protected]([email protected]))([email protected])':
     dependencies:
       '@babel/runtime': 7.24.0
       react: 18.2.0
       react-dom: 18.2.0([email protected])
 
-  '@fluentui/[email protected]([email protected])([email protected])':
+  '@fluentui/[email protected]([email protected]([email protected]))([email protected])':
     dependencies:
       '@babel/runtime': 7.24.0
       react: 18.2.0
@@ -1871,22 +1871,22 @@ snapshots:
   '@rollup/[email protected]':
     optional: true
 
-  '@semantic-ui-react/[email protected]([email protected])([email protected])':
+  '@semantic-ui-react/[email protected]([email protected]([email protected]))([email protected])':
     dependencies:
       exenv: 1.2.2
       prop-types: 15.8.1
       react: 18.2.0
       react-dom: 18.2.0([email protected])
 
-  '@so1ve/prettier-config@2.0.0([email protected])':
+  '@so1ve/prettier-config@3.1.0([email protected])':
     dependencies:
-      '@so1ve/prettier-plugin-toml': 2.0.0([email protected])
+      '@so1ve/prettier-plugin-toml': 3.1.0([email protected])
       prettier: 3.2.5
-      prettier-plugin-astro: 0.13.0
-      prettier-plugin-curly-and-jsdoc: 2.0.0([email protected])
+      prettier-plugin-astro: 0.14.1
+      prettier-plugin-curly-and-jsdoc: 3.1.0([email protected])
       prettier-plugin-pkgsort: 0.2.1([email protected])
 
-  '@so1ve/prettier-plugin-toml@2.0.0([email protected])':
+  '@so1ve/prettier-plugin-toml@3.1.0([email protected])':
     dependencies:
       prettier: 3.2.5
 
@@ -1951,7 +1951,7 @@ snapshots:
 
   '@types/[email protected]': {}
 
-  '@visactor/[email protected]([email protected])([email protected])':
+  '@visactor/[email protected]([email protected]([email protected]))([email protected])':
     dependencies:
       '@visactor/vchart': 1.8.11
       '@visactor/vgrammar-core': 0.10.11
@@ -2528,13 +2528,13 @@ snapshots:
       sort-object-keys: 1.1.3
       sort-order: 1.1.2
 
-  [email protected]3.0:
+  [email protected]4.1:
     dependencies:
-      '@astrojs/compiler': 1.8.2
+      '@astrojs/compiler': 2.10.2
       prettier: 3.2.5
       sass-formatter: 0.7.9
 
-  prettier-plugin-curly-and-jsdoc@2.0.0([email protected]):
+  prettier-plugin-curly-and-jsdoc@3.1.0([email protected]):
     dependencies:
       prettier: 3.2.5
 
@@ -2559,7 +2559,7 @@ snapshots:
       react: 18.2.0
       scheduler: 0.23.0
 
-  [email protected]([email protected])([email protected]):
+  [email protected]([email protected]([email protected]))([email protected]):
     dependencies:
       clsx: 1.2.1
       prop-types: 15.8.1
@@ -2581,7 +2581,7 @@ snapshots:
 
   [email protected]: {}
 
-  [email protected](@popperjs/[email protected])([email protected])([email protected]):
+  [email protected](@popperjs/[email protected])([email protected]([email protected]))([email protected]):
     dependencies:
       '@popperjs/core': 2.11.8
       react: 18.2.0
@@ -2591,15 +2591,15 @@ snapshots:
 
   [email protected]: {}
 
-  [email protected]([email protected])([email protected]):
+  [email protected]([email protected]([email protected]))([email protected]):
     dependencies:
       prop-types: 15.8.1
       react: 18.2.0
-      react-draggable: 4.4.6([email protected])([email protected])
+      react-draggable: 4.4.6([email protected]([email protected]))([email protected])
     transitivePeerDependencies:
       - react-dom
 
-  [email protected]([email protected])([email protected]):
+  [email protected]([email protected]([email protected]))([email protected]):
     dependencies:
       '@remix-run/router': 1.15.2
       react: 18.2.0
@@ -2615,18 +2615,18 @@ snapshots:
     dependencies:
       react: 18.2.0
 
-  [email protected]([email protected])([email protected]):
+  [email protected]([email protected]([email protected]))([email protected]):
     dependencies:
       clsx: 1.2.1
       react: 18.2.0
       react-dom: 18.2.0([email protected])
 
-  [email protected]([email protected])([email protected]):
+  [email protected]([email protected]([email protected]))([email protected]):
     dependencies:
       react: 18.2.0
       react-dom: 18.2.0([email protected])
 
-  [email protected]([email protected])([email protected]):
+  [email protected]([email protected]([email protected]))([email protected]):
     dependencies:
       '@babel/runtime': 7.24.0
       memoize-one: 5.2.1
@@ -2708,13 +2708,13 @@ snapshots:
       fs-extra: 4.0.3
       jquery: 3.7.1
 
-  [email protected]([email protected])([email protected]):
+  [email protected]([email protected]([email protected]))([email protected]):
     dependencies:
       '@babel/runtime': 7.24.0
-      '@fluentui/react-component-event-listener': 0.63.1([email protected])([email protected])
-      '@fluentui/react-component-ref': 0.63.1([email protected])([email protected])
+      '@fluentui/react-component-event-listener': 0.63.1([email protected]([email protected]))([email protected])
+      '@fluentui/react-component-ref': 0.63.1([email protected]([email protected]))([email protected])
       '@popperjs/core': 2.11.8
-      '@semantic-ui-react/event-stack': 3.1.3([email protected])([email protected])
+      '@semantic-ui-react/event-stack': 3.1.3([email protected]([email protected]))([email protected])
       clsx: 1.2.1
       keyboard-key: 1.1.0
       lodash: 4.17.21
@@ -2723,7 +2723,7 @@ snapshots:
       react: 18.2.0
       react-dom: 18.2.0([email protected])
       react-is: 18.2.0
-      react-popper: 2.3.0(@popperjs/[email protected])([email protected])([email protected])
+      react-popper: 2.3.0(@popperjs/[email protected])([email protected]([email protected]))([email protected])
       shallowequal: 1.1.0
 
   [email protected]: {}

+ 46 - 87
web/src/components/LogsTable.js

@@ -1,11 +1,12 @@
 import React, { useEffect, useState } from 'react';
 import {
   API,
-  copy, getTodayStartTimestamp,
+  copy,
+  getTodayStartTimestamp,
   isAdmin,
   showError,
   showSuccess,
-  timestamp2string
+  timestamp2string,
 } from '../helpers';
 
 import {
@@ -29,7 +30,7 @@ import {
   stringToColor,
 } from '../helpers/render';
 import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
-import {getLogOther} from "../helpers/other.js";
+import { getLogOther } from '../helpers/other.js';
 
 const { Header } = Layout;
 
@@ -144,27 +145,27 @@ function renderUseTime(type) {
 
 function renderFirstUseTime(type) {
   let time = parseFloat(type) / 1000.0;
-  time = time.toFixed(1)
+  time = time.toFixed(1);
   if (time < 3) {
     return (
-        <Tag color='green' size='large'>
-          {' '}
-          {time} s{' '}
-        </Tag>
+      <Tag color='green' size='large'>
+        {' '}
+        {time} s{' '}
+      </Tag>
     );
   } else if (time < 10) {
     return (
-        <Tag color='orange' size='large'>
-          {' '}
-          {time} s{' '}
-        </Tag>
+      <Tag color='orange' size='large'>
+        {' '}
+        {time} s{' '}
+      </Tag>
     );
   } else {
     return (
-        <Tag color='red' size='large'>
-          {' '}
-          {time} s{' '}
-        </Tag>
+      <Tag color='red' size='large'>
+        {' '}
+        {time} s{' '}
+      </Tag>
     );
   }
 }
@@ -281,22 +282,22 @@ const LogsTable = () => {
         if (record.is_stream) {
           let other = getLogOther(record.other);
           return (
-              <div>
-                <Space>
-                  {renderUseTime(text)}
-                  {renderFirstUseTime(other.frt)}
-                  {renderIsStream(record.is_stream)}
-                </Space>
-              </div>
+            <div>
+              <Space>
+                {renderUseTime(text)}
+                {renderFirstUseTime(other.frt)}
+                {renderIsStream(record.is_stream)}
+              </Space>
+            </div>
           );
         } else {
           return (
-              <div>
-                <Space>
-                  {renderUseTime(text)}
-                  {renderIsStream(record.is_stream)}
-                </Space>
-              </div>
+            <div>
+              <Space>
+                {renderUseTime(text)}
+                {renderIsStream(record.is_stream)}
+              </Space>
+            </div>
           );
         }
       },
@@ -344,7 +345,7 @@ const LogsTable = () => {
         if (record.other !== '') {
           let other = JSON.parse(record.other);
           if (other === null) {
-            return <></>
+            return <></>;
           }
           if (other.admin_info !== undefined) {
             if (
@@ -414,8 +415,6 @@ const LogsTable = () => {
   const [activePage, setActivePage] = useState(1);
   const [logCount, setLogCount] = useState(ITEMS_PER_PAGE);
   const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
-  const [searchKeyword, setSearchKeyword] = useState('');
-  const [searching, setSearching] = useState(false);
   const [logType, setLogType] = useState(0);
   const isAdminUser = isAdmin();
   let now = new Date();
@@ -451,9 +450,7 @@ const LogsTable = () => {
     let localEndTimestamp = Date.parse(end_timestamp) / 1000;
     let url = `/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
     url = encodeURI(url);
-    let res = await API.get(
-      url,
-    );
+    let res = await API.get(url);
     const { success, message, data } = res.data;
     if (success) {
       setStat(data);
@@ -467,9 +464,7 @@ const LogsTable = () => {
     let localEndTimestamp = Date.parse(end_timestamp) / 1000;
     let url = `/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}`;
     url = encodeURI(url);
-    let res = await API.get(
-      url,
-    );
+    let res = await API.get(url);
     const { success, message, data } = res.data;
     if (success) {
       setStat(data);
@@ -521,10 +516,7 @@ const LogsTable = () => {
       logs[i].timestamp2string = timestamp2string(logs[i].created_at);
       logs[i].key = '' + logs[i].id;
     }
-    // data.key = '' + data.id
     setLogs(logs);
-    setLogCount(logs.length + ITEMS_PER_PAGE);
-    // console.log(logCount);
   };
 
   const loadLogs = async (startIdx, pageSize, logType = 0) => {
@@ -542,37 +534,28 @@ const LogsTable = () => {
     const res = await API.get(url);
     const { success, message, data } = res.data;
     if (success) {
-      if (startIdx === 0) {
-        setLogsFormat(data);
-      } else {
-        let newLogs = [...logs];
-        newLogs.splice(startIdx * pageSize, data.length, ...data);
-        setLogsFormat(newLogs);
-      }
+      const newPageData = data.items;
+      setActivePage(data.page);
+      setPageSize(data.page_size);
+      setLogCount(data.total);
+
+      setLogsFormat(newPageData);
     } else {
       showError(message);
     }
     setLoading(false);
   };
 
-  const pageData = logs.slice(
-    (activePage - 1) * pageSize,
-    activePage * pageSize,
-  );
-
   const handlePageChange = (page) => {
     setActivePage(page);
-    if (page === Math.ceil(logs.length / pageSize) + 1) {
-      // In this case we have to load more data and then append them.
-      loadLogs(page - 1, pageSize, logType).then((r) => {});
-    }
+    loadLogs(page, pageSize, logType).then((r) => {});
   };
 
   const handlePageSizeChange = async (size) => {
     localStorage.setItem('page-size', size + '');
     setPageSize(size);
     setActivePage(1);
-    loadLogs(0, size)
+    loadLogs(activePage, size)
       .then()
       .catch((reason) => {
         showError(reason);
@@ -580,27 +563,24 @@ const LogsTable = () => {
   };
 
   const refresh = async () => {
-    // setLoading(true);
     setActivePage(1);
     handleEyeClick();
-    await loadLogs(0, pageSize, logType);
+    await loadLogs(activePage, pageSize, logType);
   };
 
   const copyText = async (text) => {
     if (await copy(text)) {
       showSuccess('已复制:' + text);
     } else {
-      // setSearchKeyword(text);
       Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text });
     }
   };
 
   useEffect(() => {
-    // console.log('default effect')
     const localPageSize =
       parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
     setPageSize(localPageSize);
-    loadLogs(0, localPageSize)
+    loadLogs(activePage, localPageSize)
       .then()
       .catch((reason) => {
         showError(reason);
@@ -608,25 +588,6 @@ const LogsTable = () => {
     handleEyeClick();
   }, []);
 
-  const searchLogs = async () => {
-    if (searchKeyword === '') {
-      // if keyword is blank, load files instead.
-      await loadLogs(0, pageSize);
-      setActivePage(1);
-      return;
-    }
-    setSearching(true);
-    const res = await API.get(`/api/log/self/search?keyword=${searchKeyword}`);
-    const { success, message, data } = res.data;
-    if (success) {
-      setLogs(data);
-      setActivePage(1);
-    } else {
-      showError(message);
-    }
-    setSearching(false);
-  };
-
   return (
     <>
       <Layout>
@@ -719,15 +680,13 @@ const LogsTable = () => {
             >
               查询
             </Button>
-            <Form.Section>
-
-            </Form.Section>
+            <Form.Section></Form.Section>
           </>
         </Form>
         <Table
           style={{ marginTop: 5 }}
           columns={columns}
-          dataSource={pageData}
+          dataSource={logs}
           pagination={{
             currentPage: activePage,
             pageSize: pageSize,
@@ -735,7 +694,7 @@ const LogsTable = () => {
             pageSizeOpts: [10, 20, 50, 100],
             showSizeChanger: true,
             onPageSizeChange: (size) => {
-              handlePageSizeChange(size).then();
+              handlePageSizeChange(size);
             },
             onPageChange: handlePageChange,
           }}

+ 1 - 1
web/src/components/TokensTable.js

@@ -425,7 +425,7 @@ const TokensTable = () => {
         url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`;
         break;
       case 'lobe':
-        url = `https://chat-preview.lobehub.com/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${encodedServerAddress}"}}}`;
+        url = `https://chat-preview.lobehub.com/?settings={"keyVaults":{"openai":{"apiKey":"sk-${key}","baseURL":"${encodedServerAddress}/v1"}}}`;
         break;
       case 'next-mj':
         url =

+ 18 - 17
web/src/constants/channel.constants.js

@@ -5,21 +5,21 @@ export const CHANNEL_OPTIONS = [
     text: 'Midjourney Proxy',
     value: 2,
     color: 'light-blue',
-    label: 'Midjourney Proxy',
+    label: 'Midjourney Proxy'
   },
   {
     key: 5,
     text: 'Midjourney Proxy Plus',
     value: 5,
     color: 'blue',
-    label: 'Midjourney Proxy Plus',
+    label: 'Midjourney Proxy Plus'
   },
   {
     key: 36,
     text: 'Suno API',
     value: 36,
     color: 'purple',
-    label: 'Suno API',
+    label: 'Suno API'
   },
   { key: 4, text: 'Ollama', value: 4, color: 'grey', label: 'Ollama' },
   {
@@ -27,77 +27,77 @@ export const CHANNEL_OPTIONS = [
     text: 'Anthropic Claude',
     value: 14,
     color: 'indigo',
-    label: 'Anthropic Claude',
+    label: 'Anthropic Claude'
   },
   {
     key: 33,
     text: 'AWS Claude',
     value: 33,
     color: 'indigo',
-    label: 'AWS Claude',
+    label: 'AWS Claude'
   },
   {
     key: 3,
     text: 'Azure OpenAI',
     value: 3,
     color: 'teal',
-    label: 'Azure OpenAI',
+    label: 'Azure OpenAI'
   },
   {
     key: 24,
     text: 'Google Gemini',
     value: 24,
     color: 'orange',
-    label: 'Google Gemini',
+    label: 'Google Gemini'
   },
   {
     key: 34,
     text: 'Cohere',
     value: 34,
     color: 'purple',
-    label: 'Cohere',
+    label: 'Cohere'
   },
   {
     key: 15,
     text: '百度文心千帆',
     value: 15,
     color: 'blue',
-    label: '百度文心千帆',
+    label: '百度文心千帆'
   },
   {
     key: 17,
     text: '阿里通义千问',
     value: 17,
     color: 'orange',
-    label: '阿里通义千问',
+    label: '阿里通义千问'
   },
   {
     key: 18,
     text: '讯飞星火认知',
     value: 18,
     color: 'blue',
-    label: '讯飞星火认知',
+    label: '讯飞星火认知'
   },
   {
     key: 16,
     text: '智谱 ChatGLM',
     value: 16,
     color: 'violet',
-    label: '智谱 ChatGLM',
+    label: '智谱 ChatGLM'
   },
   {
     key: 26,
     text: '智谱 GLM-4V',
     value: 26,
     color: 'purple',
-    label: '智谱 GLM-4V',
+    label: '智谱 GLM-4V'
   },
   {
     key: 11,
     text: 'Google PaLM2',
     value: 11,
     color: 'orange',
-    label: 'Google PaLM2',
+    label: 'Google PaLM2'
   },
   { key: 39, text: 'Cloudflare', value: 39, color: 'grey', label: 'Cloudflare' },
   { key: 25, text: 'Moonshot', value: 25, color: 'green', label: 'Moonshot' },
@@ -107,19 +107,20 @@ export const CHANNEL_OPTIONS = [
   { key: 35, text: 'MiniMax', value: 35, color: 'green', label: 'MiniMax' },
   { key: 37, text: 'Dify', value: 37, color: 'teal', label: 'Dify' },
   { key: 38, text: 'Jina', value: 38, color: 'blue', label: 'Jina' },
+  { key: 40, text: 'SiliconCloud', value: 40, color: 'purple', label: 'SiliconCloud' },
   { key: 8, text: '自定义渠道', value: 8, color: 'pink', label: '自定义渠道' },
   {
     key: 22,
     text: '知识库:FastGPT',
     value: 22,
     color: 'blue',
-    label: '知识库:FastGPT',
+    label: '知识库:FastGPT'
   },
   {
     key: 21,
     text: '知识库:AI Proxy',
     value: 21,
     color: 'purple',
-    label: '知识库:AI Proxy',
-  },
+    label: '知识库:AI Proxy'
+  }
 ];