mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 15:46:44 +00:00
Compare commits
3 Commits
v0.11.4
...
gemini-3-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
572ee2b919 | ||
|
|
2ae56ad842 | ||
|
|
6a3f8b1005 |
@@ -146,6 +146,15 @@ func UpdateOption(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
case "ImageOutputRatio":
|
||||
err = ratio_setting.UpdateImageOutputRatioByJSONString(option.Value.(string))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "图片输出倍率设置失败: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
case "AudioRatio":
|
||||
err = ratio_setting.UpdateAudioRatioByJSONString(option.Value.(string))
|
||||
if err != nil {
|
||||
|
||||
@@ -367,14 +367,15 @@ type GeminiChatResponse struct {
|
||||
}
|
||||
|
||||
type GeminiUsageMetadata struct {
|
||||
PromptTokenCount int `json:"promptTokenCount"`
|
||||
CandidatesTokenCount int `json:"candidatesTokenCount"`
|
||||
TotalTokenCount int `json:"totalTokenCount"`
|
||||
ThoughtsTokenCount int `json:"thoughtsTokenCount"`
|
||||
PromptTokensDetails []GeminiPromptTokensDetails `json:"promptTokensDetails"`
|
||||
PromptTokenCount int `json:"promptTokenCount"`
|
||||
CandidatesTokenCount int `json:"candidatesTokenCount"`
|
||||
TotalTokenCount int `json:"totalTokenCount"`
|
||||
ThoughtsTokenCount int `json:"thoughtsTokenCount"`
|
||||
PromptTokensDetails []GeminiTokensDetails `json:"promptTokensDetails"`
|
||||
CandidatesTokensDetails []GeminiTokensDetails `json:"candidatesTokensDetails"`
|
||||
}
|
||||
|
||||
type GeminiPromptTokensDetails struct {
|
||||
type GeminiTokensDetails struct {
|
||||
Modality string `json:"modality"`
|
||||
TokenCount int `json:"tokenCount"`
|
||||
}
|
||||
|
||||
@@ -259,6 +259,7 @@ type InputTokenDetails struct {
|
||||
|
||||
type OutputTokenDetails struct {
|
||||
TextTokens int `json:"text_tokens"`
|
||||
ImageTokens int `json:"image_tokens"`
|
||||
AudioTokens int `json:"audio_tokens"`
|
||||
ReasoningTokens int `json:"reasoning_tokens"`
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ func InitOptionMap() {
|
||||
common.OptionMap["UserUsableGroups"] = setting.UserUsableGroups2JSONString()
|
||||
common.OptionMap["CompletionRatio"] = ratio_setting.CompletionRatio2JSONString()
|
||||
common.OptionMap["ImageRatio"] = ratio_setting.ImageRatio2JSONString()
|
||||
common.OptionMap["ImageOutputRatio"] = ratio_setting.ImageOutputRatio2JSONString()
|
||||
common.OptionMap["AudioRatio"] = ratio_setting.AudioRatio2JSONString()
|
||||
common.OptionMap["AudioCompletionRatio"] = ratio_setting.AudioCompletionRatio2JSONString()
|
||||
common.OptionMap["TopUpLink"] = common.TopUpLink
|
||||
@@ -426,6 +427,8 @@ func updateOptionMap(key string, value string) (err error) {
|
||||
err = ratio_setting.UpdateCacheRatioByJSONString(value)
|
||||
case "ImageRatio":
|
||||
err = ratio_setting.UpdateImageRatioByJSONString(value)
|
||||
case "ImageOutputRatio":
|
||||
err = ratio_setting.UpdateImageOutputRatioByJSONString(value)
|
||||
case "AudioRatio":
|
||||
err = ratio_setting.UpdateAudioRatioByJSONString(value)
|
||||
case "AudioCompletionRatio":
|
||||
|
||||
@@ -1067,9 +1067,34 @@ func handleFinalStream(c *gin.Context, info *relaycommon.RelayInfo, resp *dto.Ch
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyGeminiTokensDetailsToPromptUsage(usage *dto.Usage, details []dto.GeminiTokensDetails) {
|
||||
for _, detail := range details {
|
||||
switch detail.Modality {
|
||||
case "AUDIO":
|
||||
usage.PromptTokensDetails.AudioTokens = detail.TokenCount
|
||||
case "TEXT":
|
||||
usage.PromptTokensDetails.TextTokens = detail.TokenCount
|
||||
case "IMAGE":
|
||||
usage.PromptTokensDetails.ImageTokens = detail.TokenCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applyGeminiTokensDetailsToCompletionUsage(usage *dto.Usage, details []dto.GeminiTokensDetails) {
|
||||
for _, detail := range details {
|
||||
switch detail.Modality {
|
||||
case "AUDIO":
|
||||
usage.CompletionTokenDetails.AudioTokens = detail.TokenCount
|
||||
case "TEXT":
|
||||
usage.CompletionTokenDetails.TextTokens = detail.TokenCount
|
||||
case "IMAGE":
|
||||
usage.CompletionTokenDetails.ImageTokens = detail.TokenCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func geminiStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response, callback func(data string, geminiResponse *dto.GeminiChatResponse) bool) (*dto.Usage, *types.NewAPIError) {
|
||||
var usage = &dto.Usage{}
|
||||
var imageCount int
|
||||
responseText := strings.Builder{}
|
||||
|
||||
helper.StreamScannerHandler(c, resp, info, func(data string) bool {
|
||||
@@ -1080,12 +1105,8 @@ func geminiStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http
|
||||
return false
|
||||
}
|
||||
|
||||
// 统计图片数量
|
||||
for _, candidate := range geminiResponse.Candidates {
|
||||
for _, part := range candidate.Content.Parts {
|
||||
if part.InlineData != nil && part.InlineData.MimeType != "" {
|
||||
imageCount++
|
||||
}
|
||||
if part.Text != "" {
|
||||
responseText.WriteString(part.Text)
|
||||
}
|
||||
@@ -1093,31 +1114,30 @@ func geminiStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http
|
||||
}
|
||||
|
||||
// 更新使用量统计
|
||||
if geminiResponse.UsageMetadata.TotalTokenCount != 0 {
|
||||
if geminiResponse.UsageMetadata.TotalTokenCount != 0 ||
|
||||
geminiResponse.UsageMetadata.PromptTokenCount != 0 ||
|
||||
geminiResponse.UsageMetadata.CandidatesTokenCount != 0 ||
|
||||
geminiResponse.UsageMetadata.ThoughtsTokenCount != 0 ||
|
||||
len(geminiResponse.UsageMetadata.PromptTokensDetails) > 0 ||
|
||||
len(geminiResponse.UsageMetadata.CandidatesTokensDetails) > 0 {
|
||||
usage.PromptTokens = geminiResponse.UsageMetadata.PromptTokenCount
|
||||
usage.CompletionTokens = geminiResponse.UsageMetadata.CandidatesTokenCount + geminiResponse.UsageMetadata.ThoughtsTokenCount
|
||||
usage.CompletionTokenDetails.ReasoningTokens = geminiResponse.UsageMetadata.ThoughtsTokenCount
|
||||
usage.TotalTokens = geminiResponse.UsageMetadata.TotalTokenCount
|
||||
for _, detail := range geminiResponse.UsageMetadata.PromptTokensDetails {
|
||||
if detail.Modality == "AUDIO" {
|
||||
usage.PromptTokensDetails.AudioTokens = detail.TokenCount
|
||||
} else if detail.Modality == "TEXT" {
|
||||
usage.PromptTokensDetails.TextTokens = detail.TokenCount
|
||||
}
|
||||
if geminiResponse.UsageMetadata.TotalTokenCount != 0 {
|
||||
usage.TotalTokens = geminiResponse.UsageMetadata.TotalTokenCount
|
||||
usage.CompletionTokens = usage.TotalTokens - usage.PromptTokens
|
||||
} else {
|
||||
usage.CompletionTokens = geminiResponse.UsageMetadata.CandidatesTokenCount + geminiResponse.UsageMetadata.ThoughtsTokenCount
|
||||
usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens
|
||||
}
|
||||
usage.CompletionTokenDetails.ReasoningTokens = geminiResponse.UsageMetadata.ThoughtsTokenCount
|
||||
applyGeminiTokensDetailsToPromptUsage(usage, geminiResponse.UsageMetadata.PromptTokensDetails)
|
||||
applyGeminiTokensDetailsToCompletionUsage(usage, geminiResponse.UsageMetadata.CandidatesTokensDetails)
|
||||
}
|
||||
|
||||
return callback(data, &geminiResponse)
|
||||
})
|
||||
|
||||
if imageCount != 0 {
|
||||
if usage.CompletionTokens == 0 {
|
||||
usage.CompletionTokens = imageCount * 1400
|
||||
}
|
||||
}
|
||||
|
||||
usage.PromptTokensDetails.TextTokens = usage.PromptTokens
|
||||
if usage.TotalTokens > 0 {
|
||||
if usage.TotalTokens > 0 && usage.PromptTokens > 0 {
|
||||
usage.CompletionTokens = usage.TotalTokens - usage.PromptTokens
|
||||
}
|
||||
|
||||
@@ -1223,23 +1243,23 @@ func GeminiChatHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.R
|
||||
}
|
||||
fullTextResponse := responseGeminiChat2OpenAI(c, &geminiResponse)
|
||||
fullTextResponse.Model = info.UpstreamModelName
|
||||
|
||||
usage := dto.Usage{
|
||||
PromptTokens: geminiResponse.UsageMetadata.PromptTokenCount,
|
||||
CompletionTokens: geminiResponse.UsageMetadata.CandidatesTokenCount,
|
||||
TotalTokens: geminiResponse.UsageMetadata.TotalTokenCount,
|
||||
PromptTokens: geminiResponse.UsageMetadata.PromptTokenCount,
|
||||
TotalTokens: geminiResponse.UsageMetadata.TotalTokenCount,
|
||||
}
|
||||
|
||||
usage.CompletionTokenDetails.ReasoningTokens = geminiResponse.UsageMetadata.ThoughtsTokenCount
|
||||
usage.CompletionTokens = usage.TotalTokens - usage.PromptTokens
|
||||
|
||||
for _, detail := range geminiResponse.UsageMetadata.PromptTokensDetails {
|
||||
if detail.Modality == "AUDIO" {
|
||||
usage.PromptTokensDetails.AudioTokens = detail.TokenCount
|
||||
} else if detail.Modality == "TEXT" {
|
||||
usage.PromptTokensDetails.TextTokens = detail.TokenCount
|
||||
}
|
||||
if usage.TotalTokens > 0 {
|
||||
usage.CompletionTokens = usage.TotalTokens - usage.PromptTokens
|
||||
} else {
|
||||
usage.CompletionTokens = geminiResponse.UsageMetadata.CandidatesTokenCount + geminiResponse.UsageMetadata.ThoughtsTokenCount
|
||||
usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens
|
||||
}
|
||||
|
||||
applyGeminiTokensDetailsToPromptUsage(&usage, geminiResponse.UsageMetadata.PromptTokensDetails)
|
||||
applyGeminiTokensDetailsToCompletionUsage(&usage, geminiResponse.UsageMetadata.CandidatesTokensDetails)
|
||||
|
||||
fullTextResponse.Usage = usage
|
||||
|
||||
switch info.RelayFormat {
|
||||
|
||||
@@ -12,8 +12,10 @@ import (
|
||||
"github.com/QuantumNous/new-api/logger"
|
||||
"github.com/QuantumNous/new-api/relay/channel/openrouter"
|
||||
relaycommon "github.com/QuantumNous/new-api/relay/common"
|
||||
relayconstant "github.com/QuantumNous/new-api/relay/constant"
|
||||
"github.com/QuantumNous/new-api/relay/helper"
|
||||
"github.com/QuantumNous/new-api/service"
|
||||
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
||||
|
||||
"github.com/QuantumNous/new-api/types"
|
||||
|
||||
@@ -582,6 +584,11 @@ func OpenaiHandlerWithUsage(c *gin.Context, info *relaycommon.RelayInfo, resp *h
|
||||
usageResp.PromptTokensDetails.ImageTokens += usageResp.InputTokensDetails.ImageTokens
|
||||
usageResp.PromptTokensDetails.TextTokens += usageResp.InputTokensDetails.TextTokens
|
||||
}
|
||||
if (info.RelayMode == relayconstant.RelayModeImagesGenerations || info.RelayMode == relayconstant.RelayModeImagesEdits) && usageResp.OutputTokens > 0 {
|
||||
if _, ok := ratio_setting.GetImageOutputRatio(info.OriginModelName); ok {
|
||||
usageResp.CompletionTokenDetails.ImageTokens += usageResp.OutputTokens
|
||||
}
|
||||
}
|
||||
applyUsagePostProcessing(info, &usageResp.Usage, responseBody)
|
||||
return &usageResp.Usage, nil
|
||||
}
|
||||
|
||||
@@ -203,6 +203,7 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage
|
||||
cacheTokens := usage.PromptTokensDetails.CachedTokens
|
||||
imageTokens := usage.PromptTokensDetails.ImageTokens
|
||||
audioTokens := usage.PromptTokensDetails.AudioTokens
|
||||
completionImageTokens := usage.CompletionTokenDetails.ImageTokens
|
||||
completionTokens := usage.CompletionTokens
|
||||
cachedCreationTokens := usage.PromptTokensDetails.CachedCreationTokens
|
||||
|
||||
@@ -212,6 +213,7 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage
|
||||
completionRatio := relayInfo.PriceData.CompletionRatio
|
||||
cacheRatio := relayInfo.PriceData.CacheRatio
|
||||
imageRatio := relayInfo.PriceData.ImageRatio
|
||||
imageOutputRatio := relayInfo.PriceData.ImageOutputRatio
|
||||
modelRatio := relayInfo.PriceData.ModelRatio
|
||||
groupRatio := relayInfo.PriceData.GroupRatioInfo.GroupRatio
|
||||
modelPrice := relayInfo.PriceData.ModelPrice
|
||||
@@ -223,10 +225,12 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage
|
||||
dImageTokens := decimal.NewFromInt(int64(imageTokens))
|
||||
dAudioTokens := decimal.NewFromInt(int64(audioTokens))
|
||||
dCompletionTokens := decimal.NewFromInt(int64(completionTokens))
|
||||
dCompletionImageTokens := decimal.NewFromInt(int64(completionImageTokens))
|
||||
dCachedCreationTokens := decimal.NewFromInt(int64(cachedCreationTokens))
|
||||
dCompletionRatio := decimal.NewFromFloat(completionRatio)
|
||||
dCacheRatio := decimal.NewFromFloat(cacheRatio)
|
||||
dImageRatio := decimal.NewFromFloat(imageRatio)
|
||||
dImageOutputRatio := decimal.NewFromFloat(imageOutputRatio)
|
||||
dModelRatio := decimal.NewFromFloat(modelRatio)
|
||||
dGroupRatio := decimal.NewFromFloat(groupRatio)
|
||||
dModelPrice := decimal.NewFromFloat(modelPrice)
|
||||
@@ -338,7 +342,14 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage
|
||||
Add(imageTokensWithRatio).
|
||||
Add(dCachedCreationTokensWithRatio)
|
||||
|
||||
completionQuota := dCompletionTokens.Mul(dCompletionRatio)
|
||||
baseCompletionTokens := dCompletionTokens
|
||||
var completionImageTokensWithRatio decimal.Decimal
|
||||
if !dCompletionImageTokens.IsZero() {
|
||||
baseCompletionTokens = baseCompletionTokens.Sub(dCompletionImageTokens)
|
||||
completionImageTokensWithRatio = dCompletionImageTokens.Mul(dImageOutputRatio)
|
||||
}
|
||||
|
||||
completionQuota := baseCompletionTokens.Mul(dCompletionRatio).Add(completionImageTokensWithRatio)
|
||||
|
||||
quotaCalculateDecimal = promptQuota.Add(completionQuota).Mul(ratio)
|
||||
|
||||
@@ -424,7 +435,11 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage
|
||||
if imageTokens != 0 {
|
||||
other["image"] = true
|
||||
other["image_ratio"] = imageRatio
|
||||
other["image_output"] = imageTokens
|
||||
other["image_input_tokens"] = imageTokens
|
||||
}
|
||||
if completionImageTokens != 0 {
|
||||
other["completion_image_tokens"] = completionImageTokens
|
||||
other["image_output_ratio"] = imageOutputRatio
|
||||
}
|
||||
if cachedCreationTokens != 0 {
|
||||
other["cache_creation_tokens"] = cachedCreationTokens
|
||||
|
||||
@@ -60,6 +60,7 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
|
||||
var cacheCreationRatio1h float64
|
||||
var audioRatio float64
|
||||
var audioCompletionRatio float64
|
||||
var imageOutputRatio float64
|
||||
var freeModel bool
|
||||
if !usePrice {
|
||||
preConsumedTokens := common.Max(promptTokens, common.PreConsumedQuota)
|
||||
@@ -85,6 +86,7 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
|
||||
// 固定1h和5min缓存写入价格的比例
|
||||
cacheCreationRatio1h = cacheCreationRatio * claudeCacheCreation1hMultiplier
|
||||
imageRatio, _ = ratio_setting.GetImageRatio(info.OriginModelName)
|
||||
imageOutputRatio, _ = ratio_setting.GetImageOutputRatio(info.OriginModelName)
|
||||
audioRatio = ratio_setting.GetAudioRatio(info.OriginModelName)
|
||||
audioCompletionRatio = ratio_setting.GetAudioCompletionRatio(info.OriginModelName)
|
||||
ratio := modelRatio * groupRatioInfo.GroupRatio
|
||||
@@ -124,6 +126,7 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
|
||||
UsePrice: usePrice,
|
||||
CacheRatio: cacheRatio,
|
||||
ImageRatio: imageRatio,
|
||||
ImageOutputRatio: imageOutputRatio,
|
||||
AudioRatio: audioRatio,
|
||||
AudioCompletionRatio: audioCompletionRatio,
|
||||
CacheCreationRatio: cacheCreationRatio,
|
||||
|
||||
@@ -42,10 +42,14 @@ func GetExposedData() gin.H {
|
||||
return cloneGinH(c.data)
|
||||
}
|
||||
newData := gin.H{
|
||||
"model_ratio": GetModelRatioCopy(),
|
||||
"completion_ratio": GetCompletionRatioCopy(),
|
||||
"cache_ratio": GetCacheRatioCopy(),
|
||||
"model_price": GetModelPriceCopy(),
|
||||
"model_ratio": GetModelRatioCopy(),
|
||||
"completion_ratio": GetCompletionRatioCopy(),
|
||||
"cache_ratio": GetCacheRatioCopy(),
|
||||
"model_price": GetModelPriceCopy(),
|
||||
"image_ratio": GetImageRatioCopy(),
|
||||
"image_output_ratio": GetImageOutputRatioCopy(),
|
||||
"audio_ratio": GetAudioRatioCopy(),
|
||||
"audio_completion_ratio": GetAudioCompletionRatioCopy(),
|
||||
}
|
||||
exposedData.Store(&exposedCache{
|
||||
data: newData,
|
||||
|
||||
@@ -361,6 +361,11 @@ func InitRatioSettings() {
|
||||
imageRatioMap = defaultImageRatio
|
||||
imageRatioMapMutex.Unlock()
|
||||
|
||||
// initialize imageOutputRatioMap
|
||||
imageOutputRatioMapMutex.Lock()
|
||||
imageOutputRatioMap = defaultImageOutputRatio
|
||||
imageOutputRatioMapMutex.Unlock()
|
||||
|
||||
// initialize audioRatioMap
|
||||
audioRatioMapMutex.Lock()
|
||||
audioRatioMap = defaultAudioRatio
|
||||
@@ -686,6 +691,10 @@ var defaultImageRatio = map[string]float64{
|
||||
}
|
||||
var imageRatioMap map[string]float64
|
||||
var imageRatioMapMutex sync.RWMutex
|
||||
|
||||
var defaultImageOutputRatio = map[string]float64{}
|
||||
var imageOutputRatioMap map[string]float64
|
||||
var imageOutputRatioMapMutex sync.RWMutex
|
||||
var (
|
||||
audioRatioMap map[string]float64 = nil
|
||||
audioRatioMapMutex = sync.RWMutex{}
|
||||
@@ -709,7 +718,11 @@ func UpdateImageRatioByJSONString(jsonStr string) error {
|
||||
imageRatioMapMutex.Lock()
|
||||
defer imageRatioMapMutex.Unlock()
|
||||
imageRatioMap = make(map[string]float64)
|
||||
return common.Unmarshal([]byte(jsonStr), &imageRatioMap)
|
||||
err := common.Unmarshal([]byte(jsonStr), &imageRatioMap)
|
||||
if err == nil {
|
||||
InvalidateExposedDataCache()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func GetImageRatio(name string) (float64, bool) {
|
||||
@@ -722,6 +735,37 @@ func GetImageRatio(name string) (float64, bool) {
|
||||
return ratio, true
|
||||
}
|
||||
|
||||
func ImageOutputRatio2JSONString() string {
|
||||
imageOutputRatioMapMutex.RLock()
|
||||
defer imageOutputRatioMapMutex.RUnlock()
|
||||
jsonBytes, err := common.Marshal(imageOutputRatioMap)
|
||||
if err != nil {
|
||||
common.SysError("error marshalling image output ratio: " + err.Error())
|
||||
}
|
||||
return string(jsonBytes)
|
||||
}
|
||||
|
||||
func UpdateImageOutputRatioByJSONString(jsonStr string) error {
|
||||
imageOutputRatioMapMutex.Lock()
|
||||
defer imageOutputRatioMapMutex.Unlock()
|
||||
imageOutputRatioMap = make(map[string]float64)
|
||||
err := common.Unmarshal([]byte(jsonStr), &imageOutputRatioMap)
|
||||
if err == nil {
|
||||
InvalidateExposedDataCache()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func GetImageOutputRatio(name string) (float64, bool) {
|
||||
imageOutputRatioMapMutex.RLock()
|
||||
defer imageOutputRatioMapMutex.RUnlock()
|
||||
ratio, ok := imageOutputRatioMap[name]
|
||||
if !ok {
|
||||
return 1, false
|
||||
}
|
||||
return ratio, true
|
||||
}
|
||||
|
||||
func AudioRatio2JSONString() string {
|
||||
audioRatioMapMutex.RLock()
|
||||
defer audioRatioMapMutex.RUnlock()
|
||||
@@ -787,6 +831,26 @@ func GetAudioCompletionRatioCopy() map[string]float64 {
|
||||
return copyMap
|
||||
}
|
||||
|
||||
func GetImageRatioCopy() map[string]float64 {
|
||||
imageRatioMapMutex.RLock()
|
||||
defer imageRatioMapMutex.RUnlock()
|
||||
copyMap := make(map[string]float64, len(imageRatioMap))
|
||||
for k, v := range imageRatioMap {
|
||||
copyMap[k] = v
|
||||
}
|
||||
return copyMap
|
||||
}
|
||||
|
||||
func GetImageOutputRatioCopy() map[string]float64 {
|
||||
imageOutputRatioMapMutex.RLock()
|
||||
defer imageOutputRatioMapMutex.RUnlock()
|
||||
copyMap := make(map[string]float64, len(imageOutputRatioMap))
|
||||
for k, v := range imageOutputRatioMap {
|
||||
copyMap[k] = v
|
||||
}
|
||||
return copyMap
|
||||
}
|
||||
|
||||
func GetModelRatioCopy() map[string]float64 {
|
||||
modelRatioMapMutex.RLock()
|
||||
defer modelRatioMapMutex.RUnlock()
|
||||
|
||||
@@ -18,6 +18,7 @@ type PriceData struct {
|
||||
CacheCreation5mRatio float64
|
||||
CacheCreation1hRatio float64
|
||||
ImageRatio float64
|
||||
ImageOutputRatio float64
|
||||
AudioRatio float64
|
||||
AudioCompletionRatio float64
|
||||
OtherRatios map[string]float64
|
||||
|
||||
@@ -40,6 +40,7 @@ const RatioSetting = () => {
|
||||
GroupRatio: '',
|
||||
GroupGroupRatio: '',
|
||||
ImageRatio: '',
|
||||
ImageOutputRatio: '',
|
||||
AudioRatio: '',
|
||||
AudioCompletionRatio: '',
|
||||
AutoGroups: '',
|
||||
|
||||
@@ -1174,7 +1174,9 @@ export function renderModelPrice(
|
||||
cacheRatio = 1.0,
|
||||
image = false,
|
||||
imageRatio = 1.0,
|
||||
imageOutputTokens = 0,
|
||||
imageInputTokens = 0,
|
||||
completionImageTokens = 0,
|
||||
imageOutputRatio = 1.0,
|
||||
webSearch = false,
|
||||
webSearchCallCount = 0,
|
||||
webSearchPrice = 0,
|
||||
@@ -1217,22 +1219,31 @@ export function renderModelPrice(
|
||||
let completionRatioPrice = modelRatio * 2.0 * completionRatio;
|
||||
let cacheRatioPrice = modelRatio * 2.0 * cacheRatio;
|
||||
let imageRatioPrice = modelRatio * 2.0 * imageRatio;
|
||||
let imageOutputRatioPrice = modelRatio * 2.0 * imageOutputRatio;
|
||||
|
||||
// Calculate effective input tokens (non-cached + cached with ratio applied)
|
||||
let effectiveInputTokens =
|
||||
inputTokens - cacheTokens + cacheTokens * cacheRatio;
|
||||
// Handle image tokens if present
|
||||
if (image && imageOutputTokens > 0) {
|
||||
if (image && imageInputTokens > 0) {
|
||||
effectiveInputTokens =
|
||||
inputTokens - imageOutputTokens + imageOutputTokens * imageRatio;
|
||||
inputTokens - imageInputTokens + imageInputTokens * imageRatio;
|
||||
}
|
||||
if (audioInputTokens > 0) {
|
||||
effectiveInputTokens -= audioInputTokens;
|
||||
}
|
||||
|
||||
const baseCompletionTokens = Math.max(
|
||||
completionTokens - completionImageTokens,
|
||||
0,
|
||||
);
|
||||
const shouldSplitOutput =
|
||||
completionImageTokens > 0 && imageOutputRatio !== completionRatio;
|
||||
let price =
|
||||
(effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
|
||||
(audioInputTokens / 1000000) * audioInputPrice * groupRatio +
|
||||
(completionTokens / 1000000) * completionRatioPrice * groupRatio +
|
||||
(baseCompletionTokens / 1000000) * completionRatioPrice * groupRatio +
|
||||
(completionImageTokens / 1000000) * imageOutputRatioPrice * groupRatio +
|
||||
(webSearchCallCount / 1000) * webSearchPrice * groupRatio +
|
||||
(fileSearchCallCount / 1000) * fileSearchPrice * groupRatio +
|
||||
imageGenerationCallPrice * groupRatio;
|
||||
@@ -1252,17 +1263,44 @@ export function renderModelPrice(
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{i18next.t(
|
||||
'输出价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})',
|
||||
{
|
||||
symbol: symbol,
|
||||
price: (inputRatioPrice * rate).toFixed(6),
|
||||
total: (completionRatioPrice * rate).toFixed(6),
|
||||
completionRatio: completionRatio,
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
{shouldSplitOutput ? (
|
||||
<>
|
||||
<p>
|
||||
{i18next.t(
|
||||
'文字输出价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})',
|
||||
{
|
||||
symbol: symbol,
|
||||
price: (inputRatioPrice * rate).toFixed(6),
|
||||
total: (completionRatioPrice * rate).toFixed(6),
|
||||
completionRatio: completionRatio,
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{i18next.t(
|
||||
'图片输出价格:{{symbol}}{{price}} * {{imageOutputRatio}} = {{symbol}}{{total}} / 1M tokens (图片输出倍率: {{imageOutputRatio}})',
|
||||
{
|
||||
symbol: symbol,
|
||||
price: (inputRatioPrice * rate).toFixed(6),
|
||||
total: (imageOutputRatioPrice * rate).toFixed(6),
|
||||
imageOutputRatio: imageOutputRatio,
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p>
|
||||
{i18next.t(
|
||||
'输出价格:{{symbol}}{{price}} * {{completionRatio}} = {{symbol}}{{total}} / 1M tokens (补全倍率: {{completionRatio}})',
|
||||
{
|
||||
symbol: symbol,
|
||||
price: (inputRatioPrice * rate).toFixed(6),
|
||||
total: (completionRatioPrice * rate).toFixed(6),
|
||||
completionRatio: completionRatio,
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{cacheTokens > 0 && (
|
||||
<p>
|
||||
{i18next.t(
|
||||
@@ -1276,7 +1314,7 @@ export function renderModelPrice(
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{image && imageOutputTokens > 0 && (
|
||||
{image && imageInputTokens > 0 && (
|
||||
<p>
|
||||
{i18next.t(
|
||||
'图片输入价格:{{symbol}}{{price}} * {{ratio}} = {{symbol}}{{total}} / 1M tokens (图片倍率: {{imageRatio}})',
|
||||
@@ -1318,12 +1356,12 @@ export function renderModelPrice(
|
||||
{(() => {
|
||||
// 构建输入部分描述
|
||||
let inputDesc = '';
|
||||
if (image && imageOutputTokens > 0) {
|
||||
if (image && imageInputTokens > 0) {
|
||||
inputDesc = i18next.t(
|
||||
'(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * {{symbol}}{{price}}',
|
||||
{
|
||||
nonImageInput: inputTokens - imageOutputTokens,
|
||||
imageInput: imageOutputTokens,
|
||||
nonImageInput: inputTokens - imageInputTokens,
|
||||
imageInput: imageInputTokens,
|
||||
imageRatio: imageRatio,
|
||||
symbol: symbol,
|
||||
price: (inputRatioPrice * rate).toFixed(6),
|
||||
@@ -1363,16 +1401,29 @@ export function renderModelPrice(
|
||||
}
|
||||
|
||||
// 构建输出部分描述
|
||||
const outputDesc = i18next.t(
|
||||
'输出 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}',
|
||||
{
|
||||
completion: completionTokens,
|
||||
symbol: symbol,
|
||||
compPrice: (completionRatioPrice * rate).toFixed(6),
|
||||
ratio: groupRatio,
|
||||
ratioType: ratioLabel,
|
||||
},
|
||||
);
|
||||
const outputDesc = shouldSplitOutput
|
||||
? i18next.t(
|
||||
'输出 文字输出 {{textCompletion}} tokens / 1M tokens * {{symbol}}{{textCompPrice}} + 图片输出 {{imageCompletion}} tokens / 1M tokens * {{symbol}}{{imageCompPrice}}) * {{ratioType}} {{ratio}}',
|
||||
{
|
||||
textCompletion: baseCompletionTokens,
|
||||
imageCompletion: completionImageTokens,
|
||||
symbol: symbol,
|
||||
textCompPrice: (completionRatioPrice * rate).toFixed(6),
|
||||
imageCompPrice: (imageOutputRatioPrice * rate).toFixed(6),
|
||||
ratio: groupRatio,
|
||||
ratioType: ratioLabel,
|
||||
},
|
||||
)
|
||||
: i18next.t(
|
||||
'输出 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}',
|
||||
{
|
||||
completion: completionTokens,
|
||||
symbol: symbol,
|
||||
compPrice: (completionRatioPrice * rate).toFixed(6),
|
||||
ratio: groupRatio,
|
||||
ratioType: ratioLabel,
|
||||
},
|
||||
);
|
||||
|
||||
// 构建额外服务描述
|
||||
const extraServices = [
|
||||
|
||||
@@ -451,7 +451,9 @@ export const useLogsData = () => {
|
||||
other?.cache_ratio || 1.0,
|
||||
other?.image || false,
|
||||
other?.image_ratio || 0,
|
||||
other?.image_output || 0,
|
||||
other?.image_input_tokens || 0,
|
||||
other?.completion_image_tokens || 0,
|
||||
other?.image_output_ratio || 1.0,
|
||||
other?.web_search || false,
|
||||
other?.web_search_call_count || 0,
|
||||
other?.web_search_price || 0,
|
||||
|
||||
@@ -45,6 +45,7 @@ export default function ModelRatioSettings(props) {
|
||||
CacheRatio: '',
|
||||
CompletionRatio: '',
|
||||
ImageRatio: '',
|
||||
ImageOutputRatio: '',
|
||||
AudioRatio: '',
|
||||
AudioCompletionRatio: '',
|
||||
ExposeRatioEnabled: false,
|
||||
@@ -246,6 +247,32 @@ export default function ModelRatioSettings(props) {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} sm={16}>
|
||||
<Form.TextArea
|
||||
label={t('图片输出倍率(仅部分模型支持该计费)')}
|
||||
extraText={t(
|
||||
'图片输出相关的倍率设置,键为模型名称,值为倍率,仅部分模型支持该计费',
|
||||
)}
|
||||
placeholder={t(
|
||||
'为一个 JSON 文本,键为模型名称,值为倍率,例如:{"gemini-3-pro-image-preview": 60}',
|
||||
)}
|
||||
field={'ImageOutputRatio'}
|
||||
autosize={{ minRows: 6, maxRows: 12 }}
|
||||
trigger='blur'
|
||||
stopValidateWithError
|
||||
rules={[
|
||||
{
|
||||
validator: (rule, value) => verifyJSON(value),
|
||||
message: '不是合法的 JSON 字符串',
|
||||
},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
setInputs({ ...inputs, ImageOutputRatio: value })
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} sm={16}>
|
||||
<Form.TextArea
|
||||
|
||||
Reference in New Issue
Block a user