mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 05:02:17 +00:00
feat: gemini-2.5-flash-image-preview 文本和图片输出计费
This commit is contained in:
@@ -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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 响应
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.30(text/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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
Reference in New Issue
Block a user