Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
80cfa0d0
authored
Sep 07, 2025
by
Seefs
Committed by
GitHub
Sep 07, 2025
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1677 from QuantumNous/gemini-2.5-flash-image-preview-billing
feat: gemini-2.5-flash-image-preview 文本和图片输出计费
parents
8fcc4937
a45513a7
Show whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
103 additions
and
12 deletions
+103
-12
dto/gemini.go
+5
-3
relay/channel/gemini/relay-gemini-native.go
+36
-0
relay/compatible_handler.go
+15
-0
service/token_counter.go
+1
-1
setting/model_setting/gemini.go
+1
-0
setting/operation_setting/tools.go
+11
-0
setting/ratio_setting/model_ratio.go
+2
-0
web/src/helpers/render.jsx
+30
-8
web/src/hooks/usage-logs/useUsageLogsData.jsx
+2
-0
No files found.
dto/gemini.go
View file @
80cfa0d0
...
...
@@ -2,11 +2,12 @@ package dto
import
(
"encoding/json"
"github.com/gin-gonic/gin"
"one-api/common"
"one-api/logger"
"one-api/types"
"strings"
"github.com/gin-gonic/gin"
)
type
GeminiChatRequest
struct
{
...
...
@@ -272,10 +273,11 @@ type GeminiUsageMetadata struct {
CandidatesTokenCount
int
`json:"candidatesTokenCount"`
TotalTokenCount
int
`json:"totalTokenCount"`
ThoughtsTokenCount
int
`json:"thoughtsTokenCount"`
PromptTokensDetails
[]
GeminiPromptTokensDetails
`json:"promptTokensDetails"`
PromptTokensDetails
[]
GeminiModalityTokenCount
`json:"promptTokensDetails"`
CandidatesTokensDetails
[]
GeminiModalityTokenCount
`json:"candidatesTokensDetails"`
}
type
Gemini
PromptTokensDetails
struct
{
type
Gemini
ModalityTokenCount
struct
{
Modality
string
`json:"modality"`
TokenCount
int
`json:"tokenCount"`
}
...
...
relay/channel/gemini/relay-gemini-native.go
View file @
80cfa0d0
...
...
@@ -46,6 +46,32 @@ func GeminiTextGenerationHandler(c *gin.Context, info *relaycommon.RelayInfo, re
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
{
if
detail
.
Modality
==
"AUDIO"
{
usage
.
PromptTokensDetails
.
AudioTokens
=
detail
.
TokenCount
...
...
@@ -136,6 +162,16 @@ func GeminiTextGenerationStreamHandler(c *gin.Context, info *relaycommon.RelayIn
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 响应
...
...
relay/compatible_handler.go
View file @
80cfa0d0
...
...
@@ -314,11 +314,22 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage
}
else
{
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 调用的配额
quotaCalculateDecimal
=
quotaCalculateDecimal
.
Add
(
dWebSearchQuota
)
quotaCalculateDecimal
=
quotaCalculateDecimal
.
Add
(
dFileSearchQuota
)
// 添加 audio input 独立计费
quotaCalculateDecimal
=
quotaCalculateDecimal
.
Add
(
audioInputQuota
)
// 添加 Gemini image output 计费
quotaCalculateDecimal
=
quotaCalculateDecimal
.
Add
(
dGeminiImageOutputQuota
)
quota
:=
int
(
quotaCalculateDecimal
.
Round
(
0
)
.
IntPart
())
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_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
{
ChannelId
:
relayInfo
.
ChannelId
,
PromptTokens
:
promptTokens
,
...
...
service/token_counter.go
View file @
80cfa0d0
...
...
@@ -336,7 +336,7 @@ func CountRequestToken(c *gin.Context, meta *types.TokenCountMeta, info *relayco
for
i
,
file
:=
range
meta
.
Files
{
switch
file
.
FileType
{
case
types
.
FileTypeImage
:
if
info
.
RelayFormat
==
types
.
RelayFormatGemini
{
if
info
.
RelayFormat
==
types
.
RelayFormatGemini
&&
!
strings
.
HasPrefix
(
model
,
"gemini-2.5-flash-image-preview"
)
{
tkm
+=
256
}
else
{
token
,
err
:=
getImageToken
(
file
,
model
,
info
.
IsStream
)
...
...
setting/model_setting/gemini.go
View file @
80cfa0d0
...
...
@@ -26,6 +26,7 @@ var defaultGeminiSettings = GeminiSettings{
SupportedImagineModels
:
[]
string
{
"gemini-2.0-flash-exp-image-generation"
,
"gemini-2.0-flash-exp"
,
"gemini-2.5-flash-image-preview"
,
},
ThinkingAdapterEnabled
:
false
,
ThinkingAdapterBudgetTokensPercentage
:
0.6
,
...
...
setting/operation_setting/tools.go
View file @
80cfa0d0
...
...
@@ -24,6 +24,10 @@ const (
ClaudeWebSearchPrice
=
10.00
)
const
(
Gemini25FlashImagePreviewImageOutputPrice
=
30.00
)
func
GetClaudeWebSearchPricePerThousand
()
float64
{
return
ClaudeWebSearchPrice
}
...
...
@@ -65,3 +69,10 @@ func GetGeminiInputAudioPricePerMillionTokens(modelName string) float64 {
}
return
0
}
func
GetGeminiImageOutputPricePerMillionTokens
(
modelName
string
)
float64
{
if
strings
.
HasPrefix
(
modelName
,
"gemini-2.5-flash-image-preview"
)
{
return
Gemini25FlashImagePreviewImageOutputPrice
}
return
0
}
setting/ratio_setting/model_ratio.go
View file @
80cfa0d0
...
...
@@ -178,6 +178,7 @@ var defaultModelRatio = map[string]float64{
"gemini-2.5-flash-lite-preview-thinking-*"
:
0.05
,
"gemini-2.5-flash-lite-preview-06-17"
:
0.05
,
"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
,
"chatglm_turbo"
:
0.3572
,
// ¥0.005 / 1k tokens
"chatglm_pro"
:
0.7143
,
// ¥0.01 / 1k tokens
...
...
@@ -297,6 +298,7 @@ var defaultCompletionRatio = map[string]float64{
"gpt-4o-gizmo-*"
:
3
,
"gpt-4-all"
:
2
,
"gpt-image-1"
:
8
,
"gemini-2.5-flash-image-preview"
:
8.3333333333
,
}
// InitRatioSettings initializes all model related settings maps
...
...
web/src/helpers/render.jsx
View file @
80cfa0d0
...
...
@@ -1017,7 +1017,7 @@ export function renderModelPrice(
cacheRatio
=
1.0
,
image
=
false
,
imageRatio
=
1.0
,
image
Out
putTokens
=
0
,
image
In
putTokens
=
0
,
webSearch
=
false
,
webSearchCallCount
=
0
,
webSearchPrice
=
0
,
...
...
@@ -1027,6 +1027,8 @@ export function renderModelPrice(
audioInputSeperatePrice
=
false
,
audioInputTokens
=
0
,
audioInputPrice
=
0
,
imageOutputTokens
=
0
,
imageOutputPrice
=
0
,
)
{
const
{
ratio
:
effectiveGroupRatio
,
label
:
ratioLabel
}
=
getEffectiveRatio
(
groupRatio
,
...
...
@@ -1057,9 +1059,9 @@ export function renderModelPrice(
let
effectiveInputTokens
=
inputTokens
-
cacheTokens
+
cacheTokens
*
cacheRatio
;
// Handle image tokens if present
if
(
image
&&
image
Out
putTokens
>
0
)
{
if
(
image
&&
image
In
putTokens
>
0
)
{
effectiveInputTokens
=
inputTokens
-
image
OutputTokens
+
imageOut
putTokens
*
imageRatio
;
inputTokens
-
image
InputTokens
+
imageIn
putTokens
*
imageRatio
;
}
if
(
audioInputTokens
>
0
)
{
effectiveInputTokens
-=
audioInputTokens
;
...
...
@@ -1069,7 +1071,8 @@ export function renderModelPrice(
(
audioInputTokens
/
1000000
)
*
audioInputPrice
*
groupRatio
+
(
completionTokens
/
1000000
)
*
completionRatioPrice
*
groupRatio
+
(
webSearchCallCount
/
1000
)
*
webSearchPrice
*
groupRatio
+
(
fileSearchCallCount
/
1000
)
*
fileSearchPrice
*
groupRatio
;
(
fileSearchCallCount
/
1000
)
*
fileSearchPrice
*
groupRatio
+
(
imageOutputTokens
/
1000000
)
*
imageOutputPrice
*
groupRatio
;
return
(
<>
...
...
@@ -1104,7 +1107,7 @@ export function renderModelPrice(
)
}
</
p
>
)
}
{
image
&&
image
Out
putTokens
>
0
&&
(
{
image
&&
image
In
putTokens
>
0
&&
(
<
p
>
{
i18next
.
t
(
'图片输入价格:
$
{{
price
}}
*
{{
ratio
}}
=
$
{{
total
}}
/ 1M tokens
(
图片倍率: {{imageRatio}}
)
'
,
...
...
@@ -1131,17 +1134,26 @@ export function renderModelPrice(
})
}
</
p
>
)
}
{
imageOutputPrice
>
0
&&
imageOutputTokens
>
0
&&
(
<
p
>
{
i18next
.
t
(
'图片输出价格:
$
{{
price
}}
*
分组倍率
{{
ratio
}}
=
$
{{
total
}}
/ 1M tokens', {
price
:
imageOutputPrice
,
ratio
:
groupRatio
,
total
:
imageOutputPrice
*
groupRatio
,
})
}
</
p
>
)
}
<
p
></
p
>
<
p
>
{
(()
=>
{
// 构建输入部分描述
let
inputDesc
=
''
;
if
(
image
&&
image
Out
putTokens
>
0
)
{
if
(
image
&&
image
In
putTokens
>
0
)
{
inputDesc
=
i18next
.
t
(
'
(
输入
{{
nonImageInput
}}
tokens
+
图片输入
{{
imageInput
}}
tokens
*
{{
imageRatio
}}
/ 1M tokens * ${{price}}'
,
{
nonImageInput
:
inputTokens
-
image
Out
putTokens
,
imageInput
:
image
Out
putTokens
,
nonImageInput
:
inputTokens
-
image
In
putTokens
,
imageInput
:
image
In
putTokens
,
imageRatio
:
imageRatio
,
price
:
inputRatioPrice
,
},
...
...
@@ -1211,6 +1223,16 @@ export function renderModelPrice(
},
)
:
''
,
imageOutputPrice
>
0
&&
imageOutputTokens
>
0
?
i18next
.
t
(
'
+
图片输出
{{
tokenCounts
}}
tokens
*
$
{{
price
}}
/ 1M tokens * 分组倍率{{ratio}}'
,
{
tokenCounts
:
imageOutputTokens
,
price
:
imageOutputPrice
,
ratio
:
groupRatio
,
},
)
:
''
,
].
join
(
''
);
return
i18next
.
t
(
...
...
web/src/hooks/usage-logs/useUsageLogsData.jsx
View file @
80cfa0d0
...
...
@@ -447,6 +447,8 @@ export const useLogsData = () => {
other
?.
audio_input_seperate_price
||
false
,
other
?.
audio_input_token_count
||
0
,
other
?.
audio_input_price
||
0
,
other
?.
image_output_token_count
||
0
,
other
?.
image_output_price
||
0
,
);
}
expandDataLocal
.
push
({
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment