Merge branch 'alpha'

This commit is contained in:
CaIon
2025-09-19 14:20:15 +08:00
14 changed files with 342 additions and 70 deletions

View File

@@ -501,9 +501,10 @@ func validateChannel(channel *model.Channel, isAdd bool) error {
} }
type AddChannelRequest struct { type AddChannelRequest struct {
Mode string `json:"mode"` Mode string `json:"mode"`
MultiKeyMode constant.MultiKeyMode `json:"multi_key_mode"` MultiKeyMode constant.MultiKeyMode `json:"multi_key_mode"`
Channel *model.Channel `json:"channel"` BatchAddSetKeyPrefix2Name bool `json:"batch_add_set_key_prefix_2_name"`
Channel *model.Channel `json:"channel"`
} }
func getVertexArrayKeys(keys string) ([]string, error) { func getVertexArrayKeys(keys string) ([]string, error) {
@@ -616,6 +617,13 @@ func AddChannel(c *gin.Context) {
} }
localChannel := addChannelRequest.Channel localChannel := addChannelRequest.Channel
localChannel.Key = key localChannel.Key = key
if addChannelRequest.BatchAddSetKeyPrefix2Name && len(keys) > 1 {
keyPrefix := localChannel.Key
if len(localChannel.Key) > 8 {
keyPrefix = localChannel.Key[:8]
}
localChannel.Name = fmt.Sprintf("%s %s", localChannel.Name, keyPrefix)
}
channels = append(channels, *localChannel) channels = append(channels, *localChannel)
} }
err = model.BatchInsertChannels(channels) err = model.BatchInsertChannels(channels)

View File

@@ -128,6 +128,33 @@ func UpdateOption(c *gin.Context) {
}) })
return return
} }
case "ImageRatio":
err = ratio_setting.UpdateImageRatioByJSONString(option.Value)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "图片倍率设置失败: " + err.Error(),
})
return
}
case "AudioRatio":
err = ratio_setting.UpdateAudioRatioByJSONString(option.Value)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "音频倍率设置失败: " + err.Error(),
})
return
}
case "AudioCompletionRatio":
err = ratio_setting.UpdateAudioCompletionRatioByJSONString(option.Value)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "音频补全倍率设置失败: " + err.Error(),
})
return
}
case "ModelRequestRateLimitGroup": case "ModelRequestRateLimitGroup":
err = setting.CheckModelRequestRateLimitGroup(option.Value.(string)) err = setting.CheckModelRequestRateLimitGroup(option.Value.(string))
if err != nil { if err != nil {

View File

@@ -112,6 +112,9 @@ func InitOptionMap() {
common.OptionMap["GroupGroupRatio"] = ratio_setting.GroupGroupRatio2JSONString() common.OptionMap["GroupGroupRatio"] = ratio_setting.GroupGroupRatio2JSONString()
common.OptionMap["UserUsableGroups"] = setting.UserUsableGroups2JSONString() common.OptionMap["UserUsableGroups"] = setting.UserUsableGroups2JSONString()
common.OptionMap["CompletionRatio"] = ratio_setting.CompletionRatio2JSONString() common.OptionMap["CompletionRatio"] = ratio_setting.CompletionRatio2JSONString()
common.OptionMap["ImageRatio"] = ratio_setting.ImageRatio2JSONString()
common.OptionMap["AudioRatio"] = ratio_setting.AudioRatio2JSONString()
common.OptionMap["AudioCompletionRatio"] = ratio_setting.AudioCompletionRatio2JSONString()
common.OptionMap["TopUpLink"] = common.TopUpLink common.OptionMap["TopUpLink"] = common.TopUpLink
//common.OptionMap["ChatLink"] = common.ChatLink //common.OptionMap["ChatLink"] = common.ChatLink
//common.OptionMap["ChatLink2"] = common.ChatLink2 //common.OptionMap["ChatLink2"] = common.ChatLink2
@@ -397,6 +400,12 @@ func updateOptionMap(key string, value string) (err error) {
err = ratio_setting.UpdateModelPriceByJSONString(value) err = ratio_setting.UpdateModelPriceByJSONString(value)
case "CacheRatio": case "CacheRatio":
err = ratio_setting.UpdateCacheRatioByJSONString(value) err = ratio_setting.UpdateCacheRatioByJSONString(value)
case "ImageRatio":
err = ratio_setting.UpdateImageRatioByJSONString(value)
case "AudioRatio":
err = ratio_setting.UpdateAudioRatioByJSONString(value)
case "AudioCompletionRatio":
err = ratio_setting.UpdateAudioCompletionRatioByJSONString(value)
case "TopUpLink": case "TopUpLink":
common.TopUpLink = value common.TopUpLink = value
//case "ChatLink": //case "ChatLink":

View File

@@ -23,6 +23,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference?hl=zh-cn#blob
var geminiSupportedMimeTypes = map[string]bool{ var geminiSupportedMimeTypes = map[string]bool{
"application/pdf": true, "application/pdf": true,
"audio/mpeg": true, "audio/mpeg": true,
@@ -30,6 +31,7 @@ var geminiSupportedMimeTypes = map[string]bool{
"audio/wav": true, "audio/wav": true,
"image/png": true, "image/png": true,
"image/jpeg": true, "image/jpeg": true,
"image/webp": true,
"text/plain": true, "text/plain": true,
"video/mov": true, "video/mov": true,
"video/mpeg": true, "video/mpeg": true,

View File

@@ -94,6 +94,9 @@ func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycom
// BuildRequestURL constructs the upstream URL. // BuildRequestURL constructs the upstream URL.
func (a *TaskAdaptor) BuildRequestURL(info *relaycommon.RelayInfo) (string, error) { func (a *TaskAdaptor) BuildRequestURL(info *relaycommon.RelayInfo) (string, error) {
if isNewAPIRelay(info.ApiKey) {
return fmt.Sprintf("%s/jimeng/?Action=CVSync2AsyncSubmitTask&Version=2022-08-31", a.baseURL), nil
}
return fmt.Sprintf("%s/?Action=CVSync2AsyncSubmitTask&Version=2022-08-31", a.baseURL), nil return fmt.Sprintf("%s/?Action=CVSync2AsyncSubmitTask&Version=2022-08-31", a.baseURL), nil
} }
@@ -101,7 +104,12 @@ func (a *TaskAdaptor) BuildRequestURL(info *relaycommon.RelayInfo) (string, erro
func (a *TaskAdaptor) BuildRequestHeader(c *gin.Context, req *http.Request, info *relaycommon.RelayInfo) error { func (a *TaskAdaptor) BuildRequestHeader(c *gin.Context, req *http.Request, info *relaycommon.RelayInfo) error {
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json") req.Header.Set("Accept", "application/json")
return a.signRequest(req, a.accessKey, a.secretKey) if isNewAPIRelay(info.ApiKey) {
req.Header.Set("Authorization", "Bearer "+info.ApiKey)
} else {
return a.signRequest(req, a.accessKey, a.secretKey)
}
return nil
} }
// BuildRequestBody converts request into Jimeng specific format. // BuildRequestBody converts request into Jimeng specific format.
@@ -161,6 +169,9 @@ func (a *TaskAdaptor) FetchTask(baseUrl, key string, body map[string]any) (*http
} }
uri := fmt.Sprintf("%s/?Action=CVSync2AsyncGetResult&Version=2022-08-31", baseUrl) uri := fmt.Sprintf("%s/?Action=CVSync2AsyncGetResult&Version=2022-08-31", baseUrl)
if isNewAPIRelay(key) {
uri = fmt.Sprintf("%s/jimeng/?Action=CVSync2AsyncGetResult&Version=2022-08-31", a.baseURL)
}
payload := map[string]string{ payload := map[string]string{
"req_key": "jimeng_vgfm_t2v_l20", // This is fixed value from doc: https://www.volcengine.com/docs/85621/1544774 "req_key": "jimeng_vgfm_t2v_l20", // This is fixed value from doc: https://www.volcengine.com/docs/85621/1544774
"task_id": taskID, "task_id": taskID,
@@ -178,17 +189,20 @@ func (a *TaskAdaptor) FetchTask(baseUrl, key string, body map[string]any) (*http
req.Header.Set("Accept", "application/json") req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
keyParts := strings.Split(key, "|") if isNewAPIRelay(key) {
if len(keyParts) != 2 { req.Header.Set("Authorization", "Bearer "+key)
return nil, fmt.Errorf("invalid api key format for jimeng: expected 'ak|sk'") } else {
} keyParts := strings.Split(key, "|")
accessKey := strings.TrimSpace(keyParts[0]) if len(keyParts) != 2 {
secretKey := strings.TrimSpace(keyParts[1]) return nil, fmt.Errorf("invalid api key format for jimeng: expected 'ak|sk'")
}
accessKey := strings.TrimSpace(keyParts[0])
secretKey := strings.TrimSpace(keyParts[1])
if err := a.signRequest(req, accessKey, secretKey); err != nil { if err := a.signRequest(req, accessKey, secretKey); err != nil {
return nil, errors.Wrap(err, "sign request failed") return nil, errors.Wrap(err, "sign request failed")
}
} }
return service.GetHttpClient().Do(req) return service.GetHttpClient().Do(req)
} }
@@ -384,3 +398,7 @@ func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, e
taskResult.Url = resTask.Data.VideoUrl taskResult.Url = resTask.Data.VideoUrl
return &taskResult, nil return &taskResult, nil
} }
func isNewAPIRelay(apiKey string) bool {
return strings.HasPrefix(apiKey, "sk-")
}

View File

@@ -117,6 +117,11 @@ func (a *TaskAdaptor) ValidateRequestAndSetAction(c *gin.Context, info *relaycom
// BuildRequestURL constructs the upstream URL. // BuildRequestURL constructs the upstream URL.
func (a *TaskAdaptor) BuildRequestURL(info *relaycommon.RelayInfo) (string, error) { func (a *TaskAdaptor) BuildRequestURL(info *relaycommon.RelayInfo) (string, error) {
path := lo.Ternary(info.Action == constant.TaskActionGenerate, "/v1/videos/image2video", "/v1/videos/text2video") path := lo.Ternary(info.Action == constant.TaskActionGenerate, "/v1/videos/image2video", "/v1/videos/text2video")
if isNewAPIRelay(info.ApiKey) {
return fmt.Sprintf("%s/kling%s", a.baseURL, path), nil
}
return fmt.Sprintf("%s%s", a.baseURL, path), nil return fmt.Sprintf("%s%s", a.baseURL, path), nil
} }
@@ -199,6 +204,9 @@ func (a *TaskAdaptor) FetchTask(baseUrl, key string, body map[string]any) (*http
} }
path := lo.Ternary(action == constant.TaskActionGenerate, "/v1/videos/image2video", "/v1/videos/text2video") path := lo.Ternary(action == constant.TaskActionGenerate, "/v1/videos/image2video", "/v1/videos/text2video")
url := fmt.Sprintf("%s%s/%s", baseUrl, path, taskID) url := fmt.Sprintf("%s%s/%s", baseUrl, path, taskID)
if isNewAPIRelay(key) {
url = fmt.Sprintf("%s/kling%s/%s", baseUrl, path, taskID)
}
req, err := http.NewRequest(http.MethodGet, url, nil) req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil { if err != nil {
@@ -304,8 +312,13 @@ func (a *TaskAdaptor) createJWTToken() (string, error) {
//} //}
func (a *TaskAdaptor) createJWTTokenWithKey(apiKey string) (string, error) { func (a *TaskAdaptor) createJWTTokenWithKey(apiKey string) (string, error) {
if isNewAPIRelay(apiKey) {
return apiKey, nil // new api relay
}
keyParts := strings.Split(apiKey, "|") keyParts := strings.Split(apiKey, "|")
if len(keyParts) != 2 {
return "", errors.New("invalid api_key, required format is accessKey|secretKey")
}
accessKey := strings.TrimSpace(keyParts[0]) accessKey := strings.TrimSpace(keyParts[0])
if len(keyParts) == 1 { if len(keyParts) == 1 {
return accessKey, nil return accessKey, nil
@@ -352,3 +365,7 @@ func (a *TaskAdaptor) ParseTaskResult(respBody []byte) (*relaycommon.TaskInfo, e
} }
return taskInfo, nil return taskInfo, nil
} }
func isNewAPIRelay(apiKey string) bool {
return strings.HasPrefix(apiKey, "sk-")
}

View File

@@ -90,41 +90,43 @@ func TextHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *types
if info.ChannelSetting.SystemPrompt != "" { if info.ChannelSetting.SystemPrompt != "" {
// 如果有系统提示,则将其添加到请求中 // 如果有系统提示,则将其添加到请求中
request := convertedRequest.(*dto.GeneralOpenAIRequest) request, ok := convertedRequest.(*dto.GeneralOpenAIRequest)
containSystemPrompt := false if ok {
for _, message := range request.Messages { containSystemPrompt := false
if message.Role == request.GetSystemRoleName() { for _, message := range request.Messages {
containSystemPrompt = true
break
}
}
if !containSystemPrompt {
// 如果没有系统提示,则添加系统提示
systemMessage := dto.Message{
Role: request.GetSystemRoleName(),
Content: info.ChannelSetting.SystemPrompt,
}
request.Messages = append([]dto.Message{systemMessage}, request.Messages...)
} else if info.ChannelSetting.SystemPromptOverride {
common.SetContextKey(c, constant.ContextKeySystemPromptOverride, true)
// 如果有系统提示,且允许覆盖,则拼接到前面
for i, message := range request.Messages {
if message.Role == request.GetSystemRoleName() { if message.Role == request.GetSystemRoleName() {
if message.IsStringContent() { containSystemPrompt = true
request.Messages[i].SetStringContent(info.ChannelSetting.SystemPrompt + "\n" + message.StringContent())
} else {
contents := message.ParseContent()
contents = append([]dto.MediaContent{
{
Type: dto.ContentTypeText,
Text: info.ChannelSetting.SystemPrompt,
},
}, contents...)
request.Messages[i].Content = contents
}
break break
} }
} }
if !containSystemPrompt {
// 如果没有系统提示,则添加系统提示
systemMessage := dto.Message{
Role: request.GetSystemRoleName(),
Content: info.ChannelSetting.SystemPrompt,
}
request.Messages = append([]dto.Message{systemMessage}, request.Messages...)
} else if info.ChannelSetting.SystemPromptOverride {
common.SetContextKey(c, constant.ContextKeySystemPromptOverride, true)
// 如果有系统提示,且允许覆盖,则拼接到前面
for i, message := range request.Messages {
if message.Role == request.GetSystemRoleName() {
if message.IsStringContent() {
request.Messages[i].SetStringContent(info.ChannelSetting.SystemPrompt + "\n" + message.StringContent())
} else {
contents := message.ParseContent()
contents = append([]dto.MediaContent{
{
Type: dto.ContentTypeText,
Text: info.ChannelSetting.SystemPrompt,
},
}, contents...)
request.Messages[i].Content = contents
}
break
}
}
}
} }
} }

View File

@@ -52,6 +52,8 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
var cacheRatio float64 var cacheRatio float64
var imageRatio float64 var imageRatio float64
var cacheCreationRatio float64 var cacheCreationRatio float64
var audioRatio float64
var audioCompletionRatio float64
if !usePrice { if !usePrice {
preConsumedTokens := common.Max(promptTokens, common.PreConsumedQuota) preConsumedTokens := common.Max(promptTokens, common.PreConsumedQuota)
if meta.MaxTokens != 0 { if meta.MaxTokens != 0 {
@@ -73,6 +75,8 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
cacheRatio, _ = ratio_setting.GetCacheRatio(info.OriginModelName) cacheRatio, _ = ratio_setting.GetCacheRatio(info.OriginModelName)
cacheCreationRatio, _ = ratio_setting.GetCreateCacheRatio(info.OriginModelName) cacheCreationRatio, _ = ratio_setting.GetCreateCacheRatio(info.OriginModelName)
imageRatio, _ = ratio_setting.GetImageRatio(info.OriginModelName) imageRatio, _ = ratio_setting.GetImageRatio(info.OriginModelName)
audioRatio = ratio_setting.GetAudioRatio(info.OriginModelName)
audioCompletionRatio = ratio_setting.GetAudioCompletionRatio(info.OriginModelName)
ratio := modelRatio * groupRatioInfo.GroupRatio ratio := modelRatio * groupRatioInfo.GroupRatio
preConsumedQuota = int(float64(preConsumedTokens) * ratio) preConsumedQuota = int(float64(preConsumedTokens) * ratio)
} else { } else {
@@ -90,6 +94,8 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
UsePrice: usePrice, UsePrice: usePrice,
CacheRatio: cacheRatio, CacheRatio: cacheRatio,
ImageRatio: imageRatio, ImageRatio: imageRatio,
AudioRatio: audioRatio,
AudioCompletionRatio: audioCompletionRatio,
CacheCreationRatio: cacheCreationRatio, CacheCreationRatio: cacheCreationRatio,
ShouldPreConsumedQuota: preConsumedQuota, ShouldPreConsumedQuota: preConsumedQuota,
} }

View File

@@ -19,7 +19,7 @@ func ReturnPreConsumedQuota(c *gin.Context, relayInfo *relaycommon.RelayInfo) {
gopool.Go(func() { gopool.Go(func() {
relayInfoCopy := *relayInfo relayInfoCopy := *relayInfo
err := PostConsumeQuota(&relayInfoCopy, -relayInfo.FinalPreConsumedQuota, 0, false) err := PostConsumeQuota(&relayInfoCopy, -relayInfoCopy.FinalPreConsumedQuota, 0, false)
if err != nil { if err != nil {
common.SysLog("error return pre-consumed quota: " + err.Error()) common.SysLog("error return pre-consumed quota: " + err.Error())
} }

View File

@@ -279,6 +279,18 @@ var defaultModelPrice = map[string]float64{
"mj_upload": 0.05, "mj_upload": 0.05,
} }
var defaultAudioRatio = map[string]float64{
"gpt-4o-audio-preview": 16,
"gpt-4o-mini-audio-preview": 66.67,
"gpt-4o-realtime-preview": 8,
"gpt-4o-mini-realtime-preview": 16.67,
}
var defaultAudioCompletionRatio = map[string]float64{
"gpt-4o-realtime": 2,
"gpt-4o-mini-realtime": 2,
}
var ( var (
modelPriceMap map[string]float64 = nil modelPriceMap map[string]float64 = nil
modelPriceMapMutex = sync.RWMutex{} modelPriceMapMutex = sync.RWMutex{}
@@ -327,6 +339,15 @@ func InitRatioSettings() {
imageRatioMap = defaultImageRatio imageRatioMap = defaultImageRatio
imageRatioMapMutex.Unlock() imageRatioMapMutex.Unlock()
// initialize audioRatioMap
audioRatioMapMutex.Lock()
audioRatioMap = defaultAudioRatio
audioRatioMapMutex.Unlock()
// initialize audioCompletionRatioMap
audioCompletionRatioMapMutex.Lock()
audioCompletionRatioMap = defaultAudioCompletionRatio
audioCompletionRatioMapMutex.Unlock()
} }
func GetModelPriceMap() map[string]float64 { func GetModelPriceMap() map[string]float64 {
@@ -418,6 +439,18 @@ func GetDefaultModelRatioMap() map[string]float64 {
return defaultModelRatio return defaultModelRatio
} }
func GetDefaultImageRatioMap() map[string]float64 {
return defaultImageRatio
}
func GetDefaultAudioRatioMap() map[string]float64 {
return defaultAudioRatio
}
func GetDefaultAudioCompletionRatioMap() map[string]float64 {
return defaultAudioCompletionRatio
}
func GetCompletionRatioMap() map[string]float64 { func GetCompletionRatioMap() map[string]float64 {
CompletionRatioMutex.RLock() CompletionRatioMutex.RLock()
defer CompletionRatioMutex.RUnlock() defer CompletionRatioMutex.RUnlock()
@@ -585,32 +618,22 @@ func getHardcodedCompletionModelRatio(name string) (float64, bool) {
} }
func GetAudioRatio(name string) float64 { func GetAudioRatio(name string) float64 {
if strings.Contains(name, "-realtime") { audioRatioMapMutex.RLock()
if strings.HasSuffix(name, "gpt-4o-realtime-preview") { defer audioRatioMapMutex.RUnlock()
return 8 name = FormatMatchingModelName(name)
} else if strings.Contains(name, "gpt-4o-mini-realtime-preview") { if ratio, ok := audioRatioMap[name]; ok {
return 10 / 0.6 return ratio
} else {
return 20
}
}
if strings.Contains(name, "-audio") {
if strings.HasPrefix(name, "gpt-4o-audio-preview") {
return 40 / 2.5
} else if strings.HasPrefix(name, "gpt-4o-mini-audio-preview") {
return 10 / 0.15
} else {
return 40
}
} }
return 20 return 20
} }
func GetAudioCompletionRatio(name string) float64 { func GetAudioCompletionRatio(name string) float64 {
if strings.HasPrefix(name, "gpt-4o-realtime") { audioCompletionRatioMapMutex.RLock()
return 2 defer audioCompletionRatioMapMutex.RUnlock()
} else if strings.HasPrefix(name, "gpt-4o-mini-realtime") { name = FormatMatchingModelName(name)
return 2 if ratio, ok := audioCompletionRatioMap[name]; ok {
return ratio
} }
return 2 return 2
} }
@@ -631,6 +654,14 @@ var defaultImageRatio = map[string]float64{
} }
var imageRatioMap map[string]float64 var imageRatioMap map[string]float64
var imageRatioMapMutex sync.RWMutex var imageRatioMapMutex sync.RWMutex
var (
audioRatioMap map[string]float64 = nil
audioRatioMapMutex = sync.RWMutex{}
)
var (
audioCompletionRatioMap map[string]float64 = nil
audioCompletionRatioMapMutex = sync.RWMutex{}
)
func ImageRatio2JSONString() string { func ImageRatio2JSONString() string {
imageRatioMapMutex.RLock() imageRatioMapMutex.RLock()
@@ -659,6 +690,71 @@ func GetImageRatio(name string) (float64, bool) {
return ratio, true return ratio, true
} }
func AudioRatio2JSONString() string {
audioRatioMapMutex.RLock()
defer audioRatioMapMutex.RUnlock()
jsonBytes, err := common.Marshal(audioRatioMap)
if err != nil {
common.SysError("error marshalling audio ratio: " + err.Error())
}
return string(jsonBytes)
}
func UpdateAudioRatioByJSONString(jsonStr string) error {
tmp := make(map[string]float64)
if err := common.Unmarshal([]byte(jsonStr), &tmp); err != nil {
return err
}
audioRatioMapMutex.Lock()
audioRatioMap = tmp
audioRatioMapMutex.Unlock()
InvalidateExposedDataCache()
return nil
}
func GetAudioRatioCopy() map[string]float64 {
audioRatioMapMutex.RLock()
defer audioRatioMapMutex.RUnlock()
copyMap := make(map[string]float64, len(audioRatioMap))
for k, v := range audioRatioMap {
copyMap[k] = v
}
return copyMap
}
func AudioCompletionRatio2JSONString() string {
audioCompletionRatioMapMutex.RLock()
defer audioCompletionRatioMapMutex.RUnlock()
jsonBytes, err := common.Marshal(audioCompletionRatioMap)
if err != nil {
common.SysError("error marshalling audio completion ratio: " + err.Error())
}
return string(jsonBytes)
}
func UpdateAudioCompletionRatioByJSONString(jsonStr string) error {
tmp := make(map[string]float64)
if err := common.Unmarshal([]byte(jsonStr), &tmp); err != nil {
return err
}
audioCompletionRatioMapMutex.Lock()
audioCompletionRatioMap = tmp
audioCompletionRatioMapMutex.Unlock()
InvalidateExposedDataCache()
return nil
}
func GetAudioCompletionRatioCopy() map[string]float64 {
audioCompletionRatioMapMutex.RLock()
defer audioCompletionRatioMapMutex.RUnlock()
copyMap := make(map[string]float64, len(audioCompletionRatioMap))
for k, v := range audioCompletionRatioMap {
copyMap[k] = v
}
return copyMap
}
func GetModelRatioCopy() map[string]float64 { func GetModelRatioCopy() map[string]float64 {
modelRatioMapMutex.RLock() modelRatioMapMutex.RLock()
defer modelRatioMapMutex.RUnlock() defer modelRatioMapMutex.RUnlock()

View File

@@ -15,6 +15,8 @@ type PriceData struct {
CacheRatio float64 CacheRatio float64
CacheCreationRatio float64 CacheCreationRatio float64
ImageRatio float64 ImageRatio float64
AudioRatio float64
AudioCompletionRatio float64
UsePrice bool UsePrice bool
ShouldPreConsumedQuota int ShouldPreConsumedQuota int
GroupRatioInfo GroupRatioInfo GroupRatioInfo GroupRatioInfo
@@ -27,5 +29,5 @@ type PerCallPriceData struct {
} }
func (p PriceData) ToSetting() string { func (p PriceData) ToSetting() string {
return fmt.Sprintf("ModelPrice: %f, ModelRatio: %f, CompletionRatio: %f, CacheRatio: %f, GroupRatio: %f, UsePrice: %t, CacheCreationRatio: %f, ShouldPreConsumedQuota: %d, ImageRatio: %f", p.ModelPrice, p.ModelRatio, p.CompletionRatio, p.CacheRatio, p.GroupRatioInfo.GroupRatio, p.UsePrice, p.CacheCreationRatio, p.ShouldPreConsumedQuota, p.ImageRatio) return fmt.Sprintf("ModelPrice: %f, ModelRatio: %f, CompletionRatio: %f, CacheRatio: %f, GroupRatio: %f, UsePrice: %t, CacheCreationRatio: %f, ShouldPreConsumedQuota: %d, ImageRatio: %f, AudioRatio: %f, AudioCompletionRatio: %f", p.ModelPrice, p.ModelRatio, p.CompletionRatio, p.CacheRatio, p.GroupRatioInfo.GroupRatio, p.UsePrice, p.CacheCreationRatio, p.ShouldPreConsumedQuota, p.ImageRatio, p.AudioRatio, p.AudioCompletionRatio)
} }

View File

@@ -39,6 +39,9 @@ const RatioSetting = () => {
CompletionRatio: '', CompletionRatio: '',
GroupRatio: '', GroupRatio: '',
GroupGroupRatio: '', GroupGroupRatio: '',
ImageRatio: '',
AudioRatio: '',
AudioCompletionRatio: '',
AutoGroups: '', AutoGroups: '',
DefaultUseAutoGroup: false, DefaultUseAutoGroup: false,
ExposeRatioEnabled: false, ExposeRatioEnabled: false,
@@ -61,7 +64,10 @@ const RatioSetting = () => {
item.key === 'UserUsableGroups' || item.key === 'UserUsableGroups' ||
item.key === 'CompletionRatio' || item.key === 'CompletionRatio' ||
item.key === 'ModelPrice' || item.key === 'ModelPrice' ||
item.key === 'CacheRatio' item.key === 'CacheRatio' ||
item.key === 'ImageRatio' ||
item.key === 'AudioRatio' ||
item.key === 'AudioCompletionRatio'
) { ) {
try { try {
item.value = JSON.stringify(JSON.parse(item.value), null, 2); item.value = JSON.stringify(JSON.parse(item.value), null, 2);

View File

@@ -1999,6 +1999,16 @@
"查看渠道密钥": "View channel key", "查看渠道密钥": "View channel key",
"渠道密钥信息": "Channel key information", "渠道密钥信息": "Channel key information",
"密钥获取成功": "Key acquisition successful", "密钥获取成功": "Key acquisition successful",
"模型补全倍率(仅对自定义模型有效)": "Model completion ratio (only effective for custom models)",
"图片倍率": "Image ratio",
"音频倍率": "Audio ratio",
"音频补全倍率": "Audio completion ratio",
"图片输入相关的倍率设置,键为模型名称,值为倍率": "Image input related ratio settings, key is model name, value is ratio",
"音频输入相关的倍率设置,键为模型名称,值为倍率": "Audio input related ratio settings, key is model name, value is ratio",
"音频输出补全相关的倍率设置,键为模型名称,值为倍率": "Audio output completion related ratio settings, key is model name, value is ratio",
"为一个 JSON 文本,键为模型名称,值为倍率,例如:{\"gpt-image-1\": 2}": "A JSON text with model name as key and ratio as value, e.g.: {\"gpt-image-1\": 2}",
"为一个 JSON 文本,键为模型名称,值为倍率,例如:{\"gpt-4o-audio-preview\": 16}": "A JSON text with model name as key and ratio as value, e.g.: {\"gpt-4o-audio-preview\": 16}",
"为一个 JSON 文本,键为模型名称,值为倍率,例如:{\"gpt-4o-realtime\": 2}": "A JSON text with model name as key and ratio as value, e.g.: {\"gpt-4o-realtime\": 2}",
"顶栏管理": "Header Management", "顶栏管理": "Header Management",
"控制顶栏模块显示状态,全局生效": "Control header module display status, global effect", "控制顶栏模块显示状态,全局生效": "Control header module display status, global effect",
"用户主页,展示系统信息": "User homepage, displaying system information", "用户主页,展示系统信息": "User homepage, displaying system information",
@@ -2058,7 +2068,7 @@
"需要登录访问": "Require Login", "需要登录访问": "Require Login",
"开启后未登录用户无法访问模型广场": "When enabled, unauthenticated users cannot access the model marketplace", "开启后未登录用户无法访问模型广场": "When enabled, unauthenticated users cannot access the model marketplace",
"参与官方同步": "Participate in official sync", "参与官方同步": "Participate in official sync",
"关闭后,此模型将不会被同步官方自动覆盖或创建": "When turned off, this model will be skipped by Sync official (no auto create/overwrite)", "关闭后,此模型将不会被\"同步官方\"自动覆盖或创建": "When turned off, this model will be skipped by Sync official (no auto create/overwrite)",
"同步": "Sync", "同步": "Sync",
"同步向导": "Sync Wizard", "同步向导": "Sync Wizard",
"选择方式": "Select method", "选择方式": "Select method",

View File

@@ -44,6 +44,9 @@ export default function ModelRatioSettings(props) {
ModelRatio: '', ModelRatio: '',
CacheRatio: '', CacheRatio: '',
CompletionRatio: '', CompletionRatio: '',
ImageRatio: '',
AudioRatio: '',
AudioCompletionRatio: '',
ExposeRatioEnabled: false, ExposeRatioEnabled: false,
}); });
const refForm = useRef(); const refForm = useRef();
@@ -219,6 +222,72 @@ export default function ModelRatioSettings(props) {
/> />
</Col> </Col>
</Row> </Row>
<Row gutter={16}>
<Col xs={24} sm={16}>
<Form.TextArea
label={t('图片倍率')}
extraText={t('图片输入相关的倍率设置,键为模型名称,值为倍率')}
placeholder={t('为一个 JSON 文本,键为模型名称,值为倍率,例如:{"gpt-image-1": 2}')}
field={'ImageRatio'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
stopValidateWithError
rules={[
{
validator: (rule, value) => verifyJSON(value),
message: '不是合法的 JSON 字符串',
},
]}
onChange={(value) =>
setInputs({ ...inputs, ImageRatio: value })
}
/>
</Col>
</Row>
<Row gutter={16}>
<Col xs={24} sm={16}>
<Form.TextArea
label={t('音频倍率')}
extraText={t('音频输入相关的倍率设置,键为模型名称,值为倍率')}
placeholder={t('为一个 JSON 文本,键为模型名称,值为倍率,例如:{"gpt-4o-audio-preview": 16}')}
field={'AudioRatio'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
stopValidateWithError
rules={[
{
validator: (rule, value) => verifyJSON(value),
message: '不是合法的 JSON 字符串',
},
]}
onChange={(value) =>
setInputs({ ...inputs, AudioRatio: value })
}
/>
</Col>
</Row>
<Row gutter={16}>
<Col xs={24} sm={16}>
<Form.TextArea
label={t('音频补全倍率')}
extraText={t('音频输出补全相关的倍率设置,键为模型名称,值为倍率')}
placeholder={t('为一个 JSON 文本,键为模型名称,值为倍率,例如:{"gpt-4o-realtime": 2}')}
field={'AudioCompletionRatio'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
stopValidateWithError
rules={[
{
validator: (rule, value) => verifyJSON(value),
message: '不是合法的 JSON 字符串',
},
]}
onChange={(value) =>
setInputs({ ...inputs, AudioCompletionRatio: value })
}
/>
</Col>
</Row>
<Row gutter={16}> <Row gutter={16}>
<Col span={16}> <Col span={16}>
<Form.Switch <Form.Switch