Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions controller/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,33 @@ func UpdateOption(c *gin.Context) {
})
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
}
Comment on lines +131 to +157

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

New options handled; align image-ratio cache invalidation with audio/model

Controller wiring is fine. Ensure UpdateImageRatioByJSONString also invalidates exposed cache (audio functions already do). See suggested fix in ratio_setting/model_ratio.go.

After applying the InvalidateExposedDataCache() fix, please verify the exposed ratios API reflects changes immediately after updating ImageRatio via this endpoint.


🏁 Script executed:

#!/usr/bin/env bash
# Locate model_ratio.go
file=$(fd model_ratio.go)
echo "Found file: $file"

# Inspect UpdateImageRatioByJSONString
echo "=== UpdateImageRatioByJSONString ==="
rg -nP 'func UpdateImageRatioByJSONString' -C15 "$file" || true

# Inspect UpdateAudioRatioByJSONString
echo "=== UpdateAudioRatioByJSONString ==="
rg -nP 'func UpdateAudioRatioByJSONString' -C15 "$file" || true

# Inspect UpdateAudioCompletionRatioByJSONString
echo "=== UpdateAudioCompletionRatioByJSONString ==="
rg -nP 'func UpdateAudioCompletionRatioByJSONString' -C15 "$file" || true

Length of output: 3706


Add cache invalidation to UpdateImageRatioByJSONString

UpdateImageRatioByJSONString in setting/ratio_setting/model_ratio.go (around line 673) currently unmarshals into imageRatioMap without calling InvalidateExposedDataCache(). Mirror the audio handlers by invoking InvalidateExposedDataCache() on successful unmarshal. After making this change, verify that the exposed ratios API reflects image-ratio updates immediately.

🤖 Prompt for AI Agents
In controller/option.go around lines 115-141, the ImageRatio branch updates via
ratio_setting.UpdateImageRatioByJSONString(option.Value) but the underlying
UpdateImageRatioByJSONString (setting/ratio_setting/model_ratio.go around line
673) does not call InvalidateExposedDataCache() after a successful unmarshal;
modify UpdateImageRatioByJSONString to mirror the audio handlers by invoking
InvalidateExposedDataCache() immediately after the unmarshalling/update succeeds
so the exposed ratios API reflects changes immediately, then run a quick test to
confirm the exposed ratios endpoint shows the updated image ratios.

case "ModelRequestRateLimitGroup":
err = setting.CheckModelRequestRateLimitGroup(option.Value.(string))
if err != nil {
Expand Down
9 changes: 9 additions & 0 deletions model/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ func InitOptionMap() {
common.OptionMap["GroupGroupRatio"] = ratio_setting.GroupGroupRatio2JSONString()
common.OptionMap["UserUsableGroups"] = setting.UserUsableGroups2JSONString()
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["ChatLink"] = common.ChatLink
//common.OptionMap["ChatLink2"] = common.ChatLink2
Expand Down Expand Up @@ -397,6 +400,12 @@ func updateOptionMap(key string, value string) (err error) {
err = ratio_setting.UpdateModelPriceByJSONString(value)
case "CacheRatio":
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)
Comment on lines +403 to +408

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Stale cache risk: ImageRatio update lacks exposed-data invalidation

AudioRatio/AudioCompletionRatio updaters call InvalidateExposedDataCache() on success (see setting/ratio_setting/model_ratio.go), but UpdateImageRatioByJSONString does not. Exposed ratio endpoints/UI may serve stale ImageRatio after updates.

Proposed fix (in setting/ratio_setting/model_ratio.go):

func UpdateImageRatioByJSONString(jsonStr string) error {
	imageRatioMapMutex.Lock()
	defer imageRatioMapMutex.Unlock()
	imageRatioMap = make(map[string]float64)
	err := common.Unmarshal([]byte(jsonStr), &imageRatioMap)
	if err == nil {
		InvalidateExposedDataCache()
	}
	return err
}

Verify current behavior:


🏁 Script executed:

#!/bin/bash
# Show the ImageRatio updater and check for cache invalidation usage
rg -nP -C3 'func\s+UpdateImageRatioByJSONString\(' --type=go
rg -n 'InvalidateExposedDataCache\(' setting/ratio_setting --type=go

Length of output: 1121


Add cache invalidation to UpdateImageRatioByJSONString
UpdateImageRatioByJSONString (setting/ratio_setting/model_ratio.go) never calls InvalidateExposedDataCache() after unmarshalling, so the exposed ImageRatio remains stale. Add an InvalidateExposedDataCache() call inside the if err == nil block, matching the pattern used by AudioRatio and AudioCompletionRatio updaters.

🤖 Prompt for AI Agents
In model/option.go around lines 402 to 407, the ImageRatio updater does not
invalidate the exposed data cache after a successful JSON update; modify the
UpdateImageRatioByJSONString call path (in setting/ratio_setting/model_ratio.go)
to invoke InvalidateExposedDataCache() inside the `if err == nil` block
immediately after successful unmarshalling/update, following the same pattern
used by AudioRatio and AudioCompletionRatio updaters so the exposed ImageRatio
is refreshed.

case "TopUpLink":
common.TopUpLink = value
//case "ChatLink":
Expand Down
6 changes: 6 additions & 0 deletions relay/helper/price.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
var cacheRatio float64
var imageRatio float64
var cacheCreationRatio float64
var audioRatio float64
var audioCompletionRatio float64
if !usePrice {
preConsumedTokens := common.Max(promptTokens, common.PreConsumedQuota)
if meta.MaxTokens != 0 {
Expand All @@ -73,6 +75,8 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
cacheRatio, _ = ratio_setting.GetCacheRatio(info.OriginModelName)
cacheCreationRatio, _ = ratio_setting.GetCreateCacheRatio(info.OriginModelName)
imageRatio, _ = ratio_setting.GetImageRatio(info.OriginModelName)
audioRatio = ratio_setting.GetAudioRatio(info.OriginModelName)
audioCompletionRatio = ratio_setting.GetAudioCompletionRatio(info.OriginModelName)
ratio := modelRatio * groupRatioInfo.GroupRatio
preConsumedQuota = int(float64(preConsumedTokens) * ratio)
} else {
Expand All @@ -90,6 +94,8 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
UsePrice: usePrice,
CacheRatio: cacheRatio,
ImageRatio: imageRatio,
AudioRatio: audioRatio,
AudioCompletionRatio: audioCompletionRatio,
CacheCreationRatio: cacheCreationRatio,
ShouldPreConsumedQuota: preConsumedQuota,
}
Expand Down
138 changes: 117 additions & 21 deletions setting/ratio_setting/model_ratio.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,18 @@ var defaultModelPrice = map[string]float64{
"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,
}
Comment on lines +281 to +286

@coderabbitai coderabbitai Bot Aug 30, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Mismatched model IDs between audio ratio and audio completion ratio

defaultAudioRatio uses "-realtime-preview" keys; defaultAudioCompletionRatio uses "-realtime" keys. Given incoming names are often "-realtime-preview[-DATE]", completion lookups will miss and fall back to 2. Align keys.

 var defaultAudioCompletionRatio = map[string]float64{
-   "gpt-4o-realtime":      2,
-   "gpt-4o-mini-realtime": 2,
+   "gpt-4o-realtime-preview":      2,
+   "gpt-4o-mini-realtime-preview": 2,
 }

Also applies to: 288-291

🤖 Prompt for AI Agents
In setting/ratio_setting/model_ratio.go around lines 281-286 (and similarly
lines 288-291), the map keys use "-realtime-preview" while the audio completion
ratio map uses "-realtime", causing lookups to miss when incoming model names
are "-realtime-preview[-DATE]"; update the keys so both maps use the same
canonical form (prefer matching incoming names, e.g., use "-realtime-preview" in
both maps) or normalize incoming model IDs before lookup (strip or map
"-realtime-preview" ↔ "-realtime") so lookups succeed and do not fall back to
the default 2.

✅ Addressed in commit d15718a


var defaultAudioCompletionRatio = map[string]float64{
"gpt-4o-realtime": 2,
"gpt-4o-mini-realtime": 2,
}

var (
modelPriceMap map[string]float64 = nil
modelPriceMapMutex = sync.RWMutex{}
Expand Down Expand Up @@ -326,6 +338,15 @@ func InitRatioSettings() {
imageRatioMap = defaultImageRatio
imageRatioMapMutex.Unlock()

// initialize audioRatioMap
audioRatioMapMutex.Lock()
audioRatioMap = defaultAudioRatio
audioRatioMapMutex.Unlock()

// initialize audioCompletionRatioMap
audioCompletionRatioMapMutex.Lock()
audioCompletionRatioMap = defaultAudioCompletionRatio
audioCompletionRatioMapMutex.Unlock()
}

func GetModelPriceMap() map[string]float64 {
Expand Down Expand Up @@ -417,6 +438,18 @@ func GetDefaultModelRatioMap() map[string]float64 {
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 {
CompletionRatioMutex.RLock()
defer CompletionRatioMutex.RUnlock()
Expand Down Expand Up @@ -584,32 +617,22 @@ func getHardcodedCompletionModelRatio(name string) (float64, bool) {
}

func GetAudioRatio(name string) float64 {
if strings.Contains(name, "-realtime") {
if strings.HasSuffix(name, "gpt-4o-realtime-preview") {
return 8
} else if strings.Contains(name, "gpt-4o-mini-realtime-preview") {
return 10 / 0.6
} 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
}
audioRatioMapMutex.RLock()
defer audioRatioMapMutex.RUnlock()
name = FormatMatchingModelName(name)
if ratio, ok := audioRatioMap[name]; ok {
return ratio
}
return 20
}

func GetAudioCompletionRatio(name string) float64 {
if strings.HasPrefix(name, "gpt-4o-realtime") {
return 2
} else if strings.HasPrefix(name, "gpt-4o-mini-realtime") {
return 2
audioCompletionRatioMapMutex.RLock()
defer audioCompletionRatioMapMutex.RUnlock()
name = FormatMatchingModelName(name)
if ratio, ok := audioCompletionRatioMap[name]; ok {

return ratio
}
return 2
}
Expand All @@ -630,6 +653,14 @@ var defaultImageRatio = map[string]float64{
}
var imageRatioMap map[string]float64
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 {
imageRatioMapMutex.RLock()
Expand Down Expand Up @@ -658,6 +689,71 @@ func GetImageRatio(name string) (float64, bool) {
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 {
modelRatioMapMutex.RLock()
defer modelRatioMapMutex.RUnlock()
Expand Down
4 changes: 3 additions & 1 deletion types/price_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ type PriceData struct {
CacheRatio float64
CacheCreationRatio float64
ImageRatio float64
AudioRatio float64
AudioCompletionRatio float64
UsePrice bool
ShouldPreConsumedQuota int
GroupRatioInfo GroupRatioInfo
Expand All @@ -27,5 +29,5 @@ type PerCallPriceData struct {
}

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)
}
8 changes: 7 additions & 1 deletion web/src/components/settings/RatioSetting.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ const RatioSetting = () => {
CompletionRatio: '',
GroupRatio: '',
GroupGroupRatio: '',
ImageRatio: '',
AudioRatio: '',
AudioCompletionRatio: '',
AutoGroups: '',
DefaultUseAutoGroup: false,
ExposeRatioEnabled: false,
Expand All @@ -61,7 +64,10 @@ const RatioSetting = () => {
item.key === 'UserUsableGroups' ||
item.key === 'CompletionRatio' ||
item.key === 'ModelPrice' ||
item.key === 'CacheRatio'
item.key === 'CacheRatio' ||
item.key === 'ImageRatio' ||
item.key === 'AudioRatio' ||
item.key === 'AudioCompletionRatio'
) {
try {
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
Expand Down
12 changes: 11 additions & 1 deletion web/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1999,6 +1999,16 @@
"查看渠道密钥": "View channel key",
"渠道密钥信息": "Channel key information",
"密钥获取成功": "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",
"控制顶栏模块显示状态,全局生效": "Control header module display status, global effect",
"用户主页,展示系统信息": "User homepage, displaying system information",
Expand Down Expand Up @@ -2058,7 +2068,7 @@
"需要登录访问": "Require Login",
"开启后未登录用户无法访问模型广场": "When enabled, unauthenticated users cannot access the model marketplace",
"参与官方同步": "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 Wizard",
"选择方式": "Select method",
Expand Down
Loading