From 11cf70e60d559953764c30cc4ff4fd47dad207e5 Mon Sep 17 00:00:00 2001
From: creamlike1024
Date: Tue, 16 Sep 2025 12:47:59 +0800
Subject: [PATCH] =?UTF-8?q?fix:=20openai=20responses=20api=20=E6=9C=AA?=
=?UTF-8?q?=E7=BB=9F=E8=AE=A1=E5=9B=BE=E5=83=8F=E7=94=9F=E6=88=90=E8=B0=83?=
=?UTF-8?q?=E7=94=A8=E8=AE=A1=E8=B4=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
dto/openai_response.go | 42 +++++++++++++++++++
relay/channel/openai/relay_responses.go | 35 +++++++++++-----
relay/compatible_handler.go | 13 ++++++
setting/operation_setting/tools.go | 40 ++++++++++++++++++
web/src/helpers/render.jsx | 23 +++++++++-
web/src/hooks/usage-logs/useUsageLogsData.jsx | 2 +
6 files changed, 142 insertions(+), 13 deletions(-)
diff --git a/dto/openai_response.go b/dto/openai_response.go
index 966748cb5..6353c15ff 100644
--- a/dto/openai_response.go
+++ b/dto/openai_response.go
@@ -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 {
diff --git a/relay/channel/openai/relay_responses.go b/relay/channel/openai/relay_responses.go
index e188889e4..85938a771 100644
--- a/relay/channel/openai/relay_responses.go
+++ b/relay/channel/openai/relay_responses.go
@@ -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,18 +86,25 @@ 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.Usage.InputTokens != 0 {
- usage.PromptTokens = streamResponse.Response.Usage.InputTokens
+ if streamResponse.Response != nil {
+ if streamResponse.Response.Usage != nil {
+ if streamResponse.Response.Usage.InputTokens != 0 {
+ usage.PromptTokens = streamResponse.Response.Usage.InputTokens
+ }
+ if streamResponse.Response.Usage.OutputTokens != 0 {
+ usage.CompletionTokens = streamResponse.Response.Usage.OutputTokens
+ }
+ if streamResponse.Response.Usage.TotalTokens != 0 {
+ usage.TotalTokens = streamResponse.Response.Usage.TotalTokens
+ }
+ if streamResponse.Response.Usage.InputTokensDetails != nil {
+ usage.PromptTokensDetails.CachedTokens = streamResponse.Response.Usage.InputTokensDetails.CachedTokens
+ }
}
- if streamResponse.Response.Usage.OutputTokens != 0 {
- usage.CompletionTokens = streamResponse.Response.Usage.OutputTokens
- }
- if streamResponse.Response.Usage.TotalTokens != 0 {
- usage.TotalTokens = streamResponse.Response.Usage.TotalTokens
- }
- if streamResponse.Response.Usage.InputTokensDetails != nil {
- 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":
diff --git a/relay/compatible_handler.go b/relay/compatible_handler.go
index 01ab1fff4..c931fe2a0 100644
--- a/relay/compatible_handler.go
+++ b/relay/compatible_handler.go
@@ -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,
diff --git a/setting/operation_setting/tools.go b/setting/operation_setting/tools.go
index 549a1862e..5b89d6fec 100644
--- a/setting/operation_setting/tools.go
+++ b/setting/operation_setting/tools.go
@@ -10,6 +10,18 @@ const (
FileSearchPrice = 2.5
)
+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
@@ -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
+}
diff --git a/web/src/helpers/render.jsx b/web/src/helpers/render.jsx
index 65332701b..c331d7fe8 100644
--- a/web/src/helpers/render.jsx
+++ b/web/src/helpers/render.jsx
@@ -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 +
+ (imageGenerationCall * imageGenerationCallPrice * groupRatio);
return (
<>
@@ -1131,7 +1134,13 @@ export function renderModelPrice(
})}
)}
-
+ {imageGenerationCall && imageGenerationCallPrice > 0 && (
+
+ {i18next.t('图片生成调用:${{price}} / 1次', {
+ price: imageGenerationCallPrice,
+ })}
+
+ )}
{(() => {
// 构建输入部分描述
@@ -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(
diff --git a/web/src/hooks/usage-logs/useUsageLogsData.jsx b/web/src/hooks/usage-logs/useUsageLogsData.jsx
index 81f3f539a..d434e7333 100644
--- a/web/src/hooks/usage-logs/useUsageLogsData.jsx
+++ b/web/src/hooks/usage-logs/useUsageLogsData.jsx
@@ -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({