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
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
111 additions
and
20 deletions
+111
-20
dto/gemini.go
+9
-7
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
+6
-4
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
...
@@ -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 {
...
@@ -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
Gemini
PromptTokensDetails
struct
{
type
Gemini
ModalityTokenCount
struct
{
Modality
string
`json:"modality"`
Modality
string
`json:"modality"`
TokenCount
int
`json:"tokenCount"`
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
...
@@ -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
...
@@ -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 响应
...
...
relay/compatible_handler.go
View file @
80cfa0d0
...
@@ -314,11 +314,22 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage
...
@@ -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
...
@@ -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
,
...
...
service/token_counter.go
View file @
80cfa0d0
...
@@ -336,7 +336,7 @@ func CountRequestToken(c *gin.Context, meta *types.TokenCountMeta, info *relayco
...
@@ -336,7 +336,7 @@ func CountRequestToken(c *gin.Context, meta *types.TokenCountMeta, info *relayco
for
i
,
file
:=
range
meta
.
Files
{
for
i
,
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
)
...
...
setting/model_setting/gemini.go
View file @
80cfa0d0
...
@@ -26,6 +26,7 @@ var defaultGeminiSettings = GeminiSettings{
...
@@ -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
,
...
...
setting/operation_setting/tools.go
View file @
80cfa0d0
...
@@ -24,6 +24,10 @@ const (
...
@@ -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 {
...
@@ -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
}
setting/ratio_setting/model_ratio.go
View file @
80cfa0d0
...
@@ -178,6 +178,7 @@ var defaultModelRatio = map[string]float64{
...
@@ -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 (
...
@@ -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
...
...
web/src/helpers/render.jsx
View file @
80cfa0d0
...
@@ -1017,7 +1017,7 @@ export function renderModelPrice(
...
@@ -1017,7 +1017,7 @@ export function renderModelPrice(
cacheRatio
=
1.0
,
cacheRatio
=
1.0
,
image
=
false
,
image
=
false
,
imageRatio
=
1.0
,
imageRatio
=
1.0
,
image
Out
putTokens
=
0
,
image
In
putTokens
=
0
,
webSearch
=
false
,
webSearch
=
false
,
webSearchCallCount
=
0
,
webSearchCallCount
=
0
,
webSearchPrice
=
0
,
webSearchPrice
=
0
,
...
@@ -1027,6 +1027,8 @@ export function renderModelPrice(
...
@@ -1027,6 +1027,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
(
const
{
ratio
:
effectiveGroupRatio
,
label
:
ratioLabel
}
=
getEffectiveRatio
(
groupRatio
,
groupRatio
,
...
@@ -1057,9 +1059,9 @@ export function renderModelPrice(
...
@@ -1057,9 +1059,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
&&
image
Out
putTokens
>
0
)
{
if
(
image
&&
image
In
putTokens
>
0
)
{
effectiveInputTokens
=
effectiveInputTokens
=
inputTokens
-
image
OutputTokens
+
imageOut
putTokens
*
imageRatio
;
inputTokens
-
image
InputTokens
+
imageIn
putTokens
*
imageRatio
;
}
}
if
(
audioInputTokens
>
0
)
{
if
(
audioInputTokens
>
0
)
{
effectiveInputTokens
-=
audioInputTokens
;
effectiveInputTokens
-=
audioInputTokens
;
...
@@ -1069,7 +1071,8 @@ export function renderModelPrice(
...
@@ -1069,7 +1071,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
(
<>
<>
...
@@ -1104,7 +1107,7 @@ export function renderModelPrice(
...
@@ -1104,7 +1107,7 @@ export function renderModelPrice(
)
}
)
}
</
p
>
</
p
>
)
}
)
}
{
image
&&
image
Out
putTokens
>
0
&&
(
{
image
&&
image
In
putTokens
>
0
&&
(
<
p
>
<
p
>
{
i18next
.
t
(
{
i18next
.
t
(
'图片输入价格:
$
{{
price
}}
*
{{
ratio
}}
=
$
{{
total
}}
/ 1M tokens
(
图片倍率: {{imageRatio}}
)
'
,
'图片输入价格:
$
{{
price
}}
*
{{
ratio
}}
=
$
{{
total
}}
/ 1M tokens
(
图片倍率: {{imageRatio}}
)
'
,
...
@@ -1131,17 +1134,26 @@ export function renderModelPrice(
...
@@ -1131,17 +1134,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
&&
image
Out
putTokens
>
0
)
{
if
(
image
&&
image
In
putTokens
>
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
-
image
Out
putTokens
,
nonImageInput
:
inputTokens
-
image
In
putTokens
,
imageInput
:
image
Out
putTokens
,
imageInput
:
image
In
putTokens
,
imageRatio
:
imageRatio
,
imageRatio
:
imageRatio
,
price
:
inputRatioPrice
,
price
:
inputRatioPrice
,
},
},
...
@@ -1211,6 +1223,16 @@ export function renderModelPrice(
...
@@ -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
(
''
);
].
join
(
''
);
return
i18next
.
t
(
return
i18next
.
t
(
...
...
web/src/hooks/usage-logs/useUsageLogsData.jsx
View file @
80cfa0d0
...
@@ -447,6 +447,8 @@ export const useLogsData = () => {
...
@@ -447,6 +447,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
({
...
...
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