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
37d6925e
authored
Nov 07, 2024
by
CalciumIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 完善audio计费
parent
cdcf2184
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
172 additions
and
18 deletions
+172
-18
common/model-ratio.go
+1
-0
dto/openai_response.go
+5
-3
dto/realtime.go
+1
-0
relay/common/relay_info.go
+1
-0
relay/relay-text.go
+6
-1
service/log.go
+15
-1
service/quota.go
+69
-9
web/src/components/LogsTable.js
+21
-4
web/src/helpers/render.js
+53
-0
No files found.
common/model-ratio.go
View file @
37d6925e
...
...
@@ -54,6 +54,7 @@ var defaultModelRatio = map[string]float64{
"gpt-4-turbo"
:
5
,
// $0.01 / 1K tokens
"gpt-4-turbo-2024-04-09"
:
5
,
// $0.01 / 1K tokens
//"gpt-3.5-turbo-0301": 0.75, //deprecated
"gpt-3.5-turbo"
:
0.25
,
"gpt-3.5-turbo-0613"
:
0.75
,
"gpt-3.5-turbo-16k"
:
1.5
,
// $0.003 / 1K tokens
"gpt-3.5-turbo-16k-0613"
:
1.5
,
...
...
dto/openai_response.go
View file @
37d6925e
...
...
@@ -128,7 +128,9 @@ type CompletionsStreamResponse struct {
}
type
Usage
struct
{
PromptTokens
int
`json:"prompt_tokens"`
CompletionTokens
int
`json:"completion_tokens"`
TotalTokens
int
`json:"total_tokens"`
PromptTokens
int
`json:"prompt_tokens"`
CompletionTokens
int
`json:"completion_tokens"`
TotalTokens
int
`json:"total_tokens"`
PromptTokensDetails
InputTokenDetails
`json:"prompt_tokens_details"`
CompletionTokenDetails
OutputTokenDetails
`json:"completion_tokens_details"`
}
dto/realtime.go
View file @
37d6925e
...
...
@@ -47,6 +47,7 @@ type InputTokenDetails struct {
CachedTokens
int
`json:"cached_tokens"`
TextTokens
int
`json:"text_tokens"`
AudioTokens
int
`json:"audio_tokens"`
ImageTokens
int
`json:"image_tokens"`
}
type
OutputTokenDetails
struct
{
...
...
relay/common/relay_info.go
View file @
37d6925e
...
...
@@ -41,6 +41,7 @@ type RelayInfo struct {
OutputAudioFormat
string
RealtimeTools
[]
dto
.
RealTimeTool
IsFirstRequest
bool
AudioUsage
bool
}
func
GenRelayInfoWs
(
c
*
gin
.
Context
,
ws
*
websocket
.
Conn
)
*
RelayInfo
{
...
...
relay/relay-text.go
View file @
37d6925e
...
...
@@ -207,7 +207,12 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
service
.
ResetStatusCode
(
openaiErr
,
statusCodeMappingStr
)
return
openaiErr
}
postConsumeQuota
(
c
,
relayInfo
,
textRequest
.
Model
,
usage
.
(
*
dto
.
Usage
),
ratio
,
preConsumedQuota
,
userQuota
,
modelRatio
,
groupRatio
,
modelPrice
,
getModelPriceSuccess
,
""
)
if
strings
.
HasPrefix
(
relayInfo
.
UpstreamModelName
,
"gpt-4o-audio"
)
{
service
.
PostAudioConsumeQuota
(
c
,
relayInfo
,
usage
.
(
*
dto
.
Usage
),
ratio
,
preConsumedQuota
,
userQuota
,
modelRatio
,
groupRatio
,
modelPrice
,
getModelPriceSuccess
,
""
)
}
else
{
postConsumeQuota
(
c
,
relayInfo
,
textRequest
.
Model
,
usage
.
(
*
dto
.
Usage
),
ratio
,
preConsumedQuota
,
userQuota
,
modelRatio
,
groupRatio
,
modelPrice
,
getModelPriceSuccess
,
""
)
}
return
nil
}
...
...
service/log.go
View file @
37d6925e
...
...
@@ -19,12 +19,26 @@ func GenerateTextOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, m
return
other
}
func
GenerateWssOtherInfo
(
ctx
*
gin
.
Context
,
relayInfo
*
relaycommon
.
RelayInfo
,
usage
*
dto
.
RealtimeUsage
,
modelRatio
,
groupRatio
,
completionRatio
,
modelPrice
float64
)
map
[
string
]
interface
{}
{
func
GenerateWssOtherInfo
(
ctx
*
gin
.
Context
,
relayInfo
*
relaycommon
.
RelayInfo
,
usage
*
dto
.
RealtimeUsage
,
modelRatio
,
groupRatio
,
completionRatio
,
audioRatio
,
audioCompletionRatio
,
modelPrice
float64
)
map
[
string
]
interface
{}
{
info
:=
GenerateTextOtherInfo
(
ctx
,
relayInfo
,
modelRatio
,
groupRatio
,
completionRatio
,
modelPrice
)
info
[
"ws"
]
=
true
info
[
"audio_input"
]
=
usage
.
InputTokenDetails
.
AudioTokens
info
[
"audio_output"
]
=
usage
.
OutputTokenDetails
.
AudioTokens
info
[
"text_input"
]
=
usage
.
InputTokenDetails
.
TextTokens
info
[
"text_output"
]
=
usage
.
OutputTokenDetails
.
TextTokens
info
[
"audio_ratio"
]
=
audioRatio
info
[
"audio_completion_ratio"
]
=
audioCompletionRatio
return
info
}
func
GenerateAudioOtherInfo
(
ctx
*
gin
.
Context
,
relayInfo
*
relaycommon
.
RelayInfo
,
usage
*
dto
.
Usage
,
modelRatio
,
groupRatio
,
completionRatio
,
audioRatio
,
audioCompletionRatio
,
modelPrice
float64
)
map
[
string
]
interface
{}
{
info
:=
GenerateTextOtherInfo
(
ctx
,
relayInfo
,
modelRatio
,
groupRatio
,
completionRatio
,
modelPrice
)
info
[
"audio"
]
=
true
info
[
"audio_input"
]
=
usage
.
PromptTokensDetails
.
AudioTokens
info
[
"audio_output"
]
=
usage
.
CompletionTokenDetails
.
AudioTokens
info
[
"text_input"
]
=
usage
.
PromptTokensDetails
.
TextTokens
info
[
"text_output"
]
=
usage
.
CompletionTokenDetails
.
TextTokens
info
[
"audio_ratio"
]
=
audioRatio
info
[
"audio_completion_ratio"
]
=
audioCompletionRatio
return
info
}
service/quota.go
View file @
37d6925e
...
...
@@ -9,7 +9,6 @@ import (
"one-api/dto"
"one-api/model"
relaycommon
"one-api/relay/common"
"strings"
"time"
)
...
...
@@ -124,18 +123,79 @@ func PostWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, mod
}
logModel
:=
modelName
if
strings
.
HasPrefix
(
logModel
,
"gpt-4-gizmo"
)
{
logModel
=
"gpt-4-gizmo-*"
logContent
+=
fmt
.
Sprintf
(
",模型 %s"
,
modelName
)
if
extraContent
!=
""
{
logContent
+=
", "
+
extraContent
}
other
:=
GenerateWssOtherInfo
(
ctx
,
relayInfo
,
usage
,
modelRatio
,
groupRatio
,
completionRatio
,
audioRatio
,
audioCompletionRatio
,
modelPrice
)
model
.
RecordConsumeLog
(
ctx
,
relayInfo
.
UserId
,
relayInfo
.
ChannelId
,
usage
.
InputTokens
,
usage
.
OutputTokens
,
logModel
,
tokenName
,
quota
,
logContent
,
relayInfo
.
TokenId
,
userQuota
,
int
(
useTimeSeconds
),
relayInfo
.
IsStream
,
other
)
}
func
PostAudioConsumeQuota
(
ctx
*
gin
.
Context
,
relayInfo
*
relaycommon
.
RelayInfo
,
usage
*
dto
.
Usage
,
ratio
float64
,
preConsumedQuota
int
,
userQuota
int
,
modelRatio
float64
,
groupRatio
float64
,
modelPrice
float64
,
usePrice
bool
,
extraContent
string
)
{
useTimeSeconds
:=
time
.
Now
()
.
Unix
()
-
relayInfo
.
StartTime
.
Unix
()
textInputTokens
:=
usage
.
PromptTokensDetails
.
TextTokens
textOutTokens
:=
usage
.
CompletionTokenDetails
.
TextTokens
audioInputTokens
:=
usage
.
PromptTokensDetails
.
AudioTokens
audioOutTokens
:=
usage
.
CompletionTokenDetails
.
AudioTokens
tokenName
:=
ctx
.
GetString
(
"token_name"
)
completionRatio
:=
common
.
GetCompletionRatio
(
relayInfo
.
UpstreamModelName
)
audioRatio
:=
common
.
GetAudioRatio
(
relayInfo
.
UpstreamModelName
)
audioCompletionRatio
:=
common
.
GetAudioCompletionRatio
(
relayInfo
.
UpstreamModelName
)
quota
:=
0
if
!
usePrice
{
quota
=
int
(
math
.
Round
(
float64
(
textInputTokens
)
+
float64
(
textOutTokens
)
*
completionRatio
))
quota
+=
int
(
math
.
Round
(
float64
(
audioInputTokens
)
*
audioRatio
+
float64
(
audioOutTokens
)
*
audioRatio
*
audioCompletionRatio
))
quota
=
int
(
math
.
Round
(
float64
(
quota
)
*
ratio
))
if
ratio
!=
0
&&
quota
<=
0
{
quota
=
1
}
}
else
{
quota
=
int
(
modelPrice
*
common
.
QuotaPerUnit
*
groupRatio
)
}
totalTokens
:=
usage
.
TotalTokens
var
logContent
string
if
!
usePrice
{
logContent
=
fmt
.
Sprintf
(
"模型倍率 %.2f,补全倍率 %.2f,音频倍率 %.2f,音频补全倍率 %.2f,分组倍率 %.2f"
,
modelRatio
,
completionRatio
,
audioRatio
,
audioCompletionRatio
,
groupRatio
)
}
else
{
logContent
=
fmt
.
Sprintf
(
"模型价格 %.2f,分组倍率 %.2f"
,
modelPrice
,
groupRatio
)
}
if
strings
.
HasPrefix
(
logModel
,
"gpt-4o-gizmo"
)
{
logModel
=
"gpt-4o-gizmo-*"
logContent
+=
fmt
.
Sprintf
(
",模型 %s"
,
modelName
)
// record all the consume log even if quota is 0
if
totalTokens
==
0
{
// in this case, must be some error happened
// we cannot just return, because we may have to return the pre-consumed quota
quota
=
0
logContent
+=
fmt
.
Sprintf
(
"(可能是上游超时)"
)
common
.
LogError
(
ctx
,
fmt
.
Sprintf
(
"total tokens is 0, cannot consume quota, userId %d, channelId %d, "
+
"tokenId %d, model %s, pre-consumed quota %d"
,
relayInfo
.
UserId
,
relayInfo
.
ChannelId
,
relayInfo
.
TokenId
,
relayInfo
.
UpstreamModelName
,
preConsumedQuota
))
}
else
{
quotaDelta
:=
quota
-
preConsumedQuota
if
quotaDelta
!=
0
{
err
:=
model
.
PostConsumeTokenQuota
(
relayInfo
,
userQuota
,
quotaDelta
,
preConsumedQuota
,
true
)
if
err
!=
nil
{
common
.
LogError
(
ctx
,
"error consuming token remain quota: "
+
err
.
Error
())
}
}
err
:=
model
.
CacheUpdateUserQuota
(
relayInfo
.
UserId
)
if
err
!=
nil
{
common
.
LogError
(
ctx
,
"error update user quota cache: "
+
err
.
Error
())
}
model
.
UpdateUserUsedQuotaAndRequestCount
(
relayInfo
.
UserId
,
quota
)
model
.
UpdateChannelUsedQuota
(
relayInfo
.
ChannelId
,
quota
)
}
logModel
:=
relayInfo
.
UpstreamModelName
if
extraContent
!=
""
{
logContent
+=
", "
+
extraContent
}
other
:=
Generate
WssOtherInfo
(
ctx
,
relayInfo
,
usage
,
modelRatio
,
groupRatio
,
c
ompletionRatio
,
modelPrice
)
model
.
RecordConsumeLog
(
ctx
,
relayInfo
.
UserId
,
relayInfo
.
ChannelId
,
usage
.
InputTokens
,
usage
.
Output
Tokens
,
logModel
,
other
:=
Generate
AudioOtherInfo
(
ctx
,
relayInfo
,
usage
,
modelRatio
,
groupRatio
,
completionRatio
,
audioRatio
,
audioC
ompletionRatio
,
modelPrice
)
model
.
RecordConsumeLog
(
ctx
,
relayInfo
.
UserId
,
relayInfo
.
ChannelId
,
usage
.
PromptTokens
,
usage
.
Completion
Tokens
,
logModel
,
tokenName
,
quota
,
logContent
,
relayInfo
.
TokenId
,
userQuota
,
int
(
useTimeSeconds
),
relayInfo
.
IsStream
,
other
)
}
web/src/components/LogsTable.js
View file @
37d6925e
...
...
@@ -24,10 +24,11 @@ import {
}
from
'@douyinfe/semi-ui'
;
import
{
ITEMS_PER_PAGE
}
from
'../constants'
;
import
{
renderAudioModelPrice
,
renderModelPrice
,
renderNumber
,
renderQuota
,
stringToColor
,
stringToColor
}
from
'../helpers/render'
;
import
Paragraph
from
'@douyinfe/semi-ui/lib/es/typography/paragraph'
;
import
{
getLogOther
}
from
'../helpers/other.js'
;
...
...
@@ -537,7 +538,7 @@ const LogsTable = () => {
// value: content,
// })
}
if
(
other
?.
ws
)
{
if
(
other
?.
ws
||
other
?.
audio
)
{
expandDataLocal
.
push
({
key
:
'语音输入'
,
value
:
other
.
audio_input
,
...
...
@@ -560,14 +561,30 @@ const LogsTable = () => {
value
:
logs
[
i
].
content
,
})
if
(
logs
[
i
].
type
===
2
)
{
let
content
=
renderModelPrice
(
let
content
=
''
;
if
(
other
?.
ws
||
other
?.
audio
)
{
content
=
renderAudioModelPrice
(
other
.
text_input
,
other
.
text_output
,
other
.
model_ratio
,
other
.
model_price
,
other
.
completion_ratio
,
other
.
audio_input
,
other
.
audio_output
,
other
?.
audio_ratio
,
other
?.
audio_completion_ratio
,
other
.
group_ratio
,
);
}
else
{
content
=
renderModelPrice
(
logs
[
i
].
prompt_tokens
,
logs
[
i
].
completion_tokens
,
other
.
model_ratio
,
other
.
model_price
,
other
.
completion_ratio
,
other
.
group_ratio
,
);
);
}
expandDataLocal
.
push
({
key
:
'计费过程'
,
value
:
content
,
...
...
web/src/helpers/render.js
View file @
37d6925e
...
...
@@ -173,6 +173,59 @@ export function renderModelPrice(
}
}
export
function
renderAudioModelPrice
(
inputTokens
,
completionTokens
,
modelRatio
,
modelPrice
=
-
1
,
completionRatio
,
audioInputTokens
,
audioCompletionTokens
,
audioRatio
,
audioCompletionRatio
,
groupRatio
,
)
{
// 1 ratio = $0.002 / 1K tokens
if
(
modelPrice
!==
-
1
)
{
return
'模型价格:$'
+
modelPrice
+
' * 分组倍率:'
+
groupRatio
+
' = $'
+
modelPrice
*
groupRatio
;
}
else
{
if
(
completionRatio
===
undefined
)
{
completionRatio
=
0
;
}
// 这里的 *2 是因为 1倍率=0.002刀,请勿删除
let
inputRatioPrice
=
modelRatio
*
2.0
;
let
completionRatioPrice
=
modelRatio
*
2.0
*
completionRatio
;
let
price
=
(
inputTokens
/
1000000
)
*
inputRatioPrice
*
groupRatio
+
(
completionTokens
/
1000000
)
*
completionRatioPrice
*
groupRatio
+
(
audioInputTokens
/
1000000
)
*
inputRatioPrice
*
audioRatio
*
groupRatio
+
(
audioCompletionTokens
/
1000000
)
*
inputRatioPrice
*
audioRatio
*
audioCompletionRatio
*
groupRatio
;
return
(
<>
<
article
>
<
p
>
提示:
$
{
inputRatioPrice
}
*
{
groupRatio
}
=
$
{
inputRatioPrice
*
groupRatio
}
/ 1M tokens</
p
>
<
p
>
补全:
$
{
completionRatioPrice
}
*
{
groupRatio
}
=
$
{
completionRatioPrice
*
groupRatio
}
/ 1M tokens</
p
>
<
p
>
音频提示:
$
{
inputRatioPrice
}
*
{
groupRatio
}
*
{
audioRatio
}
=
$
{
inputRatioPrice
*
audioRatio
*
groupRatio
}
/ 1M tokens</
p
>
<
p
>
音频补全:
$
{
inputRatioPrice
}
*
{
groupRatio
}
*
{
audioRatio
}
*
{
audioCompletionRatio
}
=
$
{
inputRatioPrice
*
audioRatio
*
audioCompletionRatio
*
groupRatio
}
/ 1M tokens</
p
>
<
p
><
/p
>
<
p
>
提示
{
inputTokens
}
tokens
/
1
M
tokens
*
$
{
inputRatioPrice
}
+
补全
{
' '
}
{
completionTokens
}
tokens
/
1
M
tokens
*
$
{
completionRatioPrice
}
+
<
/p
>
<
p
>
音频提示
{
audioInputTokens
}
tokens
/
1
M
tokens
*
$
{
inputRatioPrice
}
*
{
audioRatio
}
+
音频补全
{
audioCompletionTokens
}
tokens
/
1
M
tokens
*
$
{
inputRatioPrice
}
*
{
audioRatio
}
*
{
audioCompletionRatio
}
<
/p
>
<
p
>
(文字
+
音频)
*
分组
{
groupRatio
}
=
$
{
price
.
toFixed
(
6
)}
<
/p
>
<
p
>
仅供参考,以实际扣费为准
<
/p
>
<
/article
>
<
/
>
);
}
}
export
function
renderQuotaWithPrompt
(
quota
,
digits
)
{
let
displayInCurrency
=
localStorage
.
getItem
(
'display_in_currency'
);
displayInCurrency
=
displayInCurrency
===
'true'
;
...
...
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