feat: gemini-2.5-flash-image-preview 文本和图片输出计费

This commit is contained in:
creamlike1024
2025-08-27 21:30:52 +08:00
parent 7ddf3a112c
commit e732c58426
9 changed files with 111 additions and 20 deletions

View File

@@ -2,11 +2,12 @@ package dto
import ( import (
"encoding/json" "encoding/json"
"github.com/gin-gonic/gin"
"one-api/common" "one-api/common"
"one-api/logger" "one-api/logger"
"one-api/types" "one-api/types"
"strings" "strings"
"github.com/gin-gonic/gin"
) )
type GeminiChatRequest struct { type GeminiChatRequest struct {
@@ -268,14 +269,15 @@ type GeminiChatResponse struct {
} }
type GeminiUsageMetadata struct { type GeminiUsageMetadata struct {
PromptTokenCount int `json:"promptTokenCount"` PromptTokenCount int `json:"promptTokenCount"`
CandidatesTokenCount int `json:"candidatesTokenCount"` CandidatesTokenCount int `json:"candidatesTokenCount"`
TotalTokenCount int `json:"totalTokenCount"` TotalTokenCount int `json:"totalTokenCount"`
ThoughtsTokenCount int `json:"thoughtsTokenCount"` ThoughtsTokenCount int `json:"thoughtsTokenCount"`
PromptTokensDetails []GeminiPromptTokensDetails `json:"promptTokensDetails"` PromptTokensDetails []GeminiModalityTokenCount `json:"promptTokensDetails"`
CandidatesTokensDetails []GeminiModalityTokenCount `json:"candidatesTokensDetails"`
} }
type GeminiPromptTokensDetails struct { type GeminiModalityTokenCount struct {
Modality string `json:"modality"` Modality string `json:"modality"`
TokenCount int `json:"tokenCount"` TokenCount int `json:"tokenCount"`
} }

View File

@@ -46,6 +46,32 @@ func GeminiTextGenerationHandler(c *gin.Context, info *relaycommon.RelayInfo, re
usage.CompletionTokenDetails.ReasoningTokens = geminiResponse.UsageMetadata.ThoughtsTokenCount usage.CompletionTokenDetails.ReasoningTokens = geminiResponse.UsageMetadata.ThoughtsTokenCount
if strings.HasPrefix(info.UpstreamModelName, "gemini-2.5-flash-image-preview") {
imageOutputCounts := 0
for _, candidate := range geminiResponse.Candidates {
for _, part := range candidate.Content.Parts {
if part.InlineData != nil && strings.HasPrefix(part.InlineData.MimeType, "image/") {
imageOutputCounts++
}
}
}
if imageOutputCounts != 0 {
usage.CompletionTokens = usage.CompletionTokens - imageOutputCounts*1290
usage.TotalTokens = usage.TotalTokens - imageOutputCounts*1290
c.Set("gemini_image_tokens", imageOutputCounts*1290)
}
}
// if strings.HasPrefix(info.UpstreamModelName, "gemini-2.5-flash-image-preview") {
// for _, detail := range geminiResponse.UsageMetadata.CandidatesTokensDetails {
// if detail.Modality == "IMAGE" {
// usage.CompletionTokens = usage.CompletionTokens - detail.TokenCount
// usage.TotalTokens = usage.TotalTokens - detail.TokenCount
// c.Set("gemini_image_tokens", detail.TokenCount)
// }
// }
// }
for _, detail := range geminiResponse.UsageMetadata.PromptTokensDetails { for _, detail := range geminiResponse.UsageMetadata.PromptTokensDetails {
if detail.Modality == "AUDIO" { if detail.Modality == "AUDIO" {
usage.PromptTokensDetails.AudioTokens = detail.TokenCount usage.PromptTokensDetails.AudioTokens = detail.TokenCount
@@ -136,6 +162,16 @@ func GeminiTextGenerationStreamHandler(c *gin.Context, info *relaycommon.RelayIn
usage.PromptTokensDetails.TextTokens = detail.TokenCount usage.PromptTokensDetails.TextTokens = detail.TokenCount
} }
} }
if strings.HasPrefix(info.UpstreamModelName, "gemini-2.5-flash-image-preview") {
for _, detail := range geminiResponse.UsageMetadata.CandidatesTokensDetails {
if detail.Modality == "IMAGE" {
usage.CompletionTokens = usage.CompletionTokens - detail.TokenCount
usage.TotalTokens = usage.TotalTokens - detail.TokenCount
c.Set("gemini_image_tokens", detail.TokenCount)
}
}
}
} }
// 直接发送 GeminiChatResponse 响应 // 直接发送 GeminiChatResponse 响应

View File

@@ -314,11 +314,22 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage
} else { } else {
quotaCalculateDecimal = dModelPrice.Mul(dQuotaPerUnit).Mul(dGroupRatio) quotaCalculateDecimal = dModelPrice.Mul(dQuotaPerUnit).Mul(dGroupRatio)
} }
var dGeminiImageOutputQuota decimal.Decimal
var imageOutputPrice float64
if strings.HasPrefix(modelName, "gemini-2.5-flash-image-preview") {
imageOutputPrice = operation_setting.GetGeminiImageOutputPricePerMillionTokens(modelName)
if imageOutputPrice > 0 {
dImageOutputTokens := decimal.NewFromInt(int64(ctx.GetInt("gemini_image_tokens")))
dGeminiImageOutputQuota = decimal.NewFromFloat(imageOutputPrice).Div(decimal.NewFromInt(1000000)).Mul(dImageOutputTokens).Mul(dGroupRatio).Mul(dQuotaPerUnit)
}
}
// 添加 responses tools call 调用的配额 // 添加 responses tools call 调用的配额
quotaCalculateDecimal = quotaCalculateDecimal.Add(dWebSearchQuota) quotaCalculateDecimal = quotaCalculateDecimal.Add(dWebSearchQuota)
quotaCalculateDecimal = quotaCalculateDecimal.Add(dFileSearchQuota) quotaCalculateDecimal = quotaCalculateDecimal.Add(dFileSearchQuota)
// 添加 audio input 独立计费 // 添加 audio input 独立计费
quotaCalculateDecimal = quotaCalculateDecimal.Add(audioInputQuota) quotaCalculateDecimal = quotaCalculateDecimal.Add(audioInputQuota)
// 添加 Gemini image output 计费
quotaCalculateDecimal = quotaCalculateDecimal.Add(dGeminiImageOutputQuota)
quota := int(quotaCalculateDecimal.Round(0).IntPart()) quota := int(quotaCalculateDecimal.Round(0).IntPart())
totalTokens := promptTokens + completionTokens totalTokens := promptTokens + completionTokens
@@ -413,6 +424,10 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage
other["audio_input_token_count"] = audioTokens other["audio_input_token_count"] = audioTokens
other["audio_input_price"] = audioInputPrice other["audio_input_price"] = audioInputPrice
} }
if !dGeminiImageOutputQuota.IsZero() {
other["image_output_token_count"] = ctx.GetInt("gemini_image_tokens")
other["image_output_price"] = imageOutputPrice
}
model.RecordConsumeLog(ctx, relayInfo.UserId, model.RecordConsumeLogParams{ model.RecordConsumeLog(ctx, relayInfo.UserId, model.RecordConsumeLogParams{
ChannelId: relayInfo.ChannelId, ChannelId: relayInfo.ChannelId,
PromptTokens: promptTokens, PromptTokens: promptTokens,

View File

@@ -304,7 +304,7 @@ func CountRequestToken(c *gin.Context, meta *types.TokenCountMeta, info *relayco
for _, file := range meta.Files { for _, file := range meta.Files {
switch file.FileType { switch file.FileType {
case types.FileTypeImage: case types.FileTypeImage:
if info.RelayFormat == types.RelayFormatGemini { if info.RelayFormat == types.RelayFormatGemini && !strings.HasPrefix(model, "gemini-2.5-flash-image-preview") {
tkm += 256 tkm += 256
} else { } else {
token, err := getImageToken(file, model, info.IsStream) token, err := getImageToken(file, model, info.IsStream)

View File

@@ -26,6 +26,7 @@ var defaultGeminiSettings = GeminiSettings{
SupportedImagineModels: []string{ SupportedImagineModels: []string{
"gemini-2.0-flash-exp-image-generation", "gemini-2.0-flash-exp-image-generation",
"gemini-2.0-flash-exp", "gemini-2.0-flash-exp",
"gemini-2.5-flash-image-preview",
}, },
ThinkingAdapterEnabled: false, ThinkingAdapterEnabled: false,
ThinkingAdapterBudgetTokensPercentage: 0.6, ThinkingAdapterBudgetTokensPercentage: 0.6,

View File

@@ -24,6 +24,10 @@ const (
ClaudeWebSearchPrice = 10.00 ClaudeWebSearchPrice = 10.00
) )
const (
Gemini25FlashImagePreviewImageOutputPrice = 30.00
)
func GetClaudeWebSearchPricePerThousand() float64 { func GetClaudeWebSearchPricePerThousand() float64 {
return ClaudeWebSearchPrice return ClaudeWebSearchPrice
} }
@@ -65,3 +69,10 @@ func GetGeminiInputAudioPricePerMillionTokens(modelName string) float64 {
} }
return 0 return 0
} }
func GetGeminiImageOutputPricePerMillionTokens(modelName string) float64 {
if strings.HasPrefix(modelName, "gemini-2.5-flash-image-preview") {
return Gemini25FlashImagePreviewImageOutputPrice
}
return 0
}

View File

@@ -178,6 +178,7 @@ var defaultModelRatio = map[string]float64{
"gemini-2.5-flash-lite-preview-thinking-*": 0.05, "gemini-2.5-flash-lite-preview-thinking-*": 0.05,
"gemini-2.5-flash-lite-preview-06-17": 0.05, "gemini-2.5-flash-lite-preview-06-17": 0.05,
"gemini-2.5-flash": 0.15, "gemini-2.5-flash": 0.15,
"gemini-2.5-flash-image-preview": 0.15, // $0.30text/image) / 1M tokens
"text-embedding-004": 0.001, "text-embedding-004": 0.001,
"chatglm_turbo": 0.3572, // ¥0.005 / 1k tokens "chatglm_turbo": 0.3572, // ¥0.005 / 1k tokens
"chatglm_pro": 0.7143, // ¥0.01 / 1k tokens "chatglm_pro": 0.7143, // ¥0.01 / 1k tokens
@@ -293,10 +294,11 @@ var (
) )
var defaultCompletionRatio = map[string]float64{ var defaultCompletionRatio = map[string]float64{
"gpt-4-gizmo-*": 2, "gpt-4-gizmo-*": 2,
"gpt-4o-gizmo-*": 3, "gpt-4o-gizmo-*": 3,
"gpt-4-all": 2, "gpt-4-all": 2,
"gpt-image-1": 8, "gpt-image-1": 8,
"gemini-2.5-flash-image-preview": 8.3333333333,
} }
// InitRatioSettings initializes all model related settings maps // InitRatioSettings initializes all model related settings maps

View File

@@ -1080,7 +1080,7 @@ export function renderModelPrice(
cacheRatio = 1.0, cacheRatio = 1.0,
image = false, image = false,
imageRatio = 1.0, imageRatio = 1.0,
imageOutputTokens = 0, imageInputTokens = 0,
webSearch = false, webSearch = false,
webSearchCallCount = 0, webSearchCallCount = 0,
webSearchPrice = 0, webSearchPrice = 0,
@@ -1090,6 +1090,8 @@ export function renderModelPrice(
audioInputSeperatePrice = false, audioInputSeperatePrice = false,
audioInputTokens = 0, audioInputTokens = 0,
audioInputPrice = 0, audioInputPrice = 0,
imageOutputTokens = 0,
imageOutputPrice = 0,
) { ) {
const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(groupRatio, user_group_ratio); const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(groupRatio, user_group_ratio);
groupRatio = effectiveGroupRatio; groupRatio = effectiveGroupRatio;
@@ -1117,9 +1119,9 @@ export function renderModelPrice(
let effectiveInputTokens = let effectiveInputTokens =
inputTokens - cacheTokens + cacheTokens * cacheRatio; inputTokens - cacheTokens + cacheTokens * cacheRatio;
// Handle image tokens if present // Handle image tokens if present
if (image && imageOutputTokens > 0) { if (image && imageInputTokens > 0) {
effectiveInputTokens = effectiveInputTokens =
inputTokens - imageOutputTokens + imageOutputTokens * imageRatio; inputTokens - imageInputTokens + imageInputTokens * imageRatio;
} }
if (audioInputTokens > 0) { if (audioInputTokens > 0) {
effectiveInputTokens -= audioInputTokens; effectiveInputTokens -= audioInputTokens;
@@ -1129,7 +1131,8 @@ export function renderModelPrice(
(audioInputTokens / 1000000) * audioInputPrice * groupRatio + (audioInputTokens / 1000000) * audioInputPrice * groupRatio +
(completionTokens / 1000000) * completionRatioPrice * groupRatio + (completionTokens / 1000000) * completionRatioPrice * groupRatio +
(webSearchCallCount / 1000) * webSearchPrice * groupRatio + (webSearchCallCount / 1000) * webSearchPrice * groupRatio +
(fileSearchCallCount / 1000) * fileSearchPrice * groupRatio; (fileSearchCallCount / 1000) * fileSearchPrice * groupRatio +
(imageOutputTokens / 1000000) * imageOutputPrice * groupRatio;
return ( return (
<> <>
@@ -1164,7 +1167,7 @@ export function renderModelPrice(
)} )}
</p> </p>
)} )}
{image && imageOutputTokens > 0 && ( {image && imageInputTokens > 0 && (
<p> <p>
{i18next.t( {i18next.t(
'图片输入价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (图片倍率: {{imageRatio}})', '图片输入价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (图片倍率: {{imageRatio}})',
@@ -1191,17 +1194,26 @@ export function renderModelPrice(
})} })}
</p> </p>
)} )}
{imageOutputPrice > 0 && imageOutputTokens > 0 && (
<p>
{i18next.t('图片输出价格:${{price}} * 分组倍率{{ratio}} = ${{total}} / 1M tokens', {
price: imageOutputPrice,
ratio: groupRatio,
total: imageOutputPrice * groupRatio,
})}
</p>
)}
<p></p> <p></p>
<p> <p>
{(() => { {(() => {
// 构建输入部分描述 // 构建输入部分描述
let inputDesc = ''; let inputDesc = '';
if (image && imageOutputTokens > 0) { if (image && imageInputTokens > 0) {
inputDesc = i18next.t( inputDesc = i18next.t(
'(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}}', '(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}}',
{ {
nonImageInput: inputTokens - imageOutputTokens, nonImageInput: inputTokens - imageInputTokens,
imageInput: imageOutputTokens, imageInput: imageInputTokens,
imageRatio: imageRatio, imageRatio: imageRatio,
price: inputRatioPrice, price: inputRatioPrice,
}, },
@@ -1271,6 +1283,16 @@ export function renderModelPrice(
}, },
) )
: '', : '',
imageOutputPrice > 0 && imageOutputTokens > 0
? i18next.t(
' + 图片输出 {{tokenCounts}} tokens * ${{price}} / 1M tokens * 分组倍率{{ratio}}',
{
tokenCounts: imageOutputTokens,
price: imageOutputPrice,
ratio: groupRatio,
},
)
: '',
].join(''); ].join('');
return i18next.t( return i18next.t(

View File

@@ -445,6 +445,8 @@ export const useLogsData = () => {
other?.audio_input_seperate_price || false, other?.audio_input_seperate_price || false,
other?.audio_input_token_count || 0, other?.audio_input_token_count || 0,
other?.audio_input_price || 0, other?.audio_input_price || 0,
other?.image_output_token_count || 0,
other?.image_output_price || 0,
); );
} }
expandDataLocal.push({ expandDataLocal.push({