Merge branch 'main' into fix/volcengine-image-model
This commit is contained in:
@@ -6,6 +6,10 @@ import (
|
|||||||
"one-api/types"
|
"one-api/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ResponsesOutputTypeImageGenerationCall = "image_generation_call"
|
||||||
|
)
|
||||||
|
|
||||||
type SimpleResponse struct {
|
type SimpleResponse struct {
|
||||||
Usage `json:"usage"`
|
Usage `json:"usage"`
|
||||||
Error any `json:"error"`
|
Error any `json:"error"`
|
||||||
@@ -273,6 +277,42 @@ func (o *OpenAIResponsesResponse) GetOpenAIError() *types.OpenAIError {
|
|||||||
return GetOpenAIError(o.Error)
|
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 {
|
type IncompleteDetails struct {
|
||||||
Reasoning string `json:"reasoning"`
|
Reasoning string `json:"reasoning"`
|
||||||
}
|
}
|
||||||
@@ -283,6 +323,8 @@ type ResponsesOutput struct {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Content []ResponsesOutputContent `json:"content"`
|
Content []ResponsesOutputContent `json:"content"`
|
||||||
|
Quality string `json:"quality"`
|
||||||
|
Size string `json:"size"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResponsesOutputContent struct {
|
type ResponsesOutputContent struct {
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ func OaiResponsesHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http
|
|||||||
return nil, types.WithOpenAIError(*oaiError, resp.StatusCode)
|
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
|
// 写入新的 response body
|
||||||
service.IOCopyBytesGracefully(c, resp, responseBody)
|
service.IOCopyBytesGracefully(c, resp, responseBody)
|
||||||
|
|
||||||
@@ -80,18 +86,25 @@ func OaiResponsesStreamHandler(c *gin.Context, info *relaycommon.RelayInfo, resp
|
|||||||
sendResponsesStreamData(c, streamResponse, data)
|
sendResponsesStreamData(c, streamResponse, data)
|
||||||
switch streamResponse.Type {
|
switch streamResponse.Type {
|
||||||
case "response.completed":
|
case "response.completed":
|
||||||
if streamResponse.Response != nil && streamResponse.Response.Usage != nil {
|
if streamResponse.Response != nil {
|
||||||
if streamResponse.Response.Usage.InputTokens != 0 {
|
if streamResponse.Response.Usage != nil {
|
||||||
usage.PromptTokens = streamResponse.Response.Usage.InputTokens
|
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 {
|
if streamResponse.Response.HasImageGenerationCall() {
|
||||||
usage.CompletionTokens = streamResponse.Response.Usage.OutputTokens
|
c.Set("image_generation_call", true)
|
||||||
}
|
c.Set("image_generation_call_quality", streamResponse.Response.GetQuality())
|
||||||
if streamResponse.Response.Usage.TotalTokens != 0 {
|
c.Set("image_generation_call_size", streamResponse.Response.GetSize())
|
||||||
usage.TotalTokens = streamResponse.Response.Usage.TotalTokens
|
|
||||||
}
|
|
||||||
if streamResponse.Response.Usage.InputTokensDetails != nil {
|
|
||||||
usage.PromptTokensDetails.CachedTokens = streamResponse.Response.Usage.InputTokensDetails.CachedTokens
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "response.output_text.delta":
|
case "response.output_text.delta":
|
||||||
|
|||||||
@@ -276,6 +276,13 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage
|
|||||||
fileSearchTool.CallCount, dFileSearchQuota.String())
|
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
|
var quotaCalculateDecimal decimal.Decimal
|
||||||
|
|
||||||
@@ -331,6 +338,8 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage
|
|||||||
quotaCalculateDecimal = quotaCalculateDecimal.Add(dFileSearchQuota)
|
quotaCalculateDecimal = quotaCalculateDecimal.Add(dFileSearchQuota)
|
||||||
// 添加 audio input 独立计费
|
// 添加 audio input 独立计费
|
||||||
quotaCalculateDecimal = quotaCalculateDecimal.Add(audioInputQuota)
|
quotaCalculateDecimal = quotaCalculateDecimal.Add(audioInputQuota)
|
||||||
|
// 添加 image generation call 计费
|
||||||
|
quotaCalculateDecimal = quotaCalculateDecimal.Add(dImageGenerationCallQuota)
|
||||||
|
|
||||||
quota := int(quotaCalculateDecimal.Round(0).IntPart())
|
quota := int(quotaCalculateDecimal.Round(0).IntPart())
|
||||||
totalTokens := promptTokens + completionTokens
|
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_token_count"] = audioTokens
|
||||||
other["audio_input_price"] = audioInputPrice
|
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{
|
model.RecordConsumeLog(ctx, relayInfo.UserId, model.RecordConsumeLogParams{
|
||||||
ChannelId: relayInfo.ChannelId,
|
ChannelId: relayInfo.ChannelId,
|
||||||
PromptTokens: promptTokens,
|
PromptTokens: promptTokens,
|
||||||
|
|||||||
@@ -10,6 +10,18 @@ const (
|
|||||||
FileSearchPrice = 2.5
|
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 (
|
const (
|
||||||
// Gemini Audio Input Price
|
// Gemini Audio Input Price
|
||||||
Gemini25FlashPreviewInputAudioPrice = 1.00
|
Gemini25FlashPreviewInputAudioPrice = 1.00
|
||||||
@@ -65,3 +77,31 @@ func GetGeminiInputAudioPricePerMillionTokens(modelName string) float64 {
|
|||||||
}
|
}
|
||||||
return 0
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1027,6 +1027,8 @@ export function renderModelPrice(
|
|||||||
audioInputSeperatePrice = false,
|
audioInputSeperatePrice = false,
|
||||||
audioInputTokens = 0,
|
audioInputTokens = 0,
|
||||||
audioInputPrice = 0,
|
audioInputPrice = 0,
|
||||||
|
imageGenerationCall = false,
|
||||||
|
imageGenerationCallPrice = 0,
|
||||||
) {
|
) {
|
||||||
const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
|
const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
|
||||||
groupRatio,
|
groupRatio,
|
||||||
@@ -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 +
|
||||||
|
(imageGenerationCallPrice * groupRatio);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -1131,7 +1134,13 @@ export function renderModelPrice(
|
|||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p></p>
|
{imageGenerationCall && imageGenerationCallPrice > 0 && (
|
||||||
|
<p>
|
||||||
|
{i18next.t('图片生成调用:${{price}} / 1次', {
|
||||||
|
price: imageGenerationCallPrice,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<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('');
|
].join('');
|
||||||
|
|
||||||
return i18next.t(
|
return i18next.t(
|
||||||
|
|||||||
@@ -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_generation_call || false,
|
||||||
|
other?.image_generation_call_price || 0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
expandDataLocal.push({
|
expandDataLocal.push({
|
||||||
|
|||||||
Reference in New Issue
Block a user