mirror of
https://github.com/songquanpeng/one-api.git
synced 2026-04-26 03:34:25 +08:00
Compare commits
10 Commits
3c5851b09a
...
3ace262384
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ace262384 | ||
|
|
ea0721d525 | ||
|
|
d0402f9086 | ||
|
|
1fead8e7f7 | ||
|
|
47918f3143 | ||
|
|
7d3e75a0b5 | ||
|
|
09911a301d | ||
|
|
f95e6b78b8 | ||
|
|
605bb06667 | ||
|
|
d88e07fd9a |
@@ -1,9 +1,8 @@
|
||||
package helper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/songquanpeng/one-api/common/random"
|
||||
"html/template"
|
||||
"log"
|
||||
"net"
|
||||
@@ -11,6 +10,10 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/songquanpeng/one-api/common/random"
|
||||
)
|
||||
|
||||
func OpenBrowser(url string) {
|
||||
@@ -106,6 +109,18 @@ func GenRequestID() string {
|
||||
return GetTimeString() + random.GetRandomNumberString(8)
|
||||
}
|
||||
|
||||
func SetRequestID(ctx context.Context, id string) context.Context {
|
||||
return context.WithValue(ctx, RequestIdKey, id)
|
||||
}
|
||||
|
||||
func GetRequestID(ctx context.Context) string {
|
||||
rawRequestId := ctx.Value(RequestIdKey)
|
||||
if rawRequestId == nil {
|
||||
return ""
|
||||
}
|
||||
return rawRequestId.(string)
|
||||
}
|
||||
|
||||
func GetResponseID(c *gin.Context) string {
|
||||
logID := c.GetString(RequestIdKey)
|
||||
return fmt.Sprintf("chatcmpl-%s", logID)
|
||||
|
||||
@@ -7,19 +7,25 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/songquanpeng/one-api/common/config"
|
||||
"github.com/songquanpeng/one-api/common/helper"
|
||||
)
|
||||
|
||||
type loggerLevel string
|
||||
|
||||
const (
|
||||
loggerDEBUG = "DEBUG"
|
||||
loggerINFO = "INFO"
|
||||
loggerWarn = "WARN"
|
||||
loggerError = "ERR"
|
||||
loggerDEBUG loggerLevel = "DEBUG"
|
||||
loggerINFO loggerLevel = "INFO"
|
||||
loggerWarn loggerLevel = "WARN"
|
||||
loggerError loggerLevel = "ERROR"
|
||||
loggerFatal loggerLevel = "FATAL"
|
||||
)
|
||||
|
||||
var setupLogOnce sync.Once
|
||||
@@ -44,27 +50,26 @@ func SetupLogger() {
|
||||
}
|
||||
|
||||
func SysLog(s string) {
|
||||
t := time.Now()
|
||||
_, _ = fmt.Fprintf(gin.DefaultWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s)
|
||||
logHelper(nil, loggerINFO, s)
|
||||
}
|
||||
|
||||
func SysLogf(format string, a ...any) {
|
||||
SysLog(fmt.Sprintf(format, a...))
|
||||
logHelper(nil, loggerINFO, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
func SysError(s string) {
|
||||
t := time.Now()
|
||||
_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s)
|
||||
logHelper(nil, loggerError, s)
|
||||
}
|
||||
|
||||
func SysErrorf(format string, a ...any) {
|
||||
SysError(fmt.Sprintf(format, a...))
|
||||
logHelper(nil, loggerError, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
func Debug(ctx context.Context, msg string) {
|
||||
if config.DebugEnabled {
|
||||
logHelper(ctx, loggerDEBUG, msg)
|
||||
if !config.DebugEnabled {
|
||||
return
|
||||
}
|
||||
logHelper(ctx, loggerDEBUG, msg)
|
||||
}
|
||||
|
||||
func Info(ctx context.Context, msg string) {
|
||||
@@ -80,37 +85,65 @@ func Error(ctx context.Context, msg string) {
|
||||
}
|
||||
|
||||
func Debugf(ctx context.Context, format string, a ...any) {
|
||||
Debug(ctx, fmt.Sprintf(format, a...))
|
||||
logHelper(ctx, loggerDEBUG, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
func Infof(ctx context.Context, format string, a ...any) {
|
||||
Info(ctx, fmt.Sprintf(format, a...))
|
||||
logHelper(ctx, loggerINFO, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
func Warnf(ctx context.Context, format string, a ...any) {
|
||||
Warn(ctx, fmt.Sprintf(format, a...))
|
||||
logHelper(ctx, loggerWarn, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
func Errorf(ctx context.Context, format string, a ...any) {
|
||||
Error(ctx, fmt.Sprintf(format, a...))
|
||||
logHelper(ctx, loggerError, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
func logHelper(ctx context.Context, level string, msg string) {
|
||||
func FatalLog(s string) {
|
||||
logHelper(nil, loggerFatal, s)
|
||||
}
|
||||
|
||||
func FatalLogf(format string, a ...any) {
|
||||
logHelper(nil, loggerFatal, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
func logHelper(ctx context.Context, level loggerLevel, msg string) {
|
||||
writer := gin.DefaultErrorWriter
|
||||
if level == loggerINFO {
|
||||
writer = gin.DefaultWriter
|
||||
}
|
||||
id := ctx.Value(helper.RequestIdKey)
|
||||
if id == nil {
|
||||
id = helper.GenRequestID()
|
||||
var requestId string
|
||||
if ctx != nil {
|
||||
rawRequestId := helper.GetRequestID(ctx)
|
||||
if rawRequestId != "" {
|
||||
requestId = fmt.Sprintf(" | %s", rawRequestId)
|
||||
}
|
||||
}
|
||||
lineInfo, funcName := getLineInfo()
|
||||
now := time.Now()
|
||||
_, _ = fmt.Fprintf(writer, "[%s] %v | %s | %s \n", level, now.Format("2006/01/02 - 15:04:05"), id, msg)
|
||||
_, _ = fmt.Fprintf(writer, "[%s] %v%s%s %s%s \n", level, now.Format("2006/01/02 - 15:04:05"), requestId, lineInfo, funcName, msg)
|
||||
SetupLogger()
|
||||
if level == loggerFatal {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func FatalLog(v ...any) {
|
||||
t := time.Now()
|
||||
_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[FATAL] %v | %v \n", t.Format("2006/01/02 - 15:04:05"), v)
|
||||
os.Exit(1)
|
||||
func getLineInfo() (string, string) {
|
||||
funcName := "[unknown] "
|
||||
pc, file, line, ok := runtime.Caller(3)
|
||||
if ok {
|
||||
if fn := runtime.FuncForPC(pc); fn != nil {
|
||||
parts := strings.Split(fn.Name(), ".")
|
||||
funcName = "[" + parts[len(parts)-1] + "] "
|
||||
}
|
||||
} else {
|
||||
file = "unknown"
|
||||
line = 0
|
||||
}
|
||||
parts := strings.Split(file, "one-api/")
|
||||
if len(parts) > 1 {
|
||||
file = parts[1]
|
||||
}
|
||||
return fmt.Sprintf(" | %s:%d", file, line), funcName
|
||||
}
|
||||
|
||||
@@ -5,16 +5,18 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/songquanpeng/one-api/common/config"
|
||||
"github.com/songquanpeng/one-api/common/logger"
|
||||
"github.com/songquanpeng/one-api/common/random"
|
||||
"github.com/songquanpeng/one-api/controller"
|
||||
"github.com/songquanpeng/one-api/model"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type GitHubOAuthResponse struct {
|
||||
@@ -81,6 +83,7 @@ func getGitHubUserInfoByCode(code string) (*GitHubUser, error) {
|
||||
}
|
||||
|
||||
func GitHubOAuth(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
session := sessions.Default(c)
|
||||
state := c.Query("state")
|
||||
if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) {
|
||||
@@ -136,7 +139,7 @@ func GitHubOAuth(c *gin.Context) {
|
||||
user.Role = model.RoleCommonUser
|
||||
user.Status = model.UserStatusEnabled
|
||||
|
||||
if err := user.Insert(0); err != nil {
|
||||
if err := user.Insert(ctx, 0); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
|
||||
@@ -5,15 +5,17 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/songquanpeng/one-api/common/config"
|
||||
"github.com/songquanpeng/one-api/common/logger"
|
||||
"github.com/songquanpeng/one-api/controller"
|
||||
"github.com/songquanpeng/one-api/model"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LarkOAuthResponse struct {
|
||||
@@ -79,6 +81,7 @@ func getLarkUserInfoByCode(code string) (*LarkUser, error) {
|
||||
}
|
||||
|
||||
func LarkOAuth(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
session := sessions.Default(c)
|
||||
state := c.Query("state")
|
||||
if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) {
|
||||
@@ -125,7 +128,7 @@ func LarkOAuth(c *gin.Context) {
|
||||
user.Role = model.RoleCommonUser
|
||||
user.Status = model.UserStatusEnabled
|
||||
|
||||
if err := user.Insert(0); err != nil {
|
||||
if err := user.Insert(ctx, 0); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
|
||||
@@ -5,15 +5,17 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/songquanpeng/one-api/common/config"
|
||||
"github.com/songquanpeng/one-api/common/logger"
|
||||
"github.com/songquanpeng/one-api/controller"
|
||||
"github.com/songquanpeng/one-api/model"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type OidcResponse struct {
|
||||
@@ -87,6 +89,7 @@ func getOidcUserInfoByCode(code string) (*OidcUser, error) {
|
||||
}
|
||||
|
||||
func OidcAuth(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
session := sessions.Default(c)
|
||||
state := c.Query("state")
|
||||
if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) {
|
||||
@@ -142,7 +145,7 @@ func OidcAuth(c *gin.Context) {
|
||||
} else {
|
||||
user.DisplayName = "OIDC User"
|
||||
}
|
||||
err := user.Insert(0)
|
||||
err := user.Insert(ctx, 0)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
|
||||
@@ -4,14 +4,16 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/songquanpeng/one-api/common/config"
|
||||
"github.com/songquanpeng/one-api/common/ctxkey"
|
||||
"github.com/songquanpeng/one-api/controller"
|
||||
"github.com/songquanpeng/one-api/model"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type wechatLoginResponse struct {
|
||||
@@ -52,6 +54,7 @@ func getWeChatIdByCode(code string) (string, error) {
|
||||
}
|
||||
|
||||
func WeChatAuth(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
if !config.WeChatAuthEnabled {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "管理员未开启通过微信登录以及注册",
|
||||
@@ -87,7 +90,7 @@ func WeChatAuth(c *gin.Context) {
|
||||
user.Role = model.RoleCommonUser
|
||||
user.Status = model.UserStatusEnabled
|
||||
|
||||
if err := user.Insert(0); err != nil {
|
||||
if err := user.Insert(ctx, 0); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
|
||||
@@ -109,6 +109,7 @@ func Logout(c *gin.Context) {
|
||||
}
|
||||
|
||||
func Register(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
if !config.RegisterEnabled {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "管理员关闭了新用户注册",
|
||||
@@ -166,7 +167,7 @@ func Register(c *gin.Context) {
|
||||
if config.EmailVerificationEnabled {
|
||||
cleanUser.Email = user.Email
|
||||
}
|
||||
if err := cleanUser.Insert(inviterId); err != nil {
|
||||
if err := cleanUser.Insert(ctx, inviterId); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
@@ -362,6 +363,7 @@ func GetSelf(c *gin.Context) {
|
||||
}
|
||||
|
||||
func UpdateUser(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
var updatedUser model.User
|
||||
err := json.NewDecoder(c.Request.Body).Decode(&updatedUser)
|
||||
if err != nil || updatedUser.Id == 0 {
|
||||
@@ -416,7 +418,7 @@ func UpdateUser(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
if originUser.Quota != updatedUser.Quota {
|
||||
model.RecordLog(originUser.Id, model.LogTypeManage, fmt.Sprintf("管理员将用户额度从 %s修改为 %s", common.LogQuota(originUser.Quota), common.LogQuota(updatedUser.Quota)))
|
||||
model.RecordLog(ctx, originUser.Id, model.LogTypeManage, fmt.Sprintf("管理员将用户额度从 %s修改为 %s", common.LogQuota(originUser.Quota), common.LogQuota(updatedUser.Quota)))
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
@@ -535,6 +537,7 @@ func DeleteSelf(c *gin.Context) {
|
||||
}
|
||||
|
||||
func CreateUser(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
var user model.User
|
||||
err := json.NewDecoder(c.Request.Body).Decode(&user)
|
||||
if err != nil || user.Username == "" || user.Password == "" {
|
||||
@@ -568,7 +571,7 @@ func CreateUser(c *gin.Context) {
|
||||
Password: user.Password,
|
||||
DisplayName: user.DisplayName,
|
||||
}
|
||||
if err := cleanUser.Insert(0); err != nil {
|
||||
if err := cleanUser.Insert(ctx, 0); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
@@ -747,6 +750,7 @@ type topUpRequest struct {
|
||||
}
|
||||
|
||||
func TopUp(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
req := topUpRequest{}
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
@@ -757,7 +761,7 @@ func TopUp(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
id := c.GetInt("id")
|
||||
quota, err := model.Redeem(req.Key, id)
|
||||
quota, err := model.Redeem(ctx, req.Key, id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
@@ -780,6 +784,7 @@ type adminTopUpRequest struct {
|
||||
}
|
||||
|
||||
func AdminTopUp(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
req := adminTopUpRequest{}
|
||||
err := c.ShouldBindJSON(&req)
|
||||
if err != nil {
|
||||
@@ -800,7 +805,7 @@ func AdminTopUp(c *gin.Context) {
|
||||
if req.Remark == "" {
|
||||
req.Remark = fmt.Sprintf("通过 API 充值 %s", common.LogQuota(int64(req.Quota)))
|
||||
}
|
||||
model.RecordTopupLog(req.UserId, req.Remark, req.Quota)
|
||||
model.RecordTopupLog(ctx, req.UserId, req.Remark, req.Quota)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
|
||||
@@ -2,13 +2,15 @@ package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/songquanpeng/one-api/common/ctxkey"
|
||||
"github.com/songquanpeng/one-api/common/logger"
|
||||
"github.com/songquanpeng/one-api/model"
|
||||
"github.com/songquanpeng/one-api/relay/channeltype"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type ModelRequest struct {
|
||||
@@ -17,6 +19,7 @@ type ModelRequest struct {
|
||||
|
||||
func Distribute() func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
userId := c.GetInt(ctxkey.Id)
|
||||
userGroup, _ := model.CacheGetUserGroup(userId)
|
||||
c.Set(ctxkey.Group, userGroup)
|
||||
@@ -52,6 +55,7 @@ func Distribute() func(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
logger.Debugf(ctx, "user id %d, user group: %s, request model: %s, using channel #%d", userId, userGroup, requestModel, channel.Id)
|
||||
SetupContextForSelectedChannel(c, channel, requestModel)
|
||||
c.Next()
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/songquanpeng/one-api/common/helper"
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ func RequestId() func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
id := helper.GenRequestID()
|
||||
c.Set(helper.RequestIdKey, id)
|
||||
ctx := context.WithValue(c.Request.Context(), helper.RequestIdKey, id)
|
||||
ctx := helper.SetRequestID(c.Request.Context(), id)
|
||||
c.Request = c.Request.WithContext(ctx)
|
||||
c.Header(helper.RequestIdKey, id)
|
||||
c.Next()
|
||||
|
||||
35
model/log.go
35
model/log.go
@@ -4,11 +4,12 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/songquanpeng/one-api/common"
|
||||
"github.com/songquanpeng/one-api/common/config"
|
||||
"github.com/songquanpeng/one-api/common/helper"
|
||||
"github.com/songquanpeng/one-api/common/logger"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Log struct {
|
||||
@@ -24,6 +25,7 @@ type Log struct {
|
||||
PromptTokens int `json:"prompt_tokens" gorm:"default:0"`
|
||||
CompletionTokens int `json:"completion_tokens" gorm:"default:0"`
|
||||
ChannelId int `json:"channel" gorm:"index"`
|
||||
RequestId string `json:"request_id"`
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -34,7 +36,18 @@ const (
|
||||
LogTypeSystem
|
||||
)
|
||||
|
||||
func RecordLog(userId int, logType int, content string) {
|
||||
func recordLogHelper(ctx context.Context, log *Log) {
|
||||
requestId := helper.GetRequestID(ctx)
|
||||
log.RequestId = requestId
|
||||
err := LOG_DB.Create(log).Error
|
||||
if err != nil {
|
||||
logger.Error(ctx, "failed to record log: "+err.Error())
|
||||
return
|
||||
}
|
||||
logger.Infof(ctx, "record log: %+v", log)
|
||||
}
|
||||
|
||||
func RecordLog(ctx context.Context, userId int, logType int, content string) {
|
||||
if logType == LogTypeConsume && !config.LogConsumeEnabled {
|
||||
return
|
||||
}
|
||||
@@ -45,13 +58,10 @@ func RecordLog(userId int, logType int, content string) {
|
||||
Type: logType,
|
||||
Content: content,
|
||||
}
|
||||
err := LOG_DB.Create(log).Error
|
||||
if err != nil {
|
||||
logger.SysError("failed to record log: " + err.Error())
|
||||
}
|
||||
recordLogHelper(ctx, log)
|
||||
}
|
||||
|
||||
func RecordTopupLog(userId int, content string, quota int) {
|
||||
func RecordTopupLog(ctx context.Context, userId int, content string, quota int) {
|
||||
log := &Log{
|
||||
UserId: userId,
|
||||
Username: GetUsernameById(userId),
|
||||
@@ -60,14 +70,10 @@ func RecordTopupLog(userId int, content string, quota int) {
|
||||
Content: content,
|
||||
Quota: quota,
|
||||
}
|
||||
err := LOG_DB.Create(log).Error
|
||||
if err != nil {
|
||||
logger.SysError("failed to record log: " + err.Error())
|
||||
}
|
||||
recordLogHelper(ctx, log)
|
||||
}
|
||||
|
||||
func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptTokens int, completionTokens int, modelName string, tokenName string, quota int64, content string) {
|
||||
logger.Info(ctx, fmt.Sprintf("record consume log: userId=%d, channelId=%d, promptTokens=%d, completionTokens=%d, modelName=%s, tokenName=%s, quota=%d, content=%s", userId, channelId, promptTokens, completionTokens, modelName, tokenName, quota, content))
|
||||
if !config.LogConsumeEnabled {
|
||||
return
|
||||
}
|
||||
@@ -84,10 +90,7 @@ func RecordConsumeLog(ctx context.Context, userId int, channelId int, promptToke
|
||||
Quota: int(quota),
|
||||
ChannelId: channelId,
|
||||
}
|
||||
err := LOG_DB.Create(log).Error
|
||||
if err != nil {
|
||||
logger.Error(ctx, "failed to record log: "+err.Error())
|
||||
}
|
||||
recordLogHelper(ctx, log)
|
||||
}
|
||||
|
||||
func GetAllLogs(logType int, startTimestamp int64, endTimestamp int64, modelName string, username string, tokenName string, startIdx int, num int, channel int) (logs []*Log, err error) {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/songquanpeng/one-api/common"
|
||||
"github.com/songquanpeng/one-api/common/helper"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -48,7 +51,7 @@ func GetRedemptionById(id int) (*Redemption, error) {
|
||||
return &redemption, err
|
||||
}
|
||||
|
||||
func Redeem(key string, userId int) (quota int64, err error) {
|
||||
func Redeem(ctx context.Context, key string, userId int) (quota int64, err error) {
|
||||
if key == "" {
|
||||
return 0, errors.New("未提供兑换码")
|
||||
}
|
||||
@@ -82,7 +85,7 @@ func Redeem(key string, userId int) (quota int64, err error) {
|
||||
if err != nil {
|
||||
return 0, errors.New("兑换失败," + err.Error())
|
||||
}
|
||||
RecordLog(userId, LogTypeTopup, fmt.Sprintf("通过兑换码充值 %s", common.LogQuota(redemption.Quota)))
|
||||
RecordLog(ctx, userId, LogTypeTopup, fmt.Sprintf("通过兑换码充值 %s", common.LogQuota(redemption.Quota)))
|
||||
return redemption.Quota, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/songquanpeng/one-api/common"
|
||||
"github.com/songquanpeng/one-api/common/blacklist"
|
||||
"github.com/songquanpeng/one-api/common/config"
|
||||
"github.com/songquanpeng/one-api/common/helper"
|
||||
"github.com/songquanpeng/one-api/common/logger"
|
||||
"github.com/songquanpeng/one-api/common/random"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -114,7 +117,7 @@ func DeleteUserById(id int) (err error) {
|
||||
return user.Delete()
|
||||
}
|
||||
|
||||
func (user *User) Insert(inviterId int) error {
|
||||
func (user *User) Insert(ctx context.Context, inviterId int) error {
|
||||
var err error
|
||||
if user.Password != "" {
|
||||
user.Password, err = common.Password2Hash(user.Password)
|
||||
@@ -130,16 +133,16 @@ func (user *User) Insert(inviterId int) error {
|
||||
return result.Error
|
||||
}
|
||||
if config.QuotaForNewUser > 0 {
|
||||
RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("新用户注册赠送 %s", common.LogQuota(config.QuotaForNewUser)))
|
||||
RecordLog(ctx, user.Id, LogTypeSystem, fmt.Sprintf("新用户注册赠送 %s", common.LogQuota(config.QuotaForNewUser)))
|
||||
}
|
||||
if inviterId != 0 {
|
||||
if config.QuotaForInvitee > 0 {
|
||||
_ = IncreaseUserQuota(user.Id, config.QuotaForInvitee)
|
||||
RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("使用邀请码赠送 %s", common.LogQuota(config.QuotaForInvitee)))
|
||||
RecordLog(ctx, user.Id, LogTypeSystem, fmt.Sprintf("使用邀请码赠送 %s", common.LogQuota(config.QuotaForInvitee)))
|
||||
}
|
||||
if config.QuotaForInviter > 0 {
|
||||
_ = IncreaseUserQuota(inviterId, config.QuotaForInviter)
|
||||
RecordLog(inviterId, LogTypeSystem, fmt.Sprintf("邀请用户赠送 %s", common.LogQuota(config.QuotaForInviter)))
|
||||
RecordLog(ctx, inviterId, LogTypeSystem, fmt.Sprintf("邀请用户赠送 %s", common.LogQuota(config.QuotaForInviter)))
|
||||
}
|
||||
}
|
||||
// create default token
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/songquanpeng/one-api/common/config"
|
||||
"github.com/songquanpeng/one-api/common/helper"
|
||||
channelhelper "github.com/songquanpeng/one-api/relay/adaptor"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/openai"
|
||||
@@ -24,8 +23,11 @@ func (a *Adaptor) Init(meta *meta.Meta) {
|
||||
}
|
||||
|
||||
func (a *Adaptor) GetRequestURL(meta *meta.Meta) (string, error) {
|
||||
defaultVersion := config.GeminiVersion
|
||||
if meta.ActualModelName == "gemini-2.0-flash-exp" {
|
||||
var defaultVersion string
|
||||
switch meta.ActualModelName {
|
||||
case "gemini-2.0-flash-exp",
|
||||
"gemini-2.0-flash-thinking-exp",
|
||||
"gemini-2.0-flash-thinking-exp-01-21":
|
||||
defaultVersion = "v1beta"
|
||||
}
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@ var ModelList = []string{
|
||||
"gemini-1.5-flash", "gemini-1.5-pro",
|
||||
"text-embedding-004", "aqa",
|
||||
"gemini-2.0-flash-exp",
|
||||
"gemini-2.0-flash-thinking-exp",
|
||||
"gemini-2.0-flash-thinking-exp", "gemini-2.0-flash-thinking-exp-01-21",
|
||||
}
|
||||
|
||||
@@ -8,12 +8,14 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/songquanpeng/one-api/common/config"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/doubao"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/minimax"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/novita"
|
||||
"github.com/songquanpeng/one-api/relay/channeltype"
|
||||
"github.com/songquanpeng/one-api/relay/constant/role"
|
||||
"github.com/songquanpeng/one-api/relay/meta"
|
||||
"github.com/songquanpeng/one-api/relay/model"
|
||||
"github.com/songquanpeng/one-api/relay/relaymode"
|
||||
@@ -85,11 +87,12 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.G
|
||||
}
|
||||
|
||||
// o1/o1-mini/o1-preview do not support system prompt and max_tokens
|
||||
// refer: https://platform.openai.com/docs/api-reference/chat/create#chat-create-messages
|
||||
if strings.HasPrefix(request.Model, "o1") {
|
||||
request.MaxTokens = 0
|
||||
request.Messages = func(raw []model.Message) (filtered []model.Message) {
|
||||
for i := range raw {
|
||||
if raw[i].Role != "system" {
|
||||
if raw[i].Role != role.System {
|
||||
filtered = append(filtered, raw[i])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,19 @@ package tencent
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/songquanpeng/one-api/common/helper"
|
||||
"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"
|
||||
"strconv"
|
||||
"strings"
|
||||
"github.com/songquanpeng/one-api/relay/relaymode"
|
||||
)
|
||||
|
||||
// https://cloud.tencent.com/document/api/1729/101837
|
||||
@@ -52,10 +55,18 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *model.G
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tencentRequest := ConvertRequest(*request)
|
||||
var convertedRequest any
|
||||
switch relayMode {
|
||||
case relaymode.Embeddings:
|
||||
a.Action = "GetEmbedding"
|
||||
convertedRequest = ConvertEmbeddingRequest(*request)
|
||||
default:
|
||||
a.Action = "ChatCompletions"
|
||||
convertedRequest = ConvertRequest(*request)
|
||||
}
|
||||
// we have to calculate the sign here
|
||||
a.Sign = GetSign(*tencentRequest, a, secretId, secretKey)
|
||||
return tencentRequest, nil
|
||||
a.Sign = GetSign(convertedRequest, a, secretId, secretKey)
|
||||
return convertedRequest, nil
|
||||
}
|
||||
|
||||
func (a *Adaptor) ConvertImageRequest(request *model.ImageRequest) (any, error) {
|
||||
@@ -75,7 +86,12 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, meta *meta.Met
|
||||
err, responseText = StreamHandler(c, resp)
|
||||
usage = openai.ResponseText2Usage(responseText, meta.ActualModelName, meta.PromptTokens)
|
||||
} else {
|
||||
err, usage = Handler(c, resp)
|
||||
switch meta.Mode {
|
||||
case relaymode.Embeddings:
|
||||
err, usage = EmbeddingHandler(c, resp)
|
||||
default:
|
||||
err, usage = Handler(c, resp)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6,4 +6,5 @@ var ModelList = []string{
|
||||
"hunyuan-standard-256K",
|
||||
"hunyuan-pro",
|
||||
"hunyuan-vision",
|
||||
"hunyuan-embedding",
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/songquanpeng/one-api/common/render"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -16,11 +15,14 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/songquanpeng/one-api/common"
|
||||
"github.com/songquanpeng/one-api/common/conv"
|
||||
"github.com/songquanpeng/one-api/common/ctxkey"
|
||||
"github.com/songquanpeng/one-api/common/helper"
|
||||
"github.com/songquanpeng/one-api/common/logger"
|
||||
"github.com/songquanpeng/one-api/common/random"
|
||||
"github.com/songquanpeng/one-api/common/render"
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/openai"
|
||||
"github.com/songquanpeng/one-api/relay/constant"
|
||||
"github.com/songquanpeng/one-api/relay/model"
|
||||
@@ -44,8 +46,68 @@ func ConvertRequest(request model.GeneralOpenAIRequest) *ChatRequest {
|
||||
}
|
||||
}
|
||||
|
||||
func ConvertEmbeddingRequest(request model.GeneralOpenAIRequest) *EmbeddingRequest {
|
||||
return &EmbeddingRequest{
|
||||
InputList: request.ParseInput(),
|
||||
}
|
||||
}
|
||||
|
||||
func EmbeddingHandler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *model.Usage) {
|
||||
var tencentResponseP EmbeddingResponseP
|
||||
err := json.NewDecoder(resp.Body).Decode(&tencentResponseP)
|
||||
if err != nil {
|
||||
return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
|
||||
}
|
||||
|
||||
err = resp.Body.Close()
|
||||
if err != nil {
|
||||
return openai.ErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil
|
||||
}
|
||||
|
||||
tencentResponse := tencentResponseP.Response
|
||||
if tencentResponse.Error.Code != "" {
|
||||
return &model.ErrorWithStatusCode{
|
||||
Error: model.Error{
|
||||
Message: tencentResponse.Error.Message,
|
||||
Code: tencentResponse.Error.Code,
|
||||
},
|
||||
StatusCode: resp.StatusCode,
|
||||
}, nil
|
||||
}
|
||||
requestModel := c.GetString(ctxkey.RequestModel)
|
||||
fullTextResponse := embeddingResponseTencent2OpenAI(&tencentResponse)
|
||||
fullTextResponse.Model = requestModel
|
||||
jsonResponse, err := json.Marshal(fullTextResponse)
|
||||
if err != nil {
|
||||
return openai.ErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil
|
||||
}
|
||||
c.Writer.Header().Set("Content-Type", "application/json")
|
||||
c.Writer.WriteHeader(resp.StatusCode)
|
||||
_, err = c.Writer.Write(jsonResponse)
|
||||
return nil, &fullTextResponse.Usage
|
||||
}
|
||||
|
||||
func embeddingResponseTencent2OpenAI(response *EmbeddingResponse) *openai.EmbeddingResponse {
|
||||
openAIEmbeddingResponse := openai.EmbeddingResponse{
|
||||
Object: "list",
|
||||
Data: make([]openai.EmbeddingResponseItem, 0, len(response.Data)),
|
||||
Model: "hunyuan-embedding",
|
||||
Usage: model.Usage{TotalTokens: response.EmbeddingUsage.TotalTokens},
|
||||
}
|
||||
|
||||
for _, item := range response.Data {
|
||||
openAIEmbeddingResponse.Data = append(openAIEmbeddingResponse.Data, openai.EmbeddingResponseItem{
|
||||
Object: item.Object,
|
||||
Index: item.Index,
|
||||
Embedding: item.Embedding,
|
||||
})
|
||||
}
|
||||
return &openAIEmbeddingResponse
|
||||
}
|
||||
|
||||
func responseTencent2OpenAI(response *ChatResponse) *openai.TextResponse {
|
||||
fullTextResponse := openai.TextResponse{
|
||||
Id: response.ReqID,
|
||||
Object: "chat.completion",
|
||||
Created: helper.GetTimestamp(),
|
||||
Usage: model.Usage{
|
||||
@@ -148,7 +210,7 @@ func Handler(c *gin.Context, resp *http.Response) (*model.ErrorWithStatusCode, *
|
||||
return openai.ErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil
|
||||
}
|
||||
TencentResponse = responseP.Response
|
||||
if TencentResponse.Error.Code != 0 {
|
||||
if TencentResponse.Error.Code != "" {
|
||||
return &model.ErrorWithStatusCode{
|
||||
Error: model.Error{
|
||||
Message: TencentResponse.Error.Message,
|
||||
@@ -195,7 +257,7 @@ func hmacSha256(s, key string) string {
|
||||
return string(hashed.Sum(nil))
|
||||
}
|
||||
|
||||
func GetSign(req ChatRequest, adaptor *Adaptor, secId, secKey string) string {
|
||||
func GetSign(req any, adaptor *Adaptor, secId, secKey string) string {
|
||||
// build canonical request string
|
||||
host := "hunyuan.tencentcloudapi.com"
|
||||
httpRequestMethod := "POST"
|
||||
|
||||
@@ -35,16 +35,16 @@ type ChatRequest struct {
|
||||
// 1. 影响输出文本的多样性,取值越大,生成文本的多样性越强。
|
||||
// 2. 取值区间为 [0.0, 1.0],未传值时使用各模型推荐值。
|
||||
// 3. 非必要不建议使用,不合理的取值会影响效果。
|
||||
TopP *float64 `json:"TopP"`
|
||||
TopP *float64 `json:"TopP,omitempty"`
|
||||
// 说明:
|
||||
// 1. 较高的数值会使输出更加随机,而较低的数值会使其更加集中和确定。
|
||||
// 2. 取值区间为 [0.0, 2.0],未传值时使用各模型推荐值。
|
||||
// 3. 非必要不建议使用,不合理的取值会影响效果。
|
||||
Temperature *float64 `json:"Temperature"`
|
||||
Temperature *float64 `json:"Temperature,omitempty"`
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
Code int `json:"Code"`
|
||||
Code string `json:"Code"`
|
||||
Message string `json:"Message"`
|
||||
}
|
||||
|
||||
@@ -61,15 +61,41 @@ type ResponseChoices struct {
|
||||
}
|
||||
|
||||
type ChatResponse struct {
|
||||
Choices []ResponseChoices `json:"Choices,omitempty"` // 结果
|
||||
Created int64 `json:"Created,omitempty"` // unix 时间戳的字符串
|
||||
Id string `json:"Id,omitempty"` // 会话 id
|
||||
Usage Usage `json:"Usage,omitempty"` // token 数量
|
||||
Error Error `json:"Error,omitempty"` // 错误信息 注意:此字段可能返回 null,表示取不到有效值
|
||||
Note string `json:"Note,omitempty"` // 注释
|
||||
ReqID string `json:"Req_id,omitempty"` // 唯一请求 Id,每次请求都会返回。用于反馈接口入参
|
||||
Choices []ResponseChoices `json:"Choices,omitempty"` // 结果
|
||||
Created int64 `json:"Created,omitempty"` // unix 时间戳的字符串
|
||||
Id string `json:"Id,omitempty"` // 会话 id
|
||||
Usage Usage `json:"Usage,omitempty"` // token 数量
|
||||
Error Error `json:"Error,omitempty"` // 错误信息 注意:此字段可能返回 null,表示取不到有效值
|
||||
Note string `json:"Note,omitempty"` // 注释
|
||||
ReqID string `json:"RequestId,omitempty"` // 唯一请求 Id,每次请求都会返回。用于反馈接口入参
|
||||
}
|
||||
|
||||
type ChatResponseP struct {
|
||||
Response ChatResponse `json:"Response,omitempty"`
|
||||
}
|
||||
|
||||
type EmbeddingRequest struct {
|
||||
InputList []string `json:"InputList"`
|
||||
}
|
||||
|
||||
type EmbeddingData struct {
|
||||
Embedding []float64 `json:"Embedding"`
|
||||
Index int `json:"Index"`
|
||||
Object string `json:"Object"`
|
||||
}
|
||||
|
||||
type EmbeddingUsage struct {
|
||||
PromptTokens int `json:"PromptTokens"`
|
||||
TotalTokens int `json:"TotalTokens"`
|
||||
}
|
||||
|
||||
type EmbeddingResponse struct {
|
||||
Data []EmbeddingData `json:"Data"`
|
||||
EmbeddingUsage EmbeddingUsage `json:"Usage,omitempty"`
|
||||
RequestId string `json:"RequestId,omitempty"`
|
||||
Error Error `json:"Error,omitempty"`
|
||||
}
|
||||
|
||||
type EmbeddingResponseP struct {
|
||||
Response EmbeddingResponse `json:"Response,omitempty"`
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ var ModelList = []string{
|
||||
"gemini-pro", "gemini-pro-vision",
|
||||
"gemini-1.5-pro-001", "gemini-1.5-flash-001",
|
||||
"gemini-1.5-pro-002", "gemini-1.5-flash-002",
|
||||
"gemini-2.0-flash-exp", "gemini-2.0-flash-thinking-exp",
|
||||
"gemini-2.0-flash-exp",
|
||||
"gemini-2.0-flash-thinking-exp", "gemini-2.0-flash-thinking-exp-01-21",
|
||||
}
|
||||
|
||||
type Adaptor struct {
|
||||
|
||||
@@ -3,6 +3,7 @@ package billing
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/songquanpeng/one-api/common/logger"
|
||||
"github.com/songquanpeng/one-api/model"
|
||||
)
|
||||
@@ -31,7 +32,7 @@ func PostConsumeQuota(ctx context.Context, tokenId int, quotaDelta int64, totalQ
|
||||
}
|
||||
// totalQuota is total quota consumed
|
||||
if totalQuota != 0 {
|
||||
logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio)
|
||||
logContent := fmt.Sprintf("%.2f × %.2f", modelRatio, groupRatio)
|
||||
model.RecordConsumeLog(ctx, userId, channelId, int(totalQuota), 0, modelName, tokenName, totalQuota, logContent)
|
||||
model.UpdateUserUsedQuotaAndRequestCount(userId, totalQuota)
|
||||
model.UpdateChannelUsedQuota(channelId, totalQuota)
|
||||
|
||||
@@ -9,9 +9,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
USD2RMB = 7
|
||||
USD = 500 // $0.002 = 1 -> $1 = 500
|
||||
RMB = USD / USD2RMB
|
||||
USD2RMB = 7
|
||||
USD = 500 // $0.002 = 1 -> $1 = 500
|
||||
MILLI_USD = 1.0 / 1000 * USD
|
||||
RMB = USD / USD2RMB
|
||||
)
|
||||
|
||||
// ModelRatio
|
||||
@@ -115,15 +116,16 @@ var ModelRatio = map[string]float64{
|
||||
"bge-large-en": 0.002 * RMB,
|
||||
"tao-8k": 0.002 * RMB,
|
||||
// https://ai.google.dev/pricing
|
||||
"gemini-pro": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens
|
||||
"gemini-1.0-pro": 1,
|
||||
"gemini-1.5-pro": 1,
|
||||
"gemini-1.5-pro-001": 1,
|
||||
"gemini-1.5-flash": 1,
|
||||
"gemini-1.5-flash-001": 1,
|
||||
"gemini-2.0-flash-exp": 1,
|
||||
"gemini-2.0-flash-thinking-exp": 1,
|
||||
"aqa": 1,
|
||||
"gemini-pro": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens
|
||||
"gemini-1.0-pro": 1,
|
||||
"gemini-1.5-pro": 1,
|
||||
"gemini-1.5-pro-001": 1,
|
||||
"gemini-1.5-flash": 1,
|
||||
"gemini-1.5-flash-001": 1,
|
||||
"gemini-2.0-flash-exp": 1,
|
||||
"gemini-2.0-flash-thinking-exp": 1,
|
||||
"gemini-2.0-flash-thinking-exp-01-21": 1,
|
||||
"aqa": 1,
|
||||
// https://open.bigmodel.cn/pricing
|
||||
"glm-4": 0.1 * RMB,
|
||||
"glm-4v": 0.1 * RMB,
|
||||
@@ -284,8 +286,8 @@ var ModelRatio = map[string]float64{
|
||||
"command-r": 0.5 / 1000 * USD,
|
||||
"command-r-plus": 3.0 / 1000 * USD,
|
||||
// https://platform.deepseek.com/api-docs/pricing/
|
||||
"deepseek-chat": 1.0 / 1000 * RMB,
|
||||
"deepseek-coder": 1.0 / 1000 * RMB,
|
||||
"deepseek-chat": 0.14 * MILLI_USD,
|
||||
"deepseek-reasoner": 0.55 * MILLI_USD,
|
||||
// https://www.deepl.com/pro?cta=header-prices
|
||||
"deepl-zh": 25.0 / 1000 * USD,
|
||||
"deepl-en": 25.0 / 1000 * USD,
|
||||
@@ -407,6 +409,9 @@ var CompletionRatio = map[string]float64{
|
||||
"llama3-70b-8192(33)": 0.0035 / 0.00265,
|
||||
// whisper
|
||||
"whisper-1": 0, // only count input tokens
|
||||
// deepseek
|
||||
"deepseek-chat": 0.28 / 0.14,
|
||||
"deepseek-reasoner": 2.19 / 0.55,
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -3,4 +3,5 @@ package role
|
||||
const (
|
||||
System = "system"
|
||||
Assistant = "assistant"
|
||||
Developer = "developer"
|
||||
)
|
||||
|
||||
@@ -8,7 +8,10 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/songquanpeng/one-api/relay/constant/role"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/songquanpeng/one-api/common"
|
||||
"github.com/songquanpeng/one-api/common/config"
|
||||
"github.com/songquanpeng/one-api/common/logger"
|
||||
@@ -123,7 +126,7 @@ func postConsumeQuota(ctx context.Context, usage *relaymodel.Usage, meta *meta.M
|
||||
if systemPromptReset {
|
||||
extraLog = " (注意系统提示词已被重置)"
|
||||
}
|
||||
logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f,补全倍率 %.2f%s", modelRatio, groupRatio, completionRatio, extraLog)
|
||||
logContent := fmt.Sprintf("%.2f × %.2f × %.2f%s", modelRatio, groupRatio, completionRatio, extraLog)
|
||||
model.RecordConsumeLog(ctx, meta.UserId, meta.ChannelId, promptTokens, completionTokens, textRequest.Model, meta.TokenName, quota, logContent)
|
||||
model.UpdateUserUsedQuotaAndRequestCount(meta.UserId, quota)
|
||||
model.UpdateChannelUsedQuota(meta.ChannelId, quota)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/songquanpeng/one-api/common"
|
||||
"github.com/songquanpeng/one-api/common/ctxkey"
|
||||
"github.com/songquanpeng/one-api/common/logger"
|
||||
@@ -18,7 +19,7 @@ import (
|
||||
"github.com/songquanpeng/one-api/relay/adaptor/openai"
|
||||
billingratio "github.com/songquanpeng/one-api/relay/billing/ratio"
|
||||
"github.com/songquanpeng/one-api/relay/channeltype"
|
||||
metalib "github.com/songquanpeng/one-api/relay/meta"
|
||||
relaymeta "github.com/songquanpeng/one-api/relay/meta"
|
||||
relaymodel "github.com/songquanpeng/one-api/relay/model"
|
||||
)
|
||||
|
||||
@@ -65,7 +66,7 @@ func getImageSizeRatio(model string, size string) float64 {
|
||||
return 1
|
||||
}
|
||||
|
||||
func validateImageRequest(imageRequest *relaymodel.ImageRequest, _ *metalib.Meta) *relaymodel.ErrorWithStatusCode {
|
||||
func validateImageRequest(imageRequest *relaymodel.ImageRequest, _ *relaymeta.Meta) *relaymodel.ErrorWithStatusCode {
|
||||
// check prompt length
|
||||
if imageRequest.Prompt == "" {
|
||||
return openai.ErrorWrapper(errors.New("prompt is required"), "prompt_missing", http.StatusBadRequest)
|
||||
@@ -104,7 +105,7 @@ func getImageCostRatio(imageRequest *relaymodel.ImageRequest) (float64, error) {
|
||||
|
||||
func RelayImageHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatusCode {
|
||||
ctx := c.Request.Context()
|
||||
meta := metalib.GetByContext(c)
|
||||
meta := relaymeta.GetByContext(c)
|
||||
imageRequest, err := getImageRequest(c, meta.Mode)
|
||||
if err != nil {
|
||||
logger.Errorf(ctx, "getImageRequest failed: %s", err.Error())
|
||||
@@ -131,7 +132,7 @@ func RelayImageHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus
|
||||
|
||||
imageModel := imageRequest.Model
|
||||
// Convert the original image model
|
||||
imageRequest.Model = metalib.GetMappedModelName(imageRequest.Model, billingratio.ImageOriginModelName)
|
||||
imageRequest.Model = relaymeta.GetMappedModelName(imageRequest.Model, billingratio.ImageOriginModelName)
|
||||
c.Set("response_format", imageRequest.ResponseFormat)
|
||||
|
||||
var requestBody io.Reader
|
||||
@@ -210,7 +211,7 @@ func RelayImageHelper(c *gin.Context, relayMode int) *relaymodel.ErrorWithStatus
|
||||
}
|
||||
if quota != 0 {
|
||||
tokenName := c.GetString(ctxkey.TokenName)
|
||||
logContent := fmt.Sprintf("模型倍率 %.2f,分组倍率 %.2f", modelRatio, groupRatio)
|
||||
logContent := fmt.Sprintf("%.2f × %.2f", modelRatio, groupRatio)
|
||||
model.RecordConsumeLog(ctx, meta.UserId, meta.ChannelId, 0, 0, imageRequest.Model, tokenName, quota, logContent)
|
||||
model.UpdateUserUsedQuotaAndRequestCount(meta.UserId, quota)
|
||||
channelId := c.GetInt(ctxkey.ChannelId)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/songquanpeng/one-api/common/config"
|
||||
"github.com/songquanpeng/one-api/common/logger"
|
||||
"github.com/songquanpeng/one-api/relay"
|
||||
@@ -17,13 +18,13 @@ import (
|
||||
"github.com/songquanpeng/one-api/relay/billing"
|
||||
billingratio "github.com/songquanpeng/one-api/relay/billing/ratio"
|
||||
"github.com/songquanpeng/one-api/relay/channeltype"
|
||||
metalib "github.com/songquanpeng/one-api/relay/meta"
|
||||
relaymeta "github.com/songquanpeng/one-api/relay/meta"
|
||||
relaymodel "github.com/songquanpeng/one-api/relay/model"
|
||||
)
|
||||
|
||||
func RelayTextHelper(c *gin.Context) *relaymodel.ErrorWithStatusCode {
|
||||
ctx := c.Request.Context()
|
||||
meta := metalib.GetByContext(c)
|
||||
meta := relaymeta.GetByContext(c)
|
||||
// get & validate textRequest
|
||||
textRequest, err := getAndValidateTextRequest(c, meta.Mode)
|
||||
if err != nil {
|
||||
@@ -86,7 +87,7 @@ func RelayTextHelper(c *gin.Context) *relaymodel.ErrorWithStatusCode {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getRequestBody(c *gin.Context, meta *metalib.Meta, textRequest *relaymodel.GeneralOpenAIRequest, adaptor adaptor.Adaptor) (io.Reader, error) {
|
||||
func getRequestBody(c *gin.Context, meta *relaymeta.Meta, textRequest *relaymodel.GeneralOpenAIRequest, adaptor adaptor.Adaptor) (io.Reader, error) {
|
||||
if !config.EnforceIncludeUsage &&
|
||||
meta.APIType == apitype.OpenAI &&
|
||||
meta.OriginModelName == meta.ActualModelName &&
|
||||
|
||||
@@ -1,247 +1,260 @@
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { snackbarConstants } from 'constants/SnackbarConstants';
|
||||
import { API } from './api';
|
||||
import {enqueueSnackbar} from 'notistack';
|
||||
import {snackbarConstants} from 'constants/SnackbarConstants';
|
||||
import {API} from './api';
|
||||
|
||||
export function getSystemName() {
|
||||
let system_name = localStorage.getItem('system_name');
|
||||
if (!system_name) return 'One API';
|
||||
return system_name;
|
||||
let system_name = localStorage.getItem('system_name');
|
||||
if (!system_name) return 'One API';
|
||||
return system_name;
|
||||
}
|
||||
|
||||
export function isMobile() {
|
||||
return window.innerWidth <= 600;
|
||||
return window.innerWidth <= 600;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
export function SnackbarHTMLContent({ htmlContent }) {
|
||||
return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
|
||||
export function SnackbarHTMLContent({htmlContent}) {
|
||||
return <div dangerouslySetInnerHTML={{__html: htmlContent}}/>;
|
||||
}
|
||||
|
||||
export function getSnackbarOptions(variant) {
|
||||
let options = snackbarConstants.Common[variant];
|
||||
if (isMobile()) {
|
||||
// 合并 options 和 snackbarConstants.Mobile
|
||||
options = { ...options, ...snackbarConstants.Mobile };
|
||||
}
|
||||
return options;
|
||||
let options = snackbarConstants.Common[variant];
|
||||
if (isMobile()) {
|
||||
// 合并 options 和 snackbarConstants.Mobile
|
||||
options = {...options, ...snackbarConstants.Mobile};
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
export function showError(error) {
|
||||
if (error.message) {
|
||||
if (error.name === 'AxiosError') {
|
||||
switch (error.response.status) {
|
||||
case 429:
|
||||
enqueueSnackbar('错误:请求次数过多,请稍后再试!', getSnackbarOptions('ERROR'));
|
||||
break;
|
||||
case 500:
|
||||
enqueueSnackbar('错误:服务器内部错误,请联系管理员!', getSnackbarOptions('ERROR'));
|
||||
break;
|
||||
case 405:
|
||||
enqueueSnackbar('本站仅作演示之用,无服务端!', getSnackbarOptions('INFO'));
|
||||
break;
|
||||
default:
|
||||
enqueueSnackbar('错误:' + error.message, getSnackbarOptions('ERROR'));
|
||||
}
|
||||
return;
|
||||
if (error.message) {
|
||||
if (error.name === 'AxiosError') {
|
||||
switch (error.response.status) {
|
||||
case 429:
|
||||
enqueueSnackbar('错误:请求次数过多,请稍后再试!', getSnackbarOptions('ERROR'));
|
||||
break;
|
||||
case 500:
|
||||
enqueueSnackbar('错误:服务器内部错误,请联系管理员!', getSnackbarOptions('ERROR'));
|
||||
break;
|
||||
case 405:
|
||||
enqueueSnackbar('本站仅作演示之用,无服务端!', getSnackbarOptions('INFO'));
|
||||
break;
|
||||
default:
|
||||
enqueueSnackbar('错误:' + error.message, getSnackbarOptions('ERROR'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
enqueueSnackbar('错误:' + error, getSnackbarOptions('ERROR'));
|
||||
}
|
||||
} else {
|
||||
enqueueSnackbar('错误:' + error, getSnackbarOptions('ERROR'));
|
||||
}
|
||||
}
|
||||
|
||||
export function showNotice(message, isHTML = false) {
|
||||
if (isHTML) {
|
||||
enqueueSnackbar(<SnackbarHTMLContent htmlContent={message} />, getSnackbarOptions('NOTICE'));
|
||||
} else {
|
||||
enqueueSnackbar(message, getSnackbarOptions('NOTICE'));
|
||||
}
|
||||
if (isHTML) {
|
||||
enqueueSnackbar(<SnackbarHTMLContent htmlContent={message}/>, getSnackbarOptions('NOTICE'));
|
||||
} else {
|
||||
enqueueSnackbar(message, getSnackbarOptions('NOTICE'));
|
||||
}
|
||||
}
|
||||
|
||||
export function showWarning(message) {
|
||||
enqueueSnackbar(message, getSnackbarOptions('WARNING'));
|
||||
enqueueSnackbar(message, getSnackbarOptions('WARNING'));
|
||||
}
|
||||
|
||||
export function showSuccess(message) {
|
||||
enqueueSnackbar(message, getSnackbarOptions('SUCCESS'));
|
||||
enqueueSnackbar(message, getSnackbarOptions('SUCCESS'));
|
||||
}
|
||||
|
||||
export function showInfo(message) {
|
||||
enqueueSnackbar(message, getSnackbarOptions('INFO'));
|
||||
enqueueSnackbar(message, getSnackbarOptions('INFO'));
|
||||
}
|
||||
|
||||
export async function getOAuthState() {
|
||||
const res = await API.get('/api/oauth/state');
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
return data;
|
||||
} else {
|
||||
showError(message);
|
||||
return '';
|
||||
}
|
||||
const res = await API.get('/api/oauth/state');
|
||||
const {success, message, data} = res.data;
|
||||
if (success) {
|
||||
return data;
|
||||
} else {
|
||||
showError(message);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export async function onGitHubOAuthClicked(github_client_id, openInNewTab = false) {
|
||||
const state = await getOAuthState();
|
||||
if (!state) return;
|
||||
let url = `https://github.com/login/oauth/authorize?client_id=${github_client_id}&state=${state}&scope=user:email`;
|
||||
if (openInNewTab) {
|
||||
window.open(url);
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
const state = await getOAuthState();
|
||||
if (!state) return;
|
||||
let url = `https://github.com/login/oauth/authorize?client_id=${github_client_id}&state=${state}&scope=user:email`;
|
||||
if (openInNewTab) {
|
||||
window.open(url);
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
|
||||
export async function onLarkOAuthClicked(lark_client_id) {
|
||||
const state = await getOAuthState();
|
||||
if (!state) return;
|
||||
let redirect_uri = `${window.location.origin}/oauth/lark`;
|
||||
window.open(`https://accounts.feishu.cn/open-apis/authen/v1/authorize?redirect_uri=${redirect_uri}&client_id=${lark_client_id}&state=${state}`);
|
||||
const state = await getOAuthState();
|
||||
if (!state) return;
|
||||
let redirect_uri = `${window.location.origin}/oauth/lark`;
|
||||
window.open(`https://accounts.feishu.cn/open-apis/authen/v1/authorize?redirect_uri=${redirect_uri}&client_id=${lark_client_id}&state=${state}`);
|
||||
}
|
||||
|
||||
export async function onOidcClicked(auth_url, client_id, openInNewTab = false) {
|
||||
const state = await getOAuthState();
|
||||
if (!state) return;
|
||||
const redirect_uri = `${window.location.origin}/oauth/oidc`;
|
||||
const response_type = "code";
|
||||
const scope = "openid profile email";
|
||||
const url = `${auth_url}?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&state=${state}`;
|
||||
if (openInNewTab) {
|
||||
window.open(url);
|
||||
} else
|
||||
{
|
||||
window.location.href = url;
|
||||
}
|
||||
const state = await getOAuthState();
|
||||
if (!state) return;
|
||||
const redirect_uri = `${window.location.origin}/oauth/oidc`;
|
||||
const response_type = "code";
|
||||
const scope = "openid profile email";
|
||||
const url = `${auth_url}?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&state=${state}`;
|
||||
if (openInNewTab) {
|
||||
window.open(url);
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
|
||||
export function isAdmin() {
|
||||
let user = localStorage.getItem('user');
|
||||
if (!user) return false;
|
||||
user = JSON.parse(user);
|
||||
return user.role >= 10;
|
||||
let user = localStorage.getItem('user');
|
||||
if (!user) return false;
|
||||
user = JSON.parse(user);
|
||||
return user.role >= 10;
|
||||
}
|
||||
|
||||
export function timestamp2string(timestamp) {
|
||||
let date = new Date(timestamp * 1000);
|
||||
let year = date.getFullYear().toString();
|
||||
let month = (date.getMonth() + 1).toString();
|
||||
let day = date.getDate().toString();
|
||||
let hour = date.getHours().toString();
|
||||
let minute = date.getMinutes().toString();
|
||||
let second = date.getSeconds().toString();
|
||||
if (month.length === 1) {
|
||||
month = '0' + month;
|
||||
}
|
||||
if (day.length === 1) {
|
||||
day = '0' + day;
|
||||
}
|
||||
if (hour.length === 1) {
|
||||
hour = '0' + hour;
|
||||
}
|
||||
if (minute.length === 1) {
|
||||
minute = '0' + minute;
|
||||
}
|
||||
if (second.length === 1) {
|
||||
second = '0' + second;
|
||||
}
|
||||
return year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second;
|
||||
let date = new Date(timestamp * 1000);
|
||||
let year = date.getFullYear().toString();
|
||||
let month = (date.getMonth() + 1).toString();
|
||||
let day = date.getDate().toString();
|
||||
let hour = date.getHours().toString();
|
||||
let minute = date.getMinutes().toString();
|
||||
let second = date.getSeconds().toString();
|
||||
if (month.length === 1) {
|
||||
month = '0' + month;
|
||||
}
|
||||
if (day.length === 1) {
|
||||
day = '0' + day;
|
||||
}
|
||||
if (hour.length === 1) {
|
||||
hour = '0' + hour;
|
||||
}
|
||||
if (minute.length === 1) {
|
||||
minute = '0' + minute;
|
||||
}
|
||||
if (second.length === 1) {
|
||||
second = '0' + second;
|
||||
}
|
||||
return year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second;
|
||||
}
|
||||
|
||||
export function calculateQuota(quota, digits = 2) {
|
||||
let quotaPerUnit = localStorage.getItem('quota_per_unit');
|
||||
quotaPerUnit = parseFloat(quotaPerUnit);
|
||||
let quotaPerUnit = localStorage.getItem('quota_per_unit');
|
||||
quotaPerUnit = parseFloat(quotaPerUnit);
|
||||
|
||||
return (quota / quotaPerUnit).toFixed(digits);
|
||||
return (quota / quotaPerUnit).toFixed(digits);
|
||||
}
|
||||
|
||||
export function renderQuota(quota, digits = 2) {
|
||||
let displayInCurrency = localStorage.getItem('display_in_currency');
|
||||
displayInCurrency = displayInCurrency === 'true';
|
||||
if (displayInCurrency) {
|
||||
return '$' + calculateQuota(quota, digits);
|
||||
}
|
||||
return renderNumber(quota);
|
||||
let displayInCurrency = localStorage.getItem('display_in_currency');
|
||||
displayInCurrency = displayInCurrency === 'true';
|
||||
if (displayInCurrency) {
|
||||
return '$' + calculateQuota(quota, digits);
|
||||
}
|
||||
return renderNumber(quota);
|
||||
}
|
||||
|
||||
export const verifyJSON = (str) => {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
try {
|
||||
JSON.parse(str);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export function renderNumber(num) {
|
||||
if (num >= 1000000000) {
|
||||
return (num / 1000000000).toFixed(1) + 'B';
|
||||
} else if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1) + 'M';
|
||||
} else if (num >= 10000) {
|
||||
return (num / 1000).toFixed(1) + 'k';
|
||||
} else {
|
||||
return num;
|
||||
}
|
||||
if (num >= 1000000000) {
|
||||
return (num / 1000000000).toFixed(1) + 'B';
|
||||
} else if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1) + 'M';
|
||||
} else if (num >= 10000) {
|
||||
return (num / 1000).toFixed(1) + 'k';
|
||||
} else {
|
||||
return num;
|
||||
}
|
||||
}
|
||||
|
||||
export function renderQuotaWithPrompt(quota, digits) {
|
||||
let displayInCurrency = localStorage.getItem('display_in_currency');
|
||||
displayInCurrency = displayInCurrency === 'true';
|
||||
if (displayInCurrency) {
|
||||
return `(等价金额:${renderQuota(quota, digits)})`;
|
||||
}
|
||||
return '';
|
||||
let displayInCurrency = localStorage.getItem('display_in_currency');
|
||||
displayInCurrency = displayInCurrency === 'true';
|
||||
if (displayInCurrency) {
|
||||
return `(等价金额:${renderQuota(quota, digits)})`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export function downloadTextAsFile(text, filename) {
|
||||
let blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
|
||||
let url = URL.createObjectURL(blob);
|
||||
let a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
let blob = new Blob([text], {type: 'text/plain;charset=utf-8'});
|
||||
let url = URL.createObjectURL(blob);
|
||||
let a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
}
|
||||
|
||||
export function removeTrailingSlash(url) {
|
||||
if (url.endsWith('/')) {
|
||||
return url.slice(0, -1);
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
if (url.endsWith('/')) {
|
||||
return url.slice(0, -1);
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
let channelModels = undefined;
|
||||
|
||||
export async function loadChannelModels() {
|
||||
const res = await API.get('/api/models');
|
||||
const { success, data } = res.data;
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
channelModels = data;
|
||||
localStorage.setItem('channel_models', JSON.stringify(data));
|
||||
const res = await API.get('/api/models');
|
||||
const {success, data} = res.data;
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
channelModels = data;
|
||||
localStorage.setItem('channel_models', JSON.stringify(data));
|
||||
}
|
||||
|
||||
export function getChannelModels(type) {
|
||||
if (channelModels !== undefined && type in channelModels) {
|
||||
return channelModels[type];
|
||||
}
|
||||
let models = localStorage.getItem('channel_models');
|
||||
if (!models) {
|
||||
if (channelModels !== undefined && type in channelModels) {
|
||||
return channelModels[type];
|
||||
}
|
||||
let models = localStorage.getItem('channel_models');
|
||||
if (!models) {
|
||||
return [];
|
||||
}
|
||||
channelModels = JSON.parse(models);
|
||||
if (type in channelModels) {
|
||||
return channelModels[type];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
channelModels = JSON.parse(models);
|
||||
if (type in channelModels) {
|
||||
return channelModels[type];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function copy(text, name = '') {
|
||||
try {
|
||||
navigator.clipboard.writeText(text);
|
||||
} catch (error) {
|
||||
text = `复制${name}失败,请手动复制:<br /><br />${text}`;
|
||||
enqueueSnackbar(<SnackbarHTMLContent htmlContent={text} />, getSnackbarOptions('COPY'));
|
||||
return;
|
||||
}
|
||||
showSuccess(`复制${name}成功!`);
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
showNotice(`复制${name}成功!`, true);
|
||||
}, () => {
|
||||
text = `复制${name}失败,请手动复制:<br /><br />${text}`;
|
||||
enqueueSnackbar(<SnackbarHTMLContent htmlContent={text}/>, getSnackbarOptions('COPY'));
|
||||
});
|
||||
} else {
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = text;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
showNotice(`复制${name}成功!`, true);
|
||||
} catch (err) {
|
||||
text = `复制${name}失败,请手动复制:<br /><br />${text}`;
|
||||
enqueueSnackbar(<SnackbarHTMLContent htmlContent={text}/>, getSnackbarOptions('COPY'));
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,7 +328,7 @@ const LogsTable = () => {
|
||||
}}
|
||||
width={isAdminUser ? 4 : 6}
|
||||
>
|
||||
详情
|
||||
详情(模型倍率 × 分组倍率 × 补全倍率)
|
||||
</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
@@ -360,7 +360,10 @@ const LogsTable = () => {
|
||||
<Table.Cell>{log.prompt_tokens ? log.prompt_tokens : ''}</Table.Cell>
|
||||
<Table.Cell>{log.completion_tokens ? log.completion_tokens : ''}</Table.Cell>
|
||||
<Table.Cell>{log.quota ? renderQuota(log.quota, 6) : ''}</Table.Cell>
|
||||
<Table.Cell>{log.content}</Table.Cell>
|
||||
<Table.Cell>{log.content}{<>
|
||||
<br/>
|
||||
<code>{log.request_id}</code>
|
||||
</>}</Table.Cell>
|
||||
</Table.Row>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user