mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-07 23:17:27 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b1354ed51 | ||
|
|
132c71390c | ||
|
|
bb3deb7b93 | ||
|
|
f92d96e298 | ||
|
|
c86762b656 | ||
|
|
3409d7a6b6 | ||
|
|
bfba4866a5 | ||
|
|
4fc1fe318e | ||
|
|
b3576f24ef | ||
|
|
ed4d26fc9e | ||
|
|
ba56e2e8ca | ||
|
|
7c20e6d047 | ||
|
|
72d6898eb5 | ||
|
|
f2c9388139 | ||
|
|
aaf5cecefd | ||
|
|
fe2165ace6 | ||
|
|
3003d12a20 | ||
|
|
a8a2195ab1 | ||
|
|
d40e6ec25d | ||
|
|
8129aa76f9 | ||
|
|
fb8595da18 |
@@ -356,7 +356,7 @@ func GetCompletionRatio(name string) float64 {
|
||||
}
|
||||
return 2
|
||||
}
|
||||
if strings.HasPrefix(name, "o1-") {
|
||||
if strings.HasPrefix(name, "o1") {
|
||||
return 4
|
||||
}
|
||||
if name == "chatgpt-4o-latest" {
|
||||
|
||||
@@ -35,9 +35,7 @@ func StrToMap(str string) map[string]interface{} {
|
||||
m := make(map[string]interface{})
|
||||
err := json.Unmarshal([]byte(str), &m)
|
||||
if err != nil {
|
||||
return map[string]interface{}{
|
||||
"result": str,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package constant
|
||||
|
||||
var (
|
||||
FinishReasonStop = "stop"
|
||||
FinishReasonToolCalls = "tool_calls"
|
||||
FinishReasonStop = "stop"
|
||||
FinishReasonToolCalls = "tool_calls"
|
||||
FinishReasonLength = "length"
|
||||
FinishReasonFunctionCall = "function_call"
|
||||
FinishReasonContentFilter = "content_filter"
|
||||
)
|
||||
|
||||
@@ -152,8 +152,8 @@ func buildTestRequest(model string) *dto.GeneralOpenAIRequest {
|
||||
Model: "", // this will be set later
|
||||
Stream: false,
|
||||
}
|
||||
if strings.HasPrefix(model, "o1-") {
|
||||
testRequest.MaxCompletionTokens = 1
|
||||
if strings.HasPrefix(model, "o1") {
|
||||
testRequest.MaxCompletionTokens = 10
|
||||
} else if strings.HasPrefix(model, "gemini-2.0-flash-thinking") {
|
||||
testRequest.MaxTokens = 2
|
||||
} else {
|
||||
|
||||
@@ -115,8 +115,8 @@ func FetchUpstreamModels(c *gin.Context) {
|
||||
// return
|
||||
//}
|
||||
baseURL := common.ChannelBaseURLs[channel.Type]
|
||||
if channel.GetBaseURL() == "" {
|
||||
channel.BaseURL = &baseURL
|
||||
if channel.GetBaseURL() != "" {
|
||||
baseURL = channel.GetBaseURL()
|
||||
}
|
||||
url := fmt.Sprintf("%s/v1/models", baseURL)
|
||||
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key))
|
||||
@@ -419,7 +419,8 @@ func EditTagChannels(c *gin.Context) {
|
||||
}
|
||||
|
||||
type ChannelBatch struct {
|
||||
Ids []int `json:"ids"`
|
||||
Ids []int `json:"ids"`
|
||||
Tag *string `json:"tag"`
|
||||
}
|
||||
|
||||
func DeleteChannelBatch(c *gin.Context) {
|
||||
@@ -570,3 +571,29 @@ func FetchModels(c *gin.Context) {
|
||||
"data": models,
|
||||
})
|
||||
}
|
||||
|
||||
func BatchSetChannelTag(c *gin.Context) {
|
||||
channelBatch := ChannelBatch{}
|
||||
err := c.ShouldBindJSON(&channelBatch)
|
||||
if err != nil || len(channelBatch.Ids) == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "参数错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
err = model.BatchSetChannelTag(channelBatch.Ids, channelBatch.Tag)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": len(channelBatch.Ids),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@ package controller
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"one-api/setting"
|
||||
)
|
||||
|
||||
func GetGroups(c *gin.Context) {
|
||||
groupNames := make([]string, 0)
|
||||
for groupName, _ := range common.GroupRatio {
|
||||
for groupName, _ := range setting.GetGroupRatioCopy() {
|
||||
groupNames = append(groupNames, groupName)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -24,9 +24,9 @@ func GetUserGroups(c *gin.Context) {
|
||||
userGroup := ""
|
||||
userId := c.GetInt("id")
|
||||
userGroup, _ = model.CacheGetUserGroup(userId)
|
||||
for groupName, _ := range common.GroupRatio {
|
||||
for groupName, _ := range setting.GetGroupRatioCopy() {
|
||||
// UserUsableGroups contains the groups that the user can use
|
||||
userUsableGroups := common.GetUserUsableGroups(userGroup)
|
||||
userUsableGroups := setting.GetUserUsableGroups(userGroup)
|
||||
if _, ok := userUsableGroups[groupName]; ok {
|
||||
usableGroups[groupName] = userUsableGroups[groupName]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"one-api/setting"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -83,7 +84,7 @@ func UpdateOption(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
case "GroupRatio":
|
||||
err = common.CheckGroupRatio(option.Value)
|
||||
err = setting.CheckGroupRatio(option.Value)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
|
||||
@@ -4,14 +4,38 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"one-api/common"
|
||||
"one-api/model"
|
||||
"one-api/setting"
|
||||
)
|
||||
|
||||
func GetPricing(c *gin.Context) {
|
||||
pricing := model.GetPricing()
|
||||
userId, exists := c.Get("id")
|
||||
usableGroup := map[string]string{}
|
||||
groupRatio := map[string]float64{}
|
||||
for s, f := range setting.GetGroupRatioCopy() {
|
||||
groupRatio[s] = f
|
||||
}
|
||||
var group string
|
||||
if exists {
|
||||
user, err := model.GetUserById(userId.(int), false)
|
||||
if err == nil {
|
||||
group = user.Group
|
||||
}
|
||||
}
|
||||
|
||||
usableGroup = setting.GetUserUsableGroups(group)
|
||||
// check groupRatio contains usableGroup
|
||||
for group := range setting.GetGroupRatioCopy() {
|
||||
if _, ok := usableGroup[group]; !ok {
|
||||
delete(groupRatio, group)
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"success": true,
|
||||
"data": pricing,
|
||||
"group_ratio": common.GroupRatio,
|
||||
"success": true,
|
||||
"data": pricing,
|
||||
"group_ratio": groupRatio,
|
||||
"usable_group": usableGroup,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"one-api/relay/constant"
|
||||
relayconstant "one-api/relay/constant"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -83,7 +84,7 @@ func Playground(c *gin.Context) {
|
||||
if group == "" {
|
||||
group = userGroup
|
||||
} else {
|
||||
if !common.GroupInUserUsableGroups(group) && group != userGroup {
|
||||
if !setting.GroupInUserUsableGroups(group) && group != userGroup {
|
||||
openaiErr = service.OpenAIErrorWrapperLocal(errors.New("无权访问该分组"), "group_not_allowed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"one-api/model"
|
||||
relayconstant "one-api/relay/constant"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -43,12 +44,12 @@ func Distribute() func(c *gin.Context) {
|
||||
tokenGroup := c.GetString("token_group")
|
||||
if tokenGroup != "" {
|
||||
// check common.UserUsableGroups[userGroup]
|
||||
if _, ok := common.GetUserUsableGroups(userGroup)[tokenGroup]; !ok {
|
||||
if _, ok := setting.GetUserUsableGroups(userGroup)[tokenGroup]; !ok {
|
||||
abortWithOpenAiMessage(c, http.StatusForbidden, fmt.Sprintf("令牌分组 %s 已被禁用", tokenGroup))
|
||||
return
|
||||
}
|
||||
// check group in common.GroupRatio
|
||||
if _, ok := common.GroupRatio[tokenGroup]; !ok {
|
||||
if !setting.ContainsGroupRatio(tokenGroup) {
|
||||
abortWithOpenAiMessage(c, http.StatusForbidden, fmt.Sprintf("分组 %s 已被弃用", tokenGroup))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ package model
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/samber/lo"
|
||||
"gorm.io/gorm"
|
||||
"one-api/common"
|
||||
"strings"
|
||||
|
||||
"github.com/samber/lo"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Ability struct {
|
||||
@@ -173,18 +174,67 @@ func (channel *Channel) DeleteAbilities() error {
|
||||
|
||||
// UpdateAbilities updates abilities of this channel.
|
||||
// Make sure the channel is completed before calling this function.
|
||||
func (channel *Channel) UpdateAbilities() error {
|
||||
// A quick and dirty way to update abilities
|
||||
func (channel *Channel) UpdateAbilities(tx *gorm.DB) error {
|
||||
isNewTx := false
|
||||
// 如果没有传入事务,创建新的事务
|
||||
if tx == nil {
|
||||
tx = DB.Begin()
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
isNewTx = true
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// First delete all abilities of this channel
|
||||
err := channel.DeleteAbilities()
|
||||
err := tx.Where("channel_id = ?", channel.Id).Delete(&Ability{}).Error
|
||||
if err != nil {
|
||||
if isNewTx {
|
||||
tx.Rollback()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Then add new abilities
|
||||
err = channel.AddAbilities()
|
||||
if err != nil {
|
||||
return err
|
||||
models_ := strings.Split(channel.Models, ",")
|
||||
groups_ := strings.Split(channel.Group, ",")
|
||||
abilities := make([]Ability, 0, len(models_))
|
||||
for _, model := range models_ {
|
||||
for _, group := range groups_ {
|
||||
ability := Ability{
|
||||
Group: group,
|
||||
Model: model,
|
||||
ChannelId: channel.Id,
|
||||
Enabled: channel.Status == common.ChannelStatusEnabled,
|
||||
Priority: channel.Priority,
|
||||
Weight: uint(channel.GetWeight()),
|
||||
Tag: channel.Tag,
|
||||
}
|
||||
abilities = append(abilities, ability)
|
||||
}
|
||||
}
|
||||
|
||||
if len(abilities) > 0 {
|
||||
for _, chunk := range lo.Chunk(abilities, 50) {
|
||||
err = tx.Create(&chunk).Error
|
||||
if err != nil {
|
||||
if isNewTx {
|
||||
tx.Rollback()
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是新创建的事务,需要提交
|
||||
if isNewTx {
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -246,7 +296,7 @@ func FixAbility() (int, error) {
|
||||
return 0, err
|
||||
}
|
||||
for _, channel := range channels {
|
||||
err := channel.UpdateAbilities()
|
||||
err := channel.UpdateAbilities(nil)
|
||||
if err != nil {
|
||||
common.SysError(fmt.Sprintf("Update abilities of channel %d failed: %s", channel.Id, err.Error()))
|
||||
} else {
|
||||
|
||||
@@ -257,7 +257,7 @@ func (channel *Channel) Update() error {
|
||||
return err
|
||||
}
|
||||
DB.Model(channel).First(channel, "id = ?", channel.Id)
|
||||
err = channel.UpdateAbilities()
|
||||
err = channel.UpdateAbilities(nil)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -389,7 +389,7 @@ func EditChannelByTag(tag string, newTag *string, modelMapping *string, models *
|
||||
channels, err := GetChannelsByTag(updatedTag, false)
|
||||
if err == nil {
|
||||
for _, channel := range channels {
|
||||
err = channel.UpdateAbilities()
|
||||
err = channel.UpdateAbilities(nil)
|
||||
if err != nil {
|
||||
common.SysError("failed to update abilities: " + err.Error())
|
||||
}
|
||||
@@ -509,3 +509,42 @@ func (channel *Channel) SetSetting(setting map[string]interface{}) {
|
||||
}
|
||||
channel.Setting = string(settingBytes)
|
||||
}
|
||||
|
||||
func GetChannelsByIds(ids []int) ([]*Channel, error) {
|
||||
var channels []*Channel
|
||||
err := DB.Where("id in (?)", ids).Find(&channels).Error
|
||||
return channels, err
|
||||
}
|
||||
|
||||
func BatchSetChannelTag(ids []int, tag *string) error {
|
||||
// 开启事务
|
||||
tx := DB.Begin()
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
|
||||
// 更新标签
|
||||
err := tx.Model(&Channel{}).Where("id in (?)", ids).Update("tag", tag).Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
// update ability status
|
||||
channels, err := GetChannelsByIds(ids)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
for _, channel := range channels {
|
||||
err = channel.UpdateAbilities(tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 提交事务
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
@@ -87,8 +87,8 @@ func InitOptionMap() {
|
||||
common.OptionMap["PreConsumedQuota"] = strconv.Itoa(common.PreConsumedQuota)
|
||||
common.OptionMap["ModelRatio"] = common.ModelRatio2JSONString()
|
||||
common.OptionMap["ModelPrice"] = common.ModelPrice2JSONString()
|
||||
common.OptionMap["GroupRatio"] = common.GroupRatio2JSONString()
|
||||
common.OptionMap["UserUsableGroups"] = common.UserUsableGroups2JSONString()
|
||||
common.OptionMap["GroupRatio"] = setting.GroupRatio2JSONString()
|
||||
common.OptionMap["UserUsableGroups"] = setting.UserUsableGroups2JSONString()
|
||||
common.OptionMap["CompletionRatio"] = common.CompletionRatio2JSONString()
|
||||
common.OptionMap["TopUpLink"] = common.TopUpLink
|
||||
common.OptionMap["ChatLink"] = common.ChatLink
|
||||
@@ -313,9 +313,9 @@ func updateOptionMap(key string, value string) (err error) {
|
||||
case "ModelRatio":
|
||||
err = common.UpdateModelRatioByJSONString(value)
|
||||
case "GroupRatio":
|
||||
err = common.UpdateGroupRatioByJSONString(value)
|
||||
err = setting.UpdateGroupRatioByJSONString(value)
|
||||
case "UserUsableGroups":
|
||||
err = common.UpdateUserUsableGroupsByJSONString(value)
|
||||
err = setting.UpdateUserUsableGroupsByJSONString(value)
|
||||
case "CompletionRatio":
|
||||
err = common.UpdateCompletionRatioByJSONString(value)
|
||||
case "ModelPrice":
|
||||
|
||||
@@ -4,7 +4,7 @@ type GeminiChatRequest struct {
|
||||
Contents []GeminiChatContent `json:"contents"`
|
||||
SafetySettings []GeminiChatSafetySettings `json:"safety_settings,omitempty"`
|
||||
GenerationConfig GeminiChatGenerationConfig `json:"generation_config,omitempty"`
|
||||
Tools []GeminiChatTools `json:"tools,omitempty"`
|
||||
Tools []GeminiChatTool `json:"tools,omitempty"`
|
||||
SystemInstructions *GeminiChatContent `json:"system_instruction,omitempty"`
|
||||
}
|
||||
|
||||
@@ -18,16 +18,39 @@ type FunctionCall struct {
|
||||
Arguments any `json:"args"`
|
||||
}
|
||||
|
||||
type GeminiFunctionResponseContent struct {
|
||||
Name string `json:"name"`
|
||||
Content any `json:"content"`
|
||||
}
|
||||
|
||||
type FunctionResponse struct {
|
||||
Name string `json:"name"`
|
||||
Response any `json:"response"`
|
||||
Name string `json:"name"`
|
||||
Response GeminiFunctionResponseContent `json:"response"`
|
||||
}
|
||||
|
||||
type GeminiPartExecutableCode struct {
|
||||
Language string `json:"language,omitempty"`
|
||||
Code string `json:"code,omitempty"`
|
||||
}
|
||||
|
||||
type GeminiPartCodeExecutionResult struct {
|
||||
Outcome string `json:"outcome,omitempty"`
|
||||
Output string `json:"output,omitempty"`
|
||||
}
|
||||
|
||||
type GeminiFileData struct {
|
||||
MimeType string `json:"mimeType,omitempty"`
|
||||
FileUri string `json:"fileUri,omitempty"`
|
||||
}
|
||||
|
||||
type GeminiPart struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
InlineData *GeminiInlineData `json:"inlineData,omitempty"`
|
||||
FunctionCall *FunctionCall `json:"functionCall,omitempty"`
|
||||
FunctionResponse *FunctionResponse `json:"functionResponse,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
InlineData *GeminiInlineData `json:"inlineData,omitempty"`
|
||||
FunctionCall *FunctionCall `json:"functionCall,omitempty"`
|
||||
FunctionResponse *FunctionResponse `json:"functionResponse,omitempty"`
|
||||
FileData *GeminiFileData `json:"fileData,omitempty"`
|
||||
ExecutableCode *GeminiPartExecutableCode `json:"executableCode,omitempty"`
|
||||
CodeExecutionResult *GeminiPartCodeExecutionResult `json:"codeExecutionResult,omitempty"`
|
||||
}
|
||||
|
||||
type GeminiChatContent struct {
|
||||
@@ -40,9 +63,11 @@ type GeminiChatSafetySettings struct {
|
||||
Threshold string `json:"threshold"`
|
||||
}
|
||||
|
||||
type GeminiChatTools struct {
|
||||
GoogleSearch any `json:"googleSearch,omitempty"`
|
||||
FunctionDeclarations any `json:"functionDeclarations,omitempty"`
|
||||
type GeminiChatTool struct {
|
||||
GoogleSearch any `json:"googleSearch,omitempty"`
|
||||
GoogleSearchRetrieval any `json:"googleSearchRetrieval,omitempty"`
|
||||
CodeExecution any `json:"codeExecution,omitempty"`
|
||||
FunctionDeclarations any `json:"functionDeclarations,omitempty"`
|
||||
}
|
||||
|
||||
type GeminiChatGenerationConfig struct {
|
||||
@@ -54,11 +79,12 @@ type GeminiChatGenerationConfig struct {
|
||||
StopSequences []string `json:"stopSequences,omitempty"`
|
||||
ResponseMimeType string `json:"responseMimeType,omitempty"`
|
||||
ResponseSchema any `json:"responseSchema,omitempty"`
|
||||
Seed int64 `json:"seed,omitempty"`
|
||||
}
|
||||
|
||||
type GeminiChatCandidate struct {
|
||||
Content GeminiChatContent `json:"content"`
|
||||
FinishReason string `json:"finishReason"`
|
||||
FinishReason *string `json:"finishReason"`
|
||||
Index int64 `json:"index"`
|
||||
SafetyRatings []GeminiChatSafetyRating `json:"safetyRatings"`
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
// Setting safety to the lowest possible values since Gemini is already powerless enough
|
||||
func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) (*GeminiChatRequest, error) {
|
||||
|
||||
geminiRequest := GeminiChatRequest{
|
||||
Contents: make([]GeminiChatContent, 0, len(textRequest.Messages)),
|
||||
SafetySettings: []GeminiChatSafetySettings{
|
||||
@@ -46,16 +47,24 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) (*GeminiChatReque
|
||||
Temperature: textRequest.Temperature,
|
||||
TopP: textRequest.TopP,
|
||||
MaxOutputTokens: textRequest.MaxTokens,
|
||||
Seed: int64(textRequest.Seed),
|
||||
},
|
||||
}
|
||||
|
||||
// openaiContent.FuncToToolCalls()
|
||||
if textRequest.Tools != nil {
|
||||
functions := make([]dto.FunctionCall, 0, len(textRequest.Tools))
|
||||
googleSearch := false
|
||||
codeExecution := false
|
||||
for _, tool := range textRequest.Tools {
|
||||
if tool.Function.Name == "googleSearch" {
|
||||
googleSearch = true
|
||||
continue
|
||||
}
|
||||
if tool.Function.Name == "codeExecution" {
|
||||
codeExecution = true
|
||||
continue
|
||||
}
|
||||
if tool.Function.Parameters != nil {
|
||||
params, ok := tool.Function.Parameters.(map[string]interface{})
|
||||
if ok {
|
||||
@@ -68,25 +77,32 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) (*GeminiChatReque
|
||||
}
|
||||
functions = append(functions, tool.Function)
|
||||
}
|
||||
if len(functions) > 0 {
|
||||
geminiRequest.Tools = []GeminiChatTools{
|
||||
{
|
||||
FunctionDeclarations: functions,
|
||||
},
|
||||
}
|
||||
if codeExecution {
|
||||
geminiRequest.Tools = append(geminiRequest.Tools, GeminiChatTool{
|
||||
CodeExecution: make(map[string]string),
|
||||
})
|
||||
}
|
||||
if googleSearch {
|
||||
geminiRequest.Tools = append(geminiRequest.Tools, GeminiChatTools{
|
||||
geminiRequest.Tools = append(geminiRequest.Tools, GeminiChatTool{
|
||||
GoogleSearch: make(map[string]string),
|
||||
})
|
||||
}
|
||||
if len(functions) > 0 {
|
||||
geminiRequest.Tools = append(geminiRequest.Tools, GeminiChatTool{
|
||||
FunctionDeclarations: functions,
|
||||
})
|
||||
}
|
||||
// common.SysLog("tools: " + fmt.Sprintf("%+v", geminiRequest.Tools))
|
||||
// json_data, _ := json.Marshal(geminiRequest.Tools)
|
||||
// common.SysLog("tools_json: " + string(json_data))
|
||||
} else if textRequest.Functions != nil {
|
||||
geminiRequest.Tools = []GeminiChatTools{
|
||||
geminiRequest.Tools = []GeminiChatTool{
|
||||
{
|
||||
FunctionDeclarations: textRequest.Functions,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if textRequest.ResponseFormat != nil && (textRequest.ResponseFormat.Type == "json_schema" || textRequest.ResponseFormat.Type == "json_object") {
|
||||
geminiRequest.GenerationConfig.ResponseMimeType = "application/json"
|
||||
|
||||
@@ -96,20 +112,14 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) (*GeminiChatReque
|
||||
}
|
||||
}
|
||||
tool_call_ids := make(map[string]string)
|
||||
var system_content []string
|
||||
//shouldAddDummyModelMessage := false
|
||||
for _, message := range textRequest.Messages {
|
||||
|
||||
if message.Role == "system" {
|
||||
geminiRequest.SystemInstructions = &GeminiChatContent{
|
||||
Parts: []GeminiPart{
|
||||
{
|
||||
Text: message.StringContent(),
|
||||
},
|
||||
},
|
||||
}
|
||||
system_content = append(system_content, message.StringContent())
|
||||
continue
|
||||
} else if message.Role == "tool" {
|
||||
if len(geminiRequest.Contents) == 0 || geminiRequest.Contents[len(geminiRequest.Contents)-1].Role != "user" {
|
||||
} else if message.Role == "tool" || message.Role == "function" {
|
||||
if len(geminiRequest.Contents) == 0 || geminiRequest.Contents[len(geminiRequest.Contents)-1].Role == "model" {
|
||||
geminiRequest.Contents = append(geminiRequest.Contents, GeminiChatContent{
|
||||
Role: "user",
|
||||
})
|
||||
@@ -121,9 +131,16 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) (*GeminiChatReque
|
||||
} else if val, exists := tool_call_ids[message.ToolCallId]; exists {
|
||||
name = val
|
||||
}
|
||||
content := common.StrToMap(message.StringContent())
|
||||
functionResp := &FunctionResponse{
|
||||
Name: name,
|
||||
Response: common.StrToMap(message.StringContent()),
|
||||
Name: name,
|
||||
Response: GeminiFunctionResponseContent{
|
||||
Name: name,
|
||||
Content: content,
|
||||
},
|
||||
}
|
||||
if content == nil {
|
||||
functionResp.Response.Content = message.StringContent()
|
||||
}
|
||||
*parts = append(*parts, GeminiPart{
|
||||
FunctionResponse: functionResp,
|
||||
@@ -134,57 +151,65 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) (*GeminiChatReque
|
||||
content := GeminiChatContent{
|
||||
Role: message.Role,
|
||||
}
|
||||
isToolCall := false
|
||||
// isToolCall := false
|
||||
if message.ToolCalls != nil {
|
||||
message.Role = "model"
|
||||
isToolCall = true
|
||||
// message.Role = "model"
|
||||
// isToolCall = true
|
||||
for _, call := range message.ParseToolCalls() {
|
||||
args := map[string]interface{}{}
|
||||
if call.Function.Arguments != "" {
|
||||
if json.Unmarshal([]byte(call.Function.Arguments), &args) != nil {
|
||||
return nil, fmt.Errorf("invalid arguments for function %s, args: %s", call.Function.Name, call.Function.Arguments)
|
||||
}
|
||||
}
|
||||
toolCall := GeminiPart{
|
||||
FunctionCall: &FunctionCall{
|
||||
FunctionName: call.Function.Name,
|
||||
Arguments: call.Function.Parameters,
|
||||
Arguments: args,
|
||||
},
|
||||
}
|
||||
parts = append(parts, toolCall)
|
||||
tool_call_ids[call.ID] = call.Function.Name
|
||||
}
|
||||
}
|
||||
if !isToolCall {
|
||||
openaiContent := message.ParseContent()
|
||||
imageNum := 0
|
||||
for _, part := range openaiContent {
|
||||
if part.Type == dto.ContentTypeText {
|
||||
parts = append(parts, GeminiPart{
|
||||
Text: part.Text,
|
||||
})
|
||||
} else if part.Type == dto.ContentTypeImageURL {
|
||||
imageNum += 1
|
||||
|
||||
if constant.GeminiVisionMaxImageNum != -1 && imageNum > constant.GeminiVisionMaxImageNum {
|
||||
return nil, fmt.Errorf("too many images in the message, max allowed is %d", constant.GeminiVisionMaxImageNum)
|
||||
}
|
||||
// 判断是否是url
|
||||
if strings.HasPrefix(part.ImageUrl.(dto.MessageImageUrl).Url, "http") {
|
||||
// 是url,获取图片的类型和base64编码的数据
|
||||
mimeType, data, _ := service.GetImageFromUrl(part.ImageUrl.(dto.MessageImageUrl).Url)
|
||||
parts = append(parts, GeminiPart{
|
||||
InlineData: &GeminiInlineData{
|
||||
MimeType: mimeType,
|
||||
Data: data,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
_, format, base64String, err := service.DecodeBase64ImageData(part.ImageUrl.(dto.MessageImageUrl).Url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode base64 image data failed: %s", err.Error())
|
||||
}
|
||||
parts = append(parts, GeminiPart{
|
||||
InlineData: &GeminiInlineData{
|
||||
MimeType: "image/" + format,
|
||||
Data: base64String,
|
||||
},
|
||||
})
|
||||
openaiContent := message.ParseContent()
|
||||
imageNum := 0
|
||||
for _, part := range openaiContent {
|
||||
if part.Type == dto.ContentTypeText {
|
||||
if part.Text == "" {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, GeminiPart{
|
||||
Text: part.Text,
|
||||
})
|
||||
} else if part.Type == dto.ContentTypeImageURL {
|
||||
imageNum += 1
|
||||
|
||||
if constant.GeminiVisionMaxImageNum != -1 && imageNum > constant.GeminiVisionMaxImageNum {
|
||||
return nil, fmt.Errorf("too many images in the message, max allowed is %d", constant.GeminiVisionMaxImageNum)
|
||||
}
|
||||
// 判断是否是url
|
||||
if strings.HasPrefix(part.ImageUrl.(dto.MessageImageUrl).Url, "http") {
|
||||
// 是url,获取图片的类型和base64编码的数据
|
||||
mimeType, data, _ := service.GetImageFromUrl(part.ImageUrl.(dto.MessageImageUrl).Url)
|
||||
parts = append(parts, GeminiPart{
|
||||
InlineData: &GeminiInlineData{
|
||||
MimeType: mimeType,
|
||||
Data: data,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
_, format, base64String, err := service.DecodeBase64ImageData(part.ImageUrl.(dto.MessageImageUrl).Url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode base64 image data failed: %s", err.Error())
|
||||
}
|
||||
parts = append(parts, GeminiPart{
|
||||
InlineData: &GeminiInlineData{
|
||||
MimeType: "image/" + format,
|
||||
Data: base64String,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,6 +222,17 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) (*GeminiChatReque
|
||||
}
|
||||
geminiRequest.Contents = append(geminiRequest.Contents, content)
|
||||
}
|
||||
|
||||
if len(system_content) > 0 {
|
||||
geminiRequest.SystemInstructions = &GeminiChatContent{
|
||||
Parts: []GeminiPart{
|
||||
{
|
||||
Text: strings.Join(system_content, "\n"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &geminiRequest, nil
|
||||
}
|
||||
|
||||
@@ -209,12 +245,12 @@ func removeAdditionalPropertiesWithDepth(schema interface{}, depth int) interfac
|
||||
if !ok || len(v) == 0 {
|
||||
return schema
|
||||
}
|
||||
|
||||
// 删除所有的title字段
|
||||
delete(v, "title")
|
||||
// 如果type不为object和array,则直接返回
|
||||
if typeVal, exists := v["type"]; !exists || (typeVal != "object" && typeVal != "array") {
|
||||
return schema
|
||||
}
|
||||
delete(v, "title")
|
||||
switch v["type"] {
|
||||
case "object":
|
||||
delete(v, "additionalProperties")
|
||||
@@ -240,15 +276,15 @@ func removeAdditionalPropertiesWithDepth(schema interface{}, depth int) interfac
|
||||
return v
|
||||
}
|
||||
|
||||
func (g *GeminiChatResponse) GetResponseText() string {
|
||||
if g == nil {
|
||||
return ""
|
||||
}
|
||||
if len(g.Candidates) > 0 && len(g.Candidates[0].Content.Parts) > 0 {
|
||||
return g.Candidates[0].Content.Parts[0].Text
|
||||
}
|
||||
return ""
|
||||
}
|
||||
// func (g *GeminiChatResponse) GetResponseText() string {
|
||||
// if g == nil {
|
||||
// return ""
|
||||
// }
|
||||
// if len(g.Candidates) > 0 && len(g.Candidates[0].Content.Parts) > 0 {
|
||||
// return g.Candidates[0].Content.Parts[0].Text
|
||||
// }
|
||||
// return ""
|
||||
// }
|
||||
|
||||
func getToolCall(item *GeminiPart) *dto.ToolCall {
|
||||
argsBytes, err := json.Marshal(item.FunctionCall.Arguments)
|
||||
@@ -298,11 +334,10 @@ func responseGeminiChat2OpenAI(response *GeminiChatResponse) *dto.OpenAITextResp
|
||||
Choices: make([]dto.OpenAITextResponseChoice, 0, len(response.Candidates)),
|
||||
}
|
||||
content, _ := json.Marshal("")
|
||||
for i, candidate := range response.Candidates {
|
||||
// jsonData, _ := json.MarshalIndent(candidate, "", " ")
|
||||
// common.SysLog(fmt.Sprintf("candidate: %v", string(jsonData)))
|
||||
is_tool_call := false
|
||||
for _, candidate := range response.Candidates {
|
||||
choice := dto.OpenAITextResponseChoice{
|
||||
Index: i,
|
||||
Index: int(candidate.Index),
|
||||
Message: dto.Message{
|
||||
Role: "assistant",
|
||||
Content: content,
|
||||
@@ -319,48 +354,107 @@ func responseGeminiChat2OpenAI(response *GeminiChatResponse) *dto.OpenAITextResp
|
||||
tool_calls = append(tool_calls, *call)
|
||||
}
|
||||
} else {
|
||||
texts = append(texts, part.Text)
|
||||
if part.ExecutableCode != nil {
|
||||
texts = append(texts, "```"+part.ExecutableCode.Language+"\n"+part.ExecutableCode.Code+"\n```")
|
||||
} else if part.CodeExecutionResult != nil {
|
||||
texts = append(texts, "```output\n"+part.CodeExecutionResult.Output+"\n```")
|
||||
} else {
|
||||
// 过滤掉空行
|
||||
if part.Text != "\n" {
|
||||
texts = append(texts, part.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(tool_calls) > 0 {
|
||||
choice.Message.SetToolCalls(tool_calls)
|
||||
is_tool_call = true
|
||||
}
|
||||
// 过滤掉空行
|
||||
|
||||
choice.Message.SetStringContent(strings.Join(texts, "\n"))
|
||||
choice.Message.SetToolCalls(tool_calls)
|
||||
|
||||
}
|
||||
if candidate.FinishReason != nil {
|
||||
switch *candidate.FinishReason {
|
||||
case "STOP":
|
||||
choice.FinishReason = constant.FinishReasonStop
|
||||
case "MAX_TOKENS":
|
||||
choice.FinishReason = constant.FinishReasonLength
|
||||
default:
|
||||
choice.FinishReason = constant.FinishReasonContentFilter
|
||||
}
|
||||
}
|
||||
if is_tool_call {
|
||||
choice.FinishReason = constant.FinishReasonToolCalls
|
||||
}
|
||||
|
||||
fullTextResponse.Choices = append(fullTextResponse.Choices, choice)
|
||||
}
|
||||
return &fullTextResponse
|
||||
}
|
||||
|
||||
func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) *dto.ChatCompletionsStreamResponse {
|
||||
var choice dto.ChatCompletionsStreamResponseChoice
|
||||
//choice.Delta.SetContentString(geminiResponse.GetResponseText())
|
||||
if len(geminiResponse.Candidates) > 0 && len(geminiResponse.Candidates[0].Content.Parts) > 0 {
|
||||
func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) (*dto.ChatCompletionsStreamResponse, bool) {
|
||||
choices := make([]dto.ChatCompletionsStreamResponseChoice, 0, len(geminiResponse.Candidates))
|
||||
is_stop := false
|
||||
for _, candidate := range geminiResponse.Candidates {
|
||||
if candidate.FinishReason != nil && *candidate.FinishReason == "STOP" {
|
||||
is_stop = true
|
||||
candidate.FinishReason = nil
|
||||
}
|
||||
choice := dto.ChatCompletionsStreamResponseChoice{
|
||||
Index: int(candidate.Index),
|
||||
Delta: dto.ChatCompletionsStreamResponseChoiceDelta{
|
||||
Role: "assistant",
|
||||
},
|
||||
}
|
||||
var texts []string
|
||||
var tool_calls []dto.ToolCall
|
||||
for _, part := range geminiResponse.Candidates[0].Content.Parts {
|
||||
if part.FunctionCall != nil {
|
||||
if call := getToolCall(&part); call != nil {
|
||||
tool_calls = append(tool_calls, *call)
|
||||
}
|
||||
} else {
|
||||
texts = append(texts, part.Text)
|
||||
isTools := false
|
||||
if candidate.FinishReason != nil {
|
||||
// p := GeminiConvertFinishReason(*candidate.FinishReason)
|
||||
switch *candidate.FinishReason {
|
||||
case "STOP":
|
||||
choice.FinishReason = &constant.FinishReasonStop
|
||||
case "MAX_TOKENS":
|
||||
choice.FinishReason = &constant.FinishReasonLength
|
||||
default:
|
||||
choice.FinishReason = &constant.FinishReasonContentFilter
|
||||
}
|
||||
}
|
||||
if len(texts) > 0 {
|
||||
choice.Delta.SetContentString(strings.Join(texts, "\n"))
|
||||
for _, part := range candidate.Content.Parts {
|
||||
if part.FunctionCall != nil {
|
||||
isTools = true
|
||||
if call := getToolCall(&part); call != nil {
|
||||
choice.Delta.ToolCalls = append(choice.Delta.ToolCalls, *call)
|
||||
}
|
||||
} else {
|
||||
if part.ExecutableCode != nil {
|
||||
texts = append(texts, "```"+part.ExecutableCode.Language+"\n"+part.ExecutableCode.Code+"\n```\n")
|
||||
} else if part.CodeExecutionResult != nil {
|
||||
texts = append(texts, "```output\n"+part.CodeExecutionResult.Output+"\n```\n")
|
||||
} else {
|
||||
if part.Text != "\n" {
|
||||
texts = append(texts, part.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(tool_calls) > 0 {
|
||||
choice.Delta.ToolCalls = tool_calls
|
||||
choice.Delta.SetContentString(strings.Join(texts, "\n"))
|
||||
if isTools {
|
||||
choice.FinishReason = &constant.FinishReasonToolCalls
|
||||
}
|
||||
choices = append(choices, choice)
|
||||
}
|
||||
|
||||
var response dto.ChatCompletionsStreamResponse
|
||||
response.Object = "chat.completion.chunk"
|
||||
response.Model = "gemini"
|
||||
response.Choices = []dto.ChatCompletionsStreamResponseChoice{choice}
|
||||
return &response
|
||||
response.Choices = choices
|
||||
return &response, is_stop
|
||||
}
|
||||
|
||||
func GeminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) {
|
||||
responseText := ""
|
||||
// responseText := ""
|
||||
id := fmt.Sprintf("chatcmpl-%s", common.GetUUID())
|
||||
createAt := common.GetTimestamp()
|
||||
var usage = &dto.Usage{}
|
||||
@@ -384,14 +478,11 @@ func GeminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycom
|
||||
continue
|
||||
}
|
||||
|
||||
response := streamResponseGeminiChat2OpenAI(&geminiResponse)
|
||||
if response == nil {
|
||||
continue
|
||||
}
|
||||
response, is_stop := streamResponseGeminiChat2OpenAI(&geminiResponse)
|
||||
response.Id = id
|
||||
response.Created = createAt
|
||||
response.Model = info.UpstreamModelName
|
||||
responseText += response.Choices[0].Delta.GetContentString()
|
||||
// responseText += response.Choices[0].Delta.GetContentString()
|
||||
if geminiResponse.UsageMetadata.TotalTokenCount != 0 {
|
||||
usage.PromptTokens = geminiResponse.UsageMetadata.PromptTokenCount
|
||||
usage.CompletionTokens = geminiResponse.UsageMetadata.CandidatesTokenCount
|
||||
@@ -400,12 +491,17 @@ func GeminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycom
|
||||
if err != nil {
|
||||
common.LogError(c, err.Error())
|
||||
}
|
||||
if is_stop {
|
||||
response := service.GenerateStopResponse(id, createAt, info.UpstreamModelName, constant.FinishReasonStop)
|
||||
service.ObjectData(c, response)
|
||||
}
|
||||
}
|
||||
|
||||
response := service.GenerateStopResponse(id, createAt, info.UpstreamModelName, constant.FinishReasonStop)
|
||||
service.ObjectData(c, response)
|
||||
var response *dto.ChatCompletionsStreamResponse
|
||||
|
||||
usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens
|
||||
usage.PromptTokensDetails.TextTokens = usage.PromptTokens
|
||||
usage.CompletionTokenDetails.TextTokens = usage.CompletionTokens
|
||||
|
||||
if info.ShouldIncludeUsage {
|
||||
response = service.GenerateFinalUsageResponse(id, createAt, info.UpstreamModelName, *usage)
|
||||
|
||||
@@ -74,7 +74,7 @@ func AudioHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
|
||||
}
|
||||
|
||||
modelRatio := common.GetModelRatio(audioRequest.Model)
|
||||
groupRatio := common.GetGroupRatio(relayInfo.Group)
|
||||
groupRatio := setting.GetGroupRatio(relayInfo.Group)
|
||||
ratio := modelRatio * groupRatio
|
||||
preConsumedQuota := int(float64(preConsumedTokens) * ratio)
|
||||
userQuota, err := model.CacheGetUserQuota(relayInfo.UserId)
|
||||
|
||||
@@ -99,7 +99,7 @@ func ImageHelper(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusCode {
|
||||
modelPrice = 0.0025 * modelRatio
|
||||
}
|
||||
|
||||
groupRatio := common.GetGroupRatio(relayInfo.Group)
|
||||
groupRatio := setting.GetGroupRatio(relayInfo.Group)
|
||||
userQuota, err := model.CacheGetUserQuota(relayInfo.UserId)
|
||||
|
||||
sizeRatio := 1.0
|
||||
|
||||
@@ -168,7 +168,7 @@ func RelaySwapFace(c *gin.Context) *dto.MidjourneyResponse {
|
||||
modelPrice = defaultPrice
|
||||
}
|
||||
}
|
||||
groupRatio := common.GetGroupRatio(group)
|
||||
groupRatio := setting.GetGroupRatio(group)
|
||||
ratio := modelPrice * groupRatio
|
||||
userQuota, err := model.CacheGetUserQuota(userId)
|
||||
if err != nil {
|
||||
@@ -474,7 +474,7 @@ func RelayMidjourneySubmit(c *gin.Context, relayMode int) *dto.MidjourneyRespons
|
||||
modelPrice = defaultPrice
|
||||
}
|
||||
}
|
||||
groupRatio := common.GetGroupRatio(group)
|
||||
groupRatio := setting.GetGroupRatio(group)
|
||||
ratio := modelPrice * groupRatio
|
||||
userQuota, err := model.CacheGetUserQuota(userId)
|
||||
if err != nil {
|
||||
|
||||
@@ -94,7 +94,7 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
|
||||
}
|
||||
relayInfo.UpstreamModelName = textRequest.Model
|
||||
modelPrice, getModelPriceSuccess := common.GetModelPrice(textRequest.Model, false)
|
||||
groupRatio := common.GetGroupRatio(relayInfo.Group)
|
||||
groupRatio := setting.GetGroupRatio(relayInfo.Group)
|
||||
|
||||
var preConsumedQuota int
|
||||
var ratio float64
|
||||
@@ -108,10 +108,17 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
|
||||
}
|
||||
}
|
||||
|
||||
promptTokens, err := getPromptTokens(textRequest, relayInfo)
|
||||
// count messages token error 计算promptTokens错误
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapper(err, "count_token_messages_failed", http.StatusInternalServerError)
|
||||
// 获取 promptTokens,如果上下文中已经存在,则直接使用
|
||||
var promptTokens int
|
||||
if value, exists := c.Get("prompt_tokens"); exists {
|
||||
promptTokens = value.(int)
|
||||
} else {
|
||||
promptTokens, err = getPromptTokens(textRequest, relayInfo)
|
||||
// count messages token error 计算promptTokens错误
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapper(err, "count_token_messages_failed", http.StatusInternalServerError)
|
||||
}
|
||||
c.Set("prompt_tokens", promptTokens)
|
||||
}
|
||||
|
||||
if !getModelPriceSuccess {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"one-api/dto"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
)
|
||||
|
||||
func getRerankPromptToken(rerankRequest dto.RerankRequest) int {
|
||||
@@ -57,7 +58,7 @@ func RerankHelper(c *gin.Context, relayMode int) (openaiErr *dto.OpenAIErrorWith
|
||||
|
||||
relayInfo.UpstreamModelName = rerankRequest.Model
|
||||
modelPrice, success := common.GetModelPrice(rerankRequest.Model, false)
|
||||
groupRatio := common.GetGroupRatio(relayInfo.Group)
|
||||
groupRatio := setting.GetGroupRatio(relayInfo.Group)
|
||||
|
||||
var preConsumedQuota int
|
||||
var ratio float64
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
relaycommon "one-api/relay/common"
|
||||
relayconstant "one-api/relay/constant"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -48,7 +49,7 @@ func RelayTaskSubmit(c *gin.Context, relayMode int) (taskErr *dto.TaskError) {
|
||||
}
|
||||
|
||||
// 预扣
|
||||
groupRatio := common.GetGroupRatio(relayInfo.Group)
|
||||
groupRatio := setting.GetGroupRatio(relayInfo.Group)
|
||||
ratio := modelPrice * groupRatio
|
||||
userQuota, err := model.CacheGetUserQuota(relayInfo.UserId)
|
||||
if err != nil {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"one-api/dto"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/service"
|
||||
"one-api/setting"
|
||||
)
|
||||
|
||||
//func getAndValidateWssRequest(c *gin.Context, ws *websocket.Conn) (*dto.RealtimeEvent, error) {
|
||||
@@ -57,7 +58,7 @@ func WssHelper(c *gin.Context, ws *websocket.Conn) (openaiErr *dto.OpenAIErrorWi
|
||||
}
|
||||
//relayInfo.UpstreamModelName = textRequest.Model
|
||||
modelPrice, getModelPriceSuccess := common.GetModelPrice(relayInfo.UpstreamModelName, false)
|
||||
groupRatio := common.GetGroupRatio(relayInfo.Group)
|
||||
groupRatio := setting.GetGroupRatio(relayInfo.Group)
|
||||
|
||||
var preConsumedQuota int
|
||||
var ratio float64
|
||||
|
||||
@@ -99,7 +99,7 @@ func SetApiRouter(router *gin.Engine) {
|
||||
channelRoute.POST("/fix", controller.FixChannelsAbilities)
|
||||
channelRoute.GET("/fetch_models/:id", controller.FetchUpstreamModels)
|
||||
channelRoute.POST("/fetch_models", controller.FetchModels)
|
||||
|
||||
channelRoute.POST("/batch/tag", controller.BatchSetChannelTag)
|
||||
}
|
||||
tokenRoute := apiRouter.Group("/token")
|
||||
tokenRoute.Use(middleware.UserAuth())
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"one-api/dto"
|
||||
"one-api/model"
|
||||
relaycommon "one-api/relay/common"
|
||||
"one-api/setting"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -36,7 +37,7 @@ func PreWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usag
|
||||
completionRatio := common.GetCompletionRatio(modelName)
|
||||
audioRatio := common.GetAudioRatio(relayInfo.UpstreamModelName)
|
||||
audioCompletionRatio := common.GetAudioCompletionRatio(modelName)
|
||||
groupRatio := common.GetGroupRatio(relayInfo.Group)
|
||||
groupRatio := setting.GetGroupRatio(relayInfo.Group)
|
||||
modelRatio := common.GetModelRatio(modelName)
|
||||
|
||||
ratio := groupRatio * modelRatio
|
||||
|
||||
@@ -1,33 +1,47 @@
|
||||
package common
|
||||
package setting
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"one-api/common"
|
||||
)
|
||||
|
||||
var GroupRatio = map[string]float64{
|
||||
var groupRatio = map[string]float64{
|
||||
"default": 1,
|
||||
"vip": 1,
|
||||
"svip": 1,
|
||||
}
|
||||
|
||||
func GetGroupRatioCopy() map[string]float64 {
|
||||
groupRatioCopy := make(map[string]float64)
|
||||
for k, v := range groupRatio {
|
||||
groupRatioCopy[k] = v
|
||||
}
|
||||
return groupRatioCopy
|
||||
}
|
||||
|
||||
func ContainsGroupRatio(name string) bool {
|
||||
_, ok := groupRatio[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
func GroupRatio2JSONString() string {
|
||||
jsonBytes, err := json.Marshal(GroupRatio)
|
||||
jsonBytes, err := json.Marshal(groupRatio)
|
||||
if err != nil {
|
||||
SysError("error marshalling model ratio: " + err.Error())
|
||||
common.SysError("error marshalling model ratio: " + err.Error())
|
||||
}
|
||||
return string(jsonBytes)
|
||||
}
|
||||
|
||||
func UpdateGroupRatioByJSONString(jsonStr string) error {
|
||||
GroupRatio = make(map[string]float64)
|
||||
return json.Unmarshal([]byte(jsonStr), &GroupRatio)
|
||||
groupRatio = make(map[string]float64)
|
||||
return json.Unmarshal([]byte(jsonStr), &groupRatio)
|
||||
}
|
||||
|
||||
func GetGroupRatio(name string) float64 {
|
||||
ratio, ok := GroupRatio[name]
|
||||
ratio, ok := groupRatio[name]
|
||||
if !ok {
|
||||
SysError("group ratio not found: " + name)
|
||||
common.SysError("group ratio not found: " + name)
|
||||
return 1
|
||||
}
|
||||
return ratio
|
||||
@@ -1,7 +1,8 @@
|
||||
package common
|
||||
package setting
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"one-api/common"
|
||||
)
|
||||
|
||||
var UserUsableGroups = map[string]string{
|
||||
@@ -12,7 +13,7 @@ var UserUsableGroups = map[string]string{
|
||||
func UserUsableGroups2JSONString() string {
|
||||
jsonBytes, err := json.Marshal(UserUsableGroups)
|
||||
if err != nil {
|
||||
SysError("error marshalling user groups: " + err.Error())
|
||||
common.SysError("error marshalling user groups: " + err.Error())
|
||||
}
|
||||
return string(jsonBytes)
|
||||
}
|
||||
@@ -162,9 +162,15 @@ const ChannelsTable = () => {
|
||||
return (
|
||||
<div>
|
||||
<Space spacing={2}>
|
||||
{text?.split(',').map((item, index) => {
|
||||
return renderGroup(item);
|
||||
})}
|
||||
{text?.split(',')
|
||||
.sort((a, b) => {
|
||||
if (a === 'default') return -1;
|
||||
if (b === 'default') return 1;
|
||||
return a.localeCompare(b);
|
||||
})
|
||||
.map((item, index) => {
|
||||
return renderGroup(item);
|
||||
})}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
@@ -507,6 +513,8 @@ const ChannelsTable = () => {
|
||||
const [selectedChannels, setSelectedChannels] = useState([]);
|
||||
const [showEditPriority, setShowEditPriority] = useState(false);
|
||||
const [enableTagMode, setEnableTagMode] = useState(false);
|
||||
const [showBatchSetTag, setShowBatchSetTag] = useState(false);
|
||||
const [batchSetTagValue, setBatchSetTagValue] = useState('');
|
||||
|
||||
|
||||
const removeRecord = (record) => {
|
||||
@@ -968,6 +976,29 @@ const ChannelsTable = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const batchSetChannelTag = async () => {
|
||||
if (selectedChannels.length === 0) {
|
||||
showError(t('请先选择要设置标签的渠道!'));
|
||||
return;
|
||||
}
|
||||
if (batchSetTagValue === '') {
|
||||
showError(t('标签不能为空!'));
|
||||
return;
|
||||
}
|
||||
let ids = selectedChannels.map(channel => channel.id);
|
||||
const res = await API.post('/api/channel/batch/tag', {
|
||||
ids: ids,
|
||||
tag: batchSetTagValue === '' ? null : batchSetTagValue
|
||||
});
|
||||
if (res.data.success) {
|
||||
showSuccess(t('已为 ${count} 个渠道设置标签!').replace('${count}', res.data.data));
|
||||
await refresh();
|
||||
setShowBatchSetTag(false);
|
||||
} else {
|
||||
showError(res.data.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EditTagModal
|
||||
@@ -1115,11 +1146,11 @@ const ChannelsTable = () => {
|
||||
</div>
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<Space>
|
||||
<Typography.Text strong>{t('开启批量删除')}</Typography.Text>
|
||||
<Typography.Text strong>{t('开启批量操作')}</Typography.Text>
|
||||
<Switch
|
||||
label={t('开启批量删除')}
|
||||
label={t('开启批量操作')}
|
||||
uncheckedText={t('关')}
|
||||
aria-label={t('是否开启批量删除')}
|
||||
aria-label={t('是否开启批量操作')}
|
||||
onChange={(v) => {
|
||||
setEnableBatchDelete(v);
|
||||
}}
|
||||
@@ -1167,7 +1198,17 @@ const ChannelsTable = () => {
|
||||
loadChannels(0, pageSize, idSort, v);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
disabled={!enableBatchDelete}
|
||||
theme="light"
|
||||
type="primary"
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={() => setShowBatchSetTag(true)}
|
||||
>
|
||||
{t('批量设置标签')}
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1201,6 +1242,23 @@ const ChannelsTable = () => {
|
||||
: null
|
||||
}
|
||||
/>
|
||||
<Modal
|
||||
title={t('批量设置标签')}
|
||||
visible={showBatchSetTag}
|
||||
onOk={batchSetChannelTag}
|
||||
onCancel={() => setShowBatchSetTag(false)}
|
||||
maskClosable={false}
|
||||
centered={true}
|
||||
>
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<Typography.Text>{t('请输入要设置的标签名称')}</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
placeholder={t('请输入标签名称')}
|
||||
value={batchSetTagValue}
|
||||
onChange={(v) => setBatchSetTagValue(v)}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -445,17 +445,11 @@ const LogsTable = () => {
|
||||
});
|
||||
|
||||
const handleInputChange = (value, name) => {
|
||||
if (value && (name === 'start_timestamp' || name === 'end_timestamp')) {
|
||||
// 确保日期值是有效的
|
||||
const dateValue = typeof value === 'string' ? value : timestamp2string(value);
|
||||
setInputs(inputs => ({ ...inputs, [name]: dateValue }));
|
||||
} else {
|
||||
setInputs(inputs => ({ ...inputs, [name]: value }));
|
||||
}
|
||||
setInputs(inputs => ({ ...inputs, [name]: value }));
|
||||
};
|
||||
|
||||
const getLogSelfStat = async () => {
|
||||
let localStartTimestamp = Date.parse(3) / 1000;
|
||||
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||
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}&group=${group}`;
|
||||
url = encodeURI(url);
|
||||
|
||||
@@ -162,36 +162,39 @@ const ModelPricing = () => {
|
||||
title: t('可用分组'),
|
||||
dataIndex: 'enable_groups',
|
||||
render: (text, record, index) => {
|
||||
|
||||
// enable_groups is a string array
|
||||
return (
|
||||
<Space>
|
||||
{text.map((group) => {
|
||||
if (group === selectedGroup) {
|
||||
return (
|
||||
<Tag
|
||||
color='blue'
|
||||
size='large'
|
||||
prefixIcon={<IconVerify />}
|
||||
>
|
||||
{group}
|
||||
</Tag>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Tag
|
||||
color='blue'
|
||||
size='large'
|
||||
onClick={() => {
|
||||
setSelectedGroup(group);
|
||||
showInfo(t('当前查看的分组为:{{group}},倍率为:{{ratio}}', {
|
||||
group: group,
|
||||
ratio: groupRatio[group]
|
||||
}));
|
||||
}}
|
||||
>
|
||||
{group}
|
||||
</Tag>
|
||||
);
|
||||
if (usableGroup[group]) {
|
||||
if (group === selectedGroup) {
|
||||
return (
|
||||
<Tag
|
||||
color='blue'
|
||||
size='large'
|
||||
prefixIcon={<IconVerify />}
|
||||
>
|
||||
{group}
|
||||
</Tag>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Tag
|
||||
color='blue'
|
||||
size='large'
|
||||
onClick={() => {
|
||||
setSelectedGroup(group);
|
||||
showInfo(t('当前查看的分组为:{{group}},倍率为:{{ratio}}', {
|
||||
group: group,
|
||||
ratio: groupRatio[group]
|
||||
}));
|
||||
}}
|
||||
>
|
||||
{group}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
}
|
||||
})}
|
||||
</Space>
|
||||
@@ -275,6 +278,7 @@ const ModelPricing = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [groupRatio, setGroupRatio] = useState({});
|
||||
const [usableGroup, setUsableGroup] = useState({});
|
||||
|
||||
const setModelsFormat = (models, groupRatio) => {
|
||||
for (let i = 0; i < models.length; i++) {
|
||||
@@ -309,9 +313,10 @@ const ModelPricing = () => {
|
||||
let url = '';
|
||||
url = `/api/pricing`;
|
||||
const res = await API.get(url);
|
||||
const { success, message, data, group_ratio } = res.data;
|
||||
const { success, message, data, group_ratio, usable_group } = res.data;
|
||||
if (success) {
|
||||
setGroupRatio(group_ratio);
|
||||
setUsableGroup(usable_group);
|
||||
setSelectedGroup(userState.user ? userState.user.group : 'default')
|
||||
setModelsFormat(data, group_ratio);
|
||||
} else {
|
||||
|
||||
@@ -406,7 +406,7 @@ const UsersTable = () => {
|
||||
if (searchKeyword === '') {
|
||||
await loadUsers(activePage - 1);
|
||||
} else {
|
||||
await searchUsers();
|
||||
await searchUsers(searchKeyword, searchGroup);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -59,6 +59,9 @@ export function renderNumber(num) {
|
||||
}
|
||||
|
||||
export function renderQuotaNumberWithDigit(num, digits = 2) {
|
||||
if (typeof num !== 'number' || isNaN(num)) {
|
||||
return 0;
|
||||
}
|
||||
let displayInCurrency = localStorage.getItem('display_in_currency');
|
||||
num = num.toFixed(digits);
|
||||
if (displayInCurrency) {
|
||||
|
||||
@@ -180,6 +180,9 @@ export function timestamp2string1(timestamp, dataExportDefaultTime = 'hour') {
|
||||
let month = (date.getMonth() + 1).toString();
|
||||
let day = date.getDate().toString();
|
||||
let hour = date.getHours().toString();
|
||||
if (day === '24') {
|
||||
console.log("timestamp", timestamp);
|
||||
}
|
||||
if (month.length === 1) {
|
||||
month = '0' + month;
|
||||
}
|
||||
|
||||
@@ -546,8 +546,8 @@
|
||||
"是否用ID排序": "Whether to sort by ID",
|
||||
"确定?": "Sure?",
|
||||
"确定是否要删除禁用通道?": "Are you sure you want to delete the disabled channel?",
|
||||
"开启批量删除": "Enable batch selection",
|
||||
"是否开启批量删除": "Whether to enable batch selection",
|
||||
"开启批量操作": "Enable batch selection",
|
||||
"是否开启批量操作": "Whether to enable batch selection",
|
||||
"确定是否要删除所选通道?": "Are you sure you want to delete the selected channels?",
|
||||
"确定是否要修复数据库一致性?": "Are you sure you want to repair database consistency?",
|
||||
"进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用": "When performing this operation, it may cause channel access errors. Please only use it when there is a problem with the database.",
|
||||
|
||||
@@ -430,6 +430,8 @@
|
||||
"一小时后过期": "Expires after one hour",
|
||||
"一分钟后过期": "Expires after one minute",
|
||||
"创建新的令牌": "Create New Token",
|
||||
"令牌分组,默认为用户的分组": "Token group, default is the your's group",
|
||||
"IP白名单(请勿过度信任此功能)": "IP whitelist (do not overly trust this function)",
|
||||
"注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。": "Note that the quota of the token is only used to limit the maximum quota usage of the token itself, and the actual usage is limited by the remaining quota of the account.",
|
||||
"设为无限额度": "Set to unlimited quota",
|
||||
"更新令牌信息": "Update Token Information",
|
||||
@@ -546,8 +548,8 @@
|
||||
"是否用ID排序": "Whether to sort by ID",
|
||||
"确定?": "Sure?",
|
||||
"确定是否要删除禁用通道?": "Are you sure you want to delete the disabled channel?",
|
||||
"开启批量删除": "Enable batch selection",
|
||||
"是否开启批量删除": "Whether to enable batch selection",
|
||||
"开启批量操作": "Enable batch selection",
|
||||
"是否开启批量操作": "Whether to enable batch selection",
|
||||
"确定是否要删除所选通道?": "Are you sure you want to delete the selected channels?",
|
||||
"确定是否要修复数据库一致性?": "Are you sure you want to repair database consistency?",
|
||||
"进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用": "When performing this operation, it may cause channel access errors. Please only use it when there is a problem with the database.",
|
||||
@@ -866,8 +868,8 @@
|
||||
"请选择模式": "Please select mode",
|
||||
"图片代理方式": "Picture agency method",
|
||||
"用于替换 https://cdn.discordapp.com 的域名": "The domain name used to replace https://cdn.discordapp.com",
|
||||
"一个月": "a month",
|
||||
"一天": "one day",
|
||||
"一个月": "A month",
|
||||
"一天": "One day",
|
||||
"令牌渠道分组选择": "Token channel grouping selection",
|
||||
"只可使用对应分组包含的模型。": "Only models contained in the corresponding group can be used.",
|
||||
"渠道分组": "Channel grouping",
|
||||
@@ -876,7 +878,7 @@
|
||||
"启用模型限制(非必要,不建议启用)": "Enable model restrictions (not necessary, not recommended)",
|
||||
"秒": "Second",
|
||||
"更新令牌后需等待几分钟生效": "It will take a few minutes to take effect after updating the token.",
|
||||
"一小时": "one hour",
|
||||
"一小时": "One hour",
|
||||
"新建数量": "New quantity",
|
||||
"加载失败,请稍后重试": "Loading failed, please try again later",
|
||||
"未设置": "Not set",
|
||||
@@ -1235,5 +1237,8 @@
|
||||
"更多": "Expand more",
|
||||
"个模型": "models",
|
||||
"可用模型": "Available models",
|
||||
"时间范围": "Time range"
|
||||
"时间范围": "Time range",
|
||||
"批量设置标签": "Batch set tag",
|
||||
"请输入要设置的标签名称": "Please enter the tag name to be set",
|
||||
"请输入标签名称": "Please enter the tag name"
|
||||
}
|
||||
@@ -143,8 +143,7 @@ const Detail = (props) => {
|
||||
content: [
|
||||
{
|
||||
key: (datum) => datum['Model'],
|
||||
value: (datum) =>
|
||||
renderQuotaNumberWithDigit(parseFloat(datum['Usage']), 4),
|
||||
value: (datum) => renderQuota(datum['rawQuota'] || 0, 4),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -152,22 +151,28 @@ const Detail = (props) => {
|
||||
content: [
|
||||
{
|
||||
key: (datum) => datum['Model'],
|
||||
value: (datum) => datum['Usage'],
|
||||
value: (datum) => datum['rawQuota'] || 0,
|
||||
},
|
||||
],
|
||||
updateContent: (array) => {
|
||||
array.sort((a, b) => b.value - a.value);
|
||||
let sum = 0;
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
sum += parseFloat(array[i].value);
|
||||
array[i].value = renderQuotaNumberWithDigit(
|
||||
parseFloat(array[i].value),
|
||||
4,
|
||||
);
|
||||
if (array[i].key == "其他") {
|
||||
continue;
|
||||
}
|
||||
let value = parseFloat(array[i].value);
|
||||
if (isNaN(value)) {
|
||||
value = 0;
|
||||
}
|
||||
if (array[i].datum && array[i].datum.TimeSum) {
|
||||
sum = array[i].datum.TimeSum;
|
||||
}
|
||||
array[i].value = renderQuota(value, 4);
|
||||
}
|
||||
array.unshift({
|
||||
key: t('总计'),
|
||||
value: renderQuotaNumberWithDigit(sum, 4),
|
||||
value: renderQuota(sum, 4),
|
||||
});
|
||||
return array;
|
||||
},
|
||||
@@ -212,19 +217,8 @@ const Detail = (props) => {
|
||||
created_at: now.getTime() / 1000,
|
||||
});
|
||||
}
|
||||
// 根据dataExportDefaultTime重制时间粒度
|
||||
let timeGranularity = 3600;
|
||||
if (dataExportDefaultTime === 'day') {
|
||||
timeGranularity = 86400;
|
||||
} else if (dataExportDefaultTime === 'week') {
|
||||
timeGranularity = 604800;
|
||||
}
|
||||
// sort created_at
|
||||
data.sort((a, b) => a.created_at - b.created_at);
|
||||
data.forEach((item) => {
|
||||
item['created_at'] =
|
||||
Math.floor(item['created_at'] / timeGranularity) * timeGranularity;
|
||||
});
|
||||
updateChartData(data);
|
||||
} else {
|
||||
showError(message);
|
||||
@@ -250,14 +244,14 @@ const Detail = (props) => {
|
||||
let uniqueModels = new Set();
|
||||
let totalTokens = 0;
|
||||
|
||||
// 收集所有唯一的模型名称和时间点
|
||||
let uniqueTimes = new Set();
|
||||
// 收集所有唯一的模型名称
|
||||
data.forEach(item => {
|
||||
uniqueModels.add(item.model_name);
|
||||
uniqueTimes.add(timestamp2string1(item.created_at, dataExportDefaultTime));
|
||||
totalTokens += item.token_used;
|
||||
totalQuota += item.quota;
|
||||
totalTimes += item.count;
|
||||
});
|
||||
|
||||
|
||||
// 处理颜色映射
|
||||
const newModelColors = {};
|
||||
Array.from(uniqueModels).forEach((modelName) => {
|
||||
@@ -267,56 +261,82 @@ const Detail = (props) => {
|
||||
});
|
||||
setModelColors(newModelColors);
|
||||
|
||||
// 处理饼图数据
|
||||
for (let item of data) {
|
||||
totalQuota += item.quota;
|
||||
totalTimes += item.count;
|
||||
|
||||
let pieItem = newPieData.find((it) => it.type === item.model_name);
|
||||
if (pieItem) {
|
||||
pieItem.value += item.count;
|
||||
} else {
|
||||
newPieData.push({
|
||||
type: item.model_name,
|
||||
value: item.count,
|
||||
// 按时间和模型聚合数据
|
||||
let aggregatedData = new Map();
|
||||
data.forEach(item => {
|
||||
const timeKey = timestamp2string1(item.created_at, dataExportDefaultTime);
|
||||
const modelKey = item.model_name;
|
||||
const key = `${timeKey}-${modelKey}`;
|
||||
|
||||
if (!aggregatedData.has(key)) {
|
||||
aggregatedData.set(key, {
|
||||
time: timeKey,
|
||||
model: modelKey,
|
||||
quota: 0,
|
||||
count: 0
|
||||
});
|
||||
}
|
||||
|
||||
const existing = aggregatedData.get(key);
|
||||
existing.quota += item.quota;
|
||||
existing.count += item.count;
|
||||
});
|
||||
|
||||
// 处理饼图数据
|
||||
let modelTotals = new Map();
|
||||
for (let [_, value] of aggregatedData) {
|
||||
if (!modelTotals.has(value.model)) {
|
||||
modelTotals.set(value.model, 0);
|
||||
}
|
||||
modelTotals.set(value.model, modelTotals.get(value.model) + value.count);
|
||||
}
|
||||
|
||||
// 处理柱状图数据
|
||||
let timePoints = Array.from(uniqueTimes);
|
||||
newPieData = Array.from(modelTotals).map(([model, count]) => ({
|
||||
type: model,
|
||||
value: count
|
||||
}));
|
||||
|
||||
// 生成时间点序列
|
||||
let timePoints = Array.from(new Set([...aggregatedData.values()].map(d => d.time)));
|
||||
if (timePoints.length < 7) {
|
||||
// 根据时间粒度生成合适的时间点
|
||||
const generateTimePoints = () => {
|
||||
let lastTime = Math.max(...data.map(item => item.created_at));
|
||||
let points = [];
|
||||
let interval = dataExportDefaultTime === 'hour' ? 3600
|
||||
const lastTime = Math.max(...data.map(item => item.created_at));
|
||||
const interval = dataExportDefaultTime === 'hour' ? 3600
|
||||
: dataExportDefaultTime === 'day' ? 86400
|
||||
: 604800;
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
points.push(timestamp2string1(lastTime - (i * interval), dataExportDefaultTime));
|
||||
}
|
||||
return points.reverse();
|
||||
};
|
||||
|
||||
timePoints = generateTimePoints();
|
||||
|
||||
timePoints = Array.from({length: 7}, (_, i) =>
|
||||
timestamp2string1(lastTime - (6-i) * interval, dataExportDefaultTime)
|
||||
);
|
||||
}
|
||||
|
||||
// 为每个时间点和模型生成数据
|
||||
// 生成柱状图数据
|
||||
timePoints.forEach(time => {
|
||||
Array.from(uniqueModels).forEach(model => {
|
||||
let existingData = data.find(item =>
|
||||
timestamp2string1(item.created_at, dataExportDefaultTime) === time &&
|
||||
item.model_name === model
|
||||
);
|
||||
|
||||
newLineData.push({
|
||||
// 为每个时间点收集所有模型的数据
|
||||
let timeData = Array.from(uniqueModels).map(model => {
|
||||
const key = `${time}-${model}`;
|
||||
const aggregated = aggregatedData.get(key);
|
||||
return {
|
||||
Time: time,
|
||||
Model: model,
|
||||
Usage: existingData ? parseFloat(getQuotaWithUnit(existingData.quota)) : 0
|
||||
});
|
||||
rawQuota: aggregated?.quota || 0,
|
||||
Usage: aggregated?.quota ? getQuotaWithUnit(aggregated.quota, 4) : 0
|
||||
};
|
||||
});
|
||||
|
||||
// 计算该时间点的总计
|
||||
const timeSum = timeData.reduce((sum, item) => sum + item.rawQuota, 0);
|
||||
|
||||
// 按照 rawQuota 从大到小排序
|
||||
timeData.sort((a, b) => b.rawQuota - a.rawQuota);
|
||||
|
||||
// 为每个数据点添加该时间的总计
|
||||
timeData = timeData.map(item => ({
|
||||
...item,
|
||||
TimeSum: timeSum
|
||||
}));
|
||||
|
||||
// 将排序后的数据添加到 newLineData
|
||||
newLineData.push(...timeData);
|
||||
});
|
||||
|
||||
// 排序
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from '@douyinfe/semi-ui';
|
||||
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
|
||||
import { Divider } from 'semantic-ui-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const EditToken = (props) => {
|
||||
const [isEdit, setIsEdit] = useState(false);
|
||||
@@ -52,6 +53,7 @@ const EditToken = (props) => {
|
||||
const [models, setModels] = useState([]);
|
||||
const [groups, setGroups] = useState([]);
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const handleInputChange = (name, value) => {
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
};
|
||||
@@ -87,7 +89,7 @@ const EditToken = (props) => {
|
||||
}));
|
||||
setModels(localModelOptions);
|
||||
} else {
|
||||
showError(message);
|
||||
showError(t(message));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -95,15 +97,13 @@ const EditToken = (props) => {
|
||||
let res = await API.get(`/api/user/self/groups`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
// return data is a map, key is group name, value is group description
|
||||
// label is group description, value is group name
|
||||
let localGroupOptions = Object.keys(data).map((group) => ({
|
||||
label: data[group],
|
||||
value: group,
|
||||
}));
|
||||
setGroups(localGroupOptions);
|
||||
let localGroupOptions = Object.keys(data).map((group) => ({
|
||||
label: data[group],
|
||||
value: group,
|
||||
}));
|
||||
setGroups(localGroupOptions);
|
||||
} else {
|
||||
showError(message);
|
||||
showError(t(message));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -176,7 +176,7 @@ const EditToken = (props) => {
|
||||
if (localInputs.expired_time !== -1) {
|
||||
let time = Date.parse(localInputs.expired_time);
|
||||
if (isNaN(time)) {
|
||||
showError('过期时间格式错误!');
|
||||
showError(t('过期时间格式错误!'));
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -189,11 +189,11 @@ const EditToken = (props) => {
|
||||
});
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess('令牌更新成功!');
|
||||
showSuccess(t('令牌更新成功!'));
|
||||
props.refresh();
|
||||
props.handleClose();
|
||||
} else {
|
||||
showError(message);
|
||||
showError(t(message));
|
||||
}
|
||||
} else {
|
||||
// 处理新增多个令牌的情况
|
||||
@@ -209,7 +209,7 @@ const EditToken = (props) => {
|
||||
if (localInputs.expired_time !== -1) {
|
||||
let time = Date.parse(localInputs.expired_time);
|
||||
if (isNaN(time)) {
|
||||
showError('过期时间格式错误!');
|
||||
showError(t('过期时间格式错误!'));
|
||||
setLoading(false);
|
||||
break;
|
||||
}
|
||||
@@ -222,14 +222,14 @@ const EditToken = (props) => {
|
||||
if (success) {
|
||||
successCount++;
|
||||
} else {
|
||||
showError(message);
|
||||
showError(t(message));
|
||||
break; // 如果创建失败,终止循环
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount > 0) {
|
||||
showSuccess(
|
||||
`${successCount}个令牌创建成功,请在列表页面点击复制获取令牌!`,
|
||||
t('令牌创建成功,请在列表页面点击复制获取令牌!')
|
||||
);
|
||||
props.refresh();
|
||||
props.handleClose();
|
||||
@@ -245,7 +245,7 @@ const EditToken = (props) => {
|
||||
<SideSheet
|
||||
placement={isEdit ? 'right' : 'left'}
|
||||
title={
|
||||
<Title level={3}>{isEdit ? '更新令牌信息' : '创建新的令牌'}</Title>
|
||||
<Title level={3}>{isEdit ? t('更新令牌信息') : t('创建新的令牌')}</Title>
|
||||
}
|
||||
headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
|
||||
bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
|
||||
@@ -254,7 +254,7 @@ const EditToken = (props) => {
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Space>
|
||||
<Button theme='solid' size={'large'} onClick={submit}>
|
||||
提交
|
||||
{t('提交')}
|
||||
</Button>
|
||||
<Button
|
||||
theme='solid'
|
||||
@@ -262,7 +262,7 @@ const EditToken = (props) => {
|
||||
type={'tertiary'}
|
||||
onClick={handleCancel}
|
||||
>
|
||||
取消
|
||||
{t('取消')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
@@ -274,9 +274,9 @@ const EditToken = (props) => {
|
||||
<Spin spinning={loading}>
|
||||
<Input
|
||||
style={{ marginTop: 20 }}
|
||||
label='名称'
|
||||
label={t('名称')}
|
||||
name='name'
|
||||
placeholder={'请输入名称'}
|
||||
placeholder={t('请输入名称')}
|
||||
onChange={(value) => handleInputChange('name', value)}
|
||||
value={name}
|
||||
autoComplete='new-password'
|
||||
@@ -284,9 +284,9 @@ const EditToken = (props) => {
|
||||
/>
|
||||
<Divider />
|
||||
<DatePicker
|
||||
label='过期时间'
|
||||
label={t('过期时间')}
|
||||
name='expired_time'
|
||||
placeholder={'请选择过期时间'}
|
||||
placeholder={t('请选择过期时间')}
|
||||
onChange={(value) => handleInputChange('expired_time', value)}
|
||||
value={expired_time}
|
||||
autoComplete='new-password'
|
||||
@@ -300,7 +300,7 @@ const EditToken = (props) => {
|
||||
setExpiredTime(0, 0, 0, 0);
|
||||
}}
|
||||
>
|
||||
永不过期
|
||||
{t('永不过期')}
|
||||
</Button>
|
||||
<Button
|
||||
type={'tertiary'}
|
||||
@@ -308,7 +308,7 @@ const EditToken = (props) => {
|
||||
setExpiredTime(0, 0, 1, 0);
|
||||
}}
|
||||
>
|
||||
一小时
|
||||
{t('一小时')}
|
||||
</Button>
|
||||
<Button
|
||||
type={'tertiary'}
|
||||
@@ -316,7 +316,7 @@ const EditToken = (props) => {
|
||||
setExpiredTime(1, 0, 0, 0);
|
||||
}}
|
||||
>
|
||||
一个月
|
||||
{t('一个月')}
|
||||
</Button>
|
||||
<Button
|
||||
type={'tertiary'}
|
||||
@@ -324,7 +324,7 @@ const EditToken = (props) => {
|
||||
setExpiredTime(0, 1, 0, 0);
|
||||
}}
|
||||
>
|
||||
一天
|
||||
{t('一天')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
@@ -332,17 +332,15 @@ const EditToken = (props) => {
|
||||
<Divider />
|
||||
<Banner
|
||||
type={'warning'}
|
||||
description={
|
||||
'注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。'
|
||||
}
|
||||
description={t('注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。')}
|
||||
></Banner>
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<Typography.Text>{`额度${renderQuotaWithPrompt(remain_quota)}`}</Typography.Text>
|
||||
<Typography.Text>{`${t('额度')}${renderQuotaWithPrompt(remain_quota)}`}</Typography.Text>
|
||||
</div>
|
||||
<AutoComplete
|
||||
style={{ marginTop: 8 }}
|
||||
name='remain_quota'
|
||||
placeholder={'请输入额度'}
|
||||
placeholder={t('请输入额度')}
|
||||
onChange={(value) => handleInputChange('remain_quota', value)}
|
||||
value={remain_quota}
|
||||
autoComplete='new-password'
|
||||
@@ -362,22 +360,22 @@ const EditToken = (props) => {
|
||||
{!isEdit && (
|
||||
<>
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<Typography.Text>新建数量</Typography.Text>
|
||||
<Typography.Text>{t('新建数量')}</Typography.Text>
|
||||
</div>
|
||||
<AutoComplete
|
||||
style={{ marginTop: 8 }}
|
||||
label='数量'
|
||||
placeholder={'请选择或输入创建令牌的数量'}
|
||||
label={t('数量')}
|
||||
placeholder={t('请选择或输入创建令牌的数量')}
|
||||
onChange={(value) => handleTokenCountChange(value)}
|
||||
onSelect={(value) => handleTokenCountChange(value)}
|
||||
value={tokenCount.toString()}
|
||||
autoComplete='off'
|
||||
type='number'
|
||||
data={[
|
||||
{ value: 10, label: '10个' },
|
||||
{ value: 20, label: '20个' },
|
||||
{ value: 30, label: '30个' },
|
||||
{ value: 100, label: '100个' },
|
||||
{ value: 10, label: t('10个') },
|
||||
{ value: 20, label: t('20个') },
|
||||
{ value: 30, label: t('30个') },
|
||||
{ value: 100, label: t('100个') },
|
||||
]}
|
||||
disabled={unlimited_quota}
|
||||
/>
|
||||
@@ -392,17 +390,17 @@ const EditToken = (props) => {
|
||||
setUnlimitedQuota();
|
||||
}}
|
||||
>
|
||||
{unlimited_quota ? '取消无限额度' : '设为无限额度'}
|
||||
{unlimited_quota ? t('取消无限额度') : t('设为无限额度')}
|
||||
</Button>
|
||||
</div>
|
||||
<Divider />
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text>IP白名单(请勿过度信任此功能)</Typography.Text>
|
||||
<Typography.Text>{t('IP白名单(请勿过度信任此功能)')}</Typography.Text>
|
||||
</div>
|
||||
<TextArea
|
||||
label='IP白名单'
|
||||
label={t('IP白名单')}
|
||||
name='allow_ips'
|
||||
placeholder={'允许的IP,一行一个'}
|
||||
placeholder={t('允许的IP,一行一个')}
|
||||
onChange={(value) => {
|
||||
handleInputChange('allow_ips', value);
|
||||
}}
|
||||
@@ -417,16 +415,15 @@ const EditToken = (props) => {
|
||||
onChange={(e) =>
|
||||
handleInputChange('model_limits_enabled', e.target.checked)
|
||||
}
|
||||
></Checkbox>
|
||||
<Typography.Text>
|
||||
启用模型限制(非必要,不建议启用)
|
||||
</Typography.Text>
|
||||
>
|
||||
{t('启用模型限制(非必要,不建议启用)')}
|
||||
</Checkbox>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
style={{ marginTop: 8 }}
|
||||
placeholder={'请选择该渠道所支持的模型'}
|
||||
placeholder={t('请选择该渠道所支持的模型')}
|
||||
name='models'
|
||||
required
|
||||
multiple
|
||||
@@ -440,12 +437,12 @@ const EditToken = (props) => {
|
||||
disabled={!model_limits_enabled}
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text>令牌分组,默认为用户的分组</Typography.Text>
|
||||
<Typography.Text>{t('令牌分组,默认为用户的分组')}</Typography.Text>
|
||||
</div>
|
||||
{groups.length > 0 ?
|
||||
<Select
|
||||
style={{ marginTop: 8 }}
|
||||
placeholder={'令牌分组,默认为用户的分组'}
|
||||
placeholder={t('令牌分组,默认为用户的分组')}
|
||||
name='gruop'
|
||||
required
|
||||
selection
|
||||
@@ -458,7 +455,7 @@ const EditToken = (props) => {
|
||||
/>:
|
||||
<Select
|
||||
style={{ marginTop: 8 }}
|
||||
placeholder={'管理员未设置用户可选分组'}
|
||||
placeholder={t('管理员未设置用户可选分组')}
|
||||
name='gruop'
|
||||
disabled={true}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user