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
d91041fa
authored
Sep 16, 2025
by
Calcium-Ion
Committed by
GitHub
Sep 16, 2025
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1813 from QuantumNous/fix1797
fix: openai responses api 未统计图像生成调用计费
parents
46c561ee
b9befccd
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
132 additions
and
3 deletions
+132
-3
dto/openai_response.go
+42
-0
relay/channel/openai/relay_responses.go
+14
-1
relay/compatible_handler.go
+13
-0
setting/operation_setting/tools.go
+40
-0
web/src/helpers/render.jsx
+21
-2
web/src/hooks/usage-logs/useUsageLogsData.jsx
+2
-0
No files found.
dto/openai_response.go
View file @
d91041fa
...
...
@@ -6,6 +6,10 @@ import (
"one-api/types"
)
const
(
ResponsesOutputTypeImageGenerationCall
=
"image_generation_call"
)
type
SimpleResponse
struct
{
Usage
`json:"usage"`
Error
any
`json:"error"`
...
...
@@ -273,6 +277,42 @@ func (o *OpenAIResponsesResponse) GetOpenAIError() *types.OpenAIError {
return
GetOpenAIError
(
o
.
Error
)
}
func
(
o
*
OpenAIResponsesResponse
)
HasImageGenerationCall
()
bool
{
if
len
(
o
.
Output
)
==
0
{
return
false
}
for
_
,
output
:=
range
o
.
Output
{
if
output
.
Type
==
ResponsesOutputTypeImageGenerationCall
{
return
true
}
}
return
false
}
func
(
o
*
OpenAIResponsesResponse
)
GetQuality
()
string
{
if
len
(
o
.
Output
)
==
0
{
return
""
}
for
_
,
output
:=
range
o
.
Output
{
if
output
.
Type
==
ResponsesOutputTypeImageGenerationCall
{
return
output
.
Quality
}
}
return
""
}
func
(
o
*
OpenAIResponsesResponse
)
GetSize
()
string
{
if
len
(
o
.
Output
)
==
0
{
return
""
}
for
_
,
output
:=
range
o
.
Output
{
if
output
.
Type
==
ResponsesOutputTypeImageGenerationCall
{
return
output
.
Size
}
}
return
""
}
type
IncompleteDetails
struct
{
Reasoning
string
`json:"reasoning"`
}
...
...
@@ -283,6 +323,8 @@ type ResponsesOutput struct {
Status
string
`json:"status"`
Role
string
`json:"role"`
Content
[]
ResponsesOutputContent
`json:"content"`
Quality
string
`json:"quality"`
Size
string
`json:"size"`
}
type
ResponsesOutputContent
struct
{
...
...
relay/channel/openai/relay_responses.go
View file @
d91041fa
...
...
@@ -33,6 +33,12 @@ func OaiResponsesHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http
return
nil
,
types
.
WithOpenAIError
(
*
oaiError
,
resp
.
StatusCode
)
}
if
responsesResponse
.
HasImageGenerationCall
()
{
c
.
Set
(
"image_generation_call"
,
true
)
c
.
Set
(
"image_generation_call_quality"
,
responsesResponse
.
GetQuality
())
c
.
Set
(
"image_generation_call_size"
,
responsesResponse
.
GetSize
())
}
// 写入新的 response body
service
.
IOCopyBytesGracefully
(
c
,
resp
,
responseBody
)
...
...
@@ -80,7 +86,8 @@ func OaiResponsesStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp
sendResponsesStreamData
(
c
,
streamResponse
,
data
)
switch
streamResponse
.
Type
{
case
"response.completed"
:
if
streamResponse
.
Response
!=
nil
&&
streamResponse
.
Response
.
Usage
!=
nil
{
if
streamResponse
.
Response
!=
nil
{
if
streamResponse
.
Response
.
Usage
!=
nil
{
if
streamResponse
.
Response
.
Usage
.
InputTokens
!=
0
{
usage
.
PromptTokens
=
streamResponse
.
Response
.
Usage
.
InputTokens
}
...
...
@@ -94,6 +101,12 @@ func OaiResponsesStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp
usage
.
PromptTokensDetails
.
CachedTokens
=
streamResponse
.
Response
.
Usage
.
InputTokensDetails
.
CachedTokens
}
}
if
streamResponse
.
Response
.
HasImageGenerationCall
()
{
c
.
Set
(
"image_generation_call"
,
true
)
c
.
Set
(
"image_generation_call_quality"
,
streamResponse
.
Response
.
GetQuality
())
c
.
Set
(
"image_generation_call_size"
,
streamResponse
.
Response
.
GetSize
())
}
}
case
"response.output_text.delta"
:
// 处理输出文本
responseTextBuilder
.
WriteString
(
streamResponse
.
Delta
)
...
...
relay/compatible_handler.go
View file @
d91041fa
...
...
@@ -276,6 +276,13 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage
fileSearchTool
.
CallCount
,
dFileSearchQuota
.
String
())
}
}
var
dImageGenerationCallQuota
decimal
.
Decimal
var
imageGenerationCallPrice
float64
if
ctx
.
GetBool
(
"image_generation_call"
)
{
imageGenerationCallPrice
=
operation_setting
.
GetGPTImage1PriceOnceCall
(
ctx
.
GetString
(
"image_generation_call_quality"
),
ctx
.
GetString
(
"image_generation_call_size"
))
dImageGenerationCallQuota
=
decimal
.
NewFromFloat
(
imageGenerationCallPrice
)
.
Mul
(
dGroupRatio
)
.
Mul
(
dQuotaPerUnit
)
extraContent
+=
fmt
.
Sprintf
(
"Image Generation Call 花费 %s"
,
dImageGenerationCallQuota
.
String
())
}
var
quotaCalculateDecimal
decimal
.
Decimal
...
...
@@ -331,6 +338,8 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage
quotaCalculateDecimal
=
quotaCalculateDecimal
.
Add
(
dFileSearchQuota
)
// 添加 audio input 独立计费
quotaCalculateDecimal
=
quotaCalculateDecimal
.
Add
(
audioInputQuota
)
// 添加 image generation call 计费
quotaCalculateDecimal
=
quotaCalculateDecimal
.
Add
(
dImageGenerationCallQuota
)
quota
:=
int
(
quotaCalculateDecimal
.
Round
(
0
)
.
IntPart
())
totalTokens
:=
promptTokens
+
completionTokens
...
...
@@ -429,6 +438,10 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage
other
[
"audio_input_token_count"
]
=
audioTokens
other
[
"audio_input_price"
]
=
audioInputPrice
}
if
!
dImageGenerationCallQuota
.
IsZero
()
{
other
[
"image_generation_call"
]
=
true
other
[
"image_generation_call_price"
]
=
imageGenerationCallPrice
}
model
.
RecordConsumeLog
(
ctx
,
relayInfo
.
UserId
,
model
.
RecordConsumeLogParams
{
ChannelId
:
relayInfo
.
ChannelId
,
PromptTokens
:
promptTokens
,
...
...
setting/operation_setting/tools.go
View file @
d91041fa
...
...
@@ -11,6 +11,18 @@ const (
)
const
(
GPTImage1Low1024x1024
=
0.011
GPTImage1Low1024x1536
=
0.016
GPTImage1Low1536x1024
=
0.016
GPTImage1Medium1024x1024
=
0.042
GPTImage1Medium1024x1536
=
0.063
GPTImage1Medium1536x1024
=
0.063
GPTImage1High1024x1024
=
0.167
GPTImage1High1024x1536
=
0.25
GPTImage1High1536x1024
=
0.25
)
const
(
// Gemini Audio Input Price
Gemini25FlashPreviewInputAudioPrice
=
1.00
Gemini25FlashProductionInputAudioPrice
=
1.00
// for `gemini-2.5-flash`
...
...
@@ -65,3 +77,31 @@ func GetGeminiInputAudioPricePerMillionTokens(modelName string) float64 {
}
return
0
}
func
GetGPTImage1PriceOnceCall
(
quality
string
,
size
string
)
float64
{
prices
:=
map
[
string
]
map
[
string
]
float64
{
"low"
:
{
"1024x1024"
:
GPTImage1Low1024x1024
,
"1024x1536"
:
GPTImage1Low1024x1536
,
"1536x1024"
:
GPTImage1Low1536x1024
,
},
"medium"
:
{
"1024x1024"
:
GPTImage1Medium1024x1024
,
"1024x1536"
:
GPTImage1Medium1024x1536
,
"1536x1024"
:
GPTImage1Medium1536x1024
,
},
"high"
:
{
"1024x1024"
:
GPTImage1High1024x1024
,
"1024x1536"
:
GPTImage1High1024x1536
,
"1536x1024"
:
GPTImage1High1536x1024
,
},
}
if
qualityMap
,
exists
:=
prices
[
quality
];
exists
{
if
price
,
exists
:=
qualityMap
[
size
];
exists
{
return
price
}
}
return
GPTImage1High1024x1024
}
web/src/helpers/render.jsx
View file @
d91041fa
...
...
@@ -1027,6 +1027,8 @@ export function renderModelPrice(
audioInputSeperatePrice
=
false
,
audioInputTokens
=
0
,
audioInputPrice
=
0
,
imageGenerationCall
=
false
,
imageGenerationCallPrice
=
0
,
)
{
const
{
ratio
:
effectiveGroupRatio
,
label
:
ratioLabel
}
=
getEffectiveRatio
(
groupRatio
,
...
...
@@ -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
+
(
imageGenerationCallPrice
*
groupRatio
);
return
(
<>
...
...
@@ -1131,7 +1134,13 @@ export function renderModelPrice(
})
}
</
p
>
)
}
<
p
></
p
>
{
imageGenerationCall
&&
imageGenerationCallPrice
>
0
&&
(
<
p
>
{
i18next
.
t
(
'图片生成调用:
$
{{
price
}}
/ 1次', {
price
:
imageGenerationCallPrice
,
})
}
</
p
>
)
}
<
p
>
{
(()
=>
{
// 构建输入部分描述
...
...
@@ -1211,6 +1220,16 @@ export function renderModelPrice(
},
)
:
''
,
imageGenerationCall
&&
imageGenerationCallPrice
>
0
?
i18next
.
t
(
'
+
图片生成调用
$
{{
price
}}
/ 1次 * {{ratioType}} {{ratio}}'
,
{
price
:
imageGenerationCallPrice
,
ratio
:
groupRatio
,
ratioType
:
ratioLabel
,
},
)
:
''
,
].
join
(
''
);
return
i18next
.
t
(
...
...
web/src/hooks/usage-logs/useUsageLogsData.jsx
View file @
d91041fa
...
...
@@ -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_generation_call
||
false
,
other
?.
image_generation_call_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