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
645b19b9
authored
May 07, 2025
by
CaIon
Browse files
Options
Browse Files
Download
Plain Diff
Merge remote-tracking branch 'origin/main'
parents
062dbb10
2cbedaf6
Show whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
505 additions
and
138 deletions
+505
-138
controller/user.go
+25
-1
dto/openai_response.go
+9
-4
middleware/model-rate-limit.go
+2
-0
model/user.go
+1
-0
relay/channel/openai/adaptor.go
+1
-1
relay/channel/openai/helper.go
+7
-0
relay/channel/openai/relay-openai.go
+0
-99
relay/channel/openai/relay_responses.go
+119
-0
relay/channel/vertex/adaptor.go
+1
-1
relay/common/relay_info.go
+37
-0
relay/helper/model_mapped.go
+32
-3
relay/relay-responses.go
+5
-5
relay/relay-text.go
+46
-0
setting/operation_setting/tools.go
+57
-0
web/src/components/LogsTable.js
+13
-1
web/src/components/PersonalSetting.js
+46
-9
web/src/helpers/render.js
+104
-14
No files found.
controller/user.go
View file @
645b19b9
...
@@ -592,7 +592,14 @@ func UpdateSelf(c *gin.Context) {
...
@@ -592,7 +592,14 @@ func UpdateSelf(c *gin.Context) {
user
.
Password
=
""
// rollback to what it should be
user
.
Password
=
""
// rollback to what it should be
cleanUser
.
Password
=
""
cleanUser
.
Password
=
""
}
}
updatePassword
:=
user
.
Password
!=
""
updatePassword
,
err
:=
checkUpdatePassword
(
user
.
OriginalPassword
,
user
.
Password
,
cleanUser
.
Id
)
if
err
!=
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"message"
:
err
.
Error
(),
})
return
}
if
err
:=
cleanUser
.
Update
(
updatePassword
);
err
!=
nil
{
if
err
:=
cleanUser
.
Update
(
updatePassword
);
err
!=
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"success"
:
false
,
...
@@ -608,6 +615,23 @@ func UpdateSelf(c *gin.Context) {
...
@@ -608,6 +615,23 @@ func UpdateSelf(c *gin.Context) {
return
return
}
}
func
checkUpdatePassword
(
originalPassword
string
,
newPassword
string
,
userId
int
)
(
updatePassword
bool
,
err
error
)
{
var
currentUser
*
model
.
User
currentUser
,
err
=
model
.
GetUserById
(
userId
,
true
)
if
err
!=
nil
{
return
}
if
!
common
.
ValidatePasswordAndHash
(
originalPassword
,
currentUser
.
Password
)
{
err
=
fmt
.
Errorf
(
"原密码错误"
)
return
}
if
newPassword
==
""
{
return
}
updatePassword
=
true
return
}
func
DeleteUser
(
c
*
gin
.
Context
)
{
func
DeleteUser
(
c
*
gin
.
Context
)
{
id
,
err
:=
strconv
.
Atoi
(
c
.
Param
(
"id"
))
id
,
err
:=
strconv
.
Atoi
(
c
.
Param
(
"id"
))
if
err
!=
nil
{
if
err
!=
nil
{
...
...
dto/openai_response.go
View file @
645b19b9
...
@@ -211,7 +211,7 @@ type OpenAIResponsesResponse struct {
...
@@ -211,7 +211,7 @@ type OpenAIResponsesResponse struct {
Store
bool
`json:"store"`
Store
bool
`json:"store"`
Temperature
float64
`json:"temperature"`
Temperature
float64
`json:"temperature"`
ToolChoice
string
`json:"tool_choice"`
ToolChoice
string
`json:"tool_choice"`
Tools
[]
interface
{}
`json:"tools"`
Tools
[]
ResponsesToolsCall
`json:"tools"`
TopP
float64
`json:"top_p"`
TopP
float64
`json:"top_p"`
Truncation
string
`json:"truncation"`
Truncation
string
`json:"truncation"`
Usage
*
Usage
`json:"usage"`
Usage
*
Usage
`json:"usage"`
...
@@ -238,8 +238,12 @@ type ResponsesOutputContent struct {
...
@@ -238,8 +238,12 @@ type ResponsesOutputContent struct {
}
}
const
(
const
(
BuildInTools_WebSearch
=
"web_search_preview"
BuildInToolWebSearchPreview
=
"web_search_preview"
BuildInTools_FileSearch
=
"file_search"
BuildInToolFileSearch
=
"file_search"
)
const
(
BuildInCallWebSearchCall
=
"web_search_call"
)
)
const
(
const
(
...
@@ -250,6 +254,7 @@ const (
...
@@ -250,6 +254,7 @@ const (
// ResponsesStreamResponse 用于处理 /v1/responses 流式响应
// ResponsesStreamResponse 用于处理 /v1/responses 流式响应
type
ResponsesStreamResponse
struct
{
type
ResponsesStreamResponse
struct
{
Type
string
`json:"type"`
Type
string
`json:"type"`
Response
*
OpenAIResponsesResponse
`json:"response"`
Response
*
OpenAIResponsesResponse
`json:"response
,omitempty
"`
Delta
string
`json:"delta,omitempty"`
Delta
string
`json:"delta,omitempty"`
Item
*
ResponsesOutput
`json:"item,omitempty"`
}
}
middleware/model-rate-limit.go
View file @
645b19b9
...
@@ -93,6 +93,7 @@ func redisRateLimitHandler(duration int64, totalMaxCount, successMaxCount int) g
...
@@ -93,6 +93,7 @@ func redisRateLimitHandler(duration int64, totalMaxCount, successMaxCount int) g
}
}
//2.检查总请求数限制并记录总请求(当totalMaxCount为0时会自动跳过,使用令牌桶限流器
//2.检查总请求数限制并记录总请求(当totalMaxCount为0时会自动跳过,使用令牌桶限流器
if
totalMaxCount
>
0
{
totalKey
:=
fmt
.
Sprintf
(
"rateLimit:%s"
,
userId
)
totalKey
:=
fmt
.
Sprintf
(
"rateLimit:%s"
,
userId
)
// 初始化
// 初始化
tb
:=
limiter
.
New
(
ctx
,
rdb
)
tb
:=
limiter
.
New
(
ctx
,
rdb
)
...
@@ -113,6 +114,7 @@ func redisRateLimitHandler(duration int64, totalMaxCount, successMaxCount int) g
...
@@ -113,6 +114,7 @@ func redisRateLimitHandler(duration int64, totalMaxCount, successMaxCount int) g
if
!
allowed
{
if
!
allowed
{
abortWithOpenAiMessage
(
c
,
http
.
StatusTooManyRequests
,
fmt
.
Sprintf
(
"您已达到总请求数限制:%d分钟内最多请求%d次,包括失败次数,请检查您的请求是否正确"
,
setting
.
ModelRequestRateLimitDurationMinutes
,
totalMaxCount
))
abortWithOpenAiMessage
(
c
,
http
.
StatusTooManyRequests
,
fmt
.
Sprintf
(
"您已达到总请求数限制:%d分钟内最多请求%d次,包括失败次数,请检查您的请求是否正确"
,
setting
.
ModelRequestRateLimitDurationMinutes
,
totalMaxCount
))
}
}
}
// 4. 处理请求
// 4. 处理请求
c
.
Next
()
c
.
Next
()
...
...
model/user.go
View file @
645b19b9
...
@@ -18,6 +18,7 @@ type User struct {
...
@@ -18,6 +18,7 @@ type User struct {
Id
int
`json:"id"`
Id
int
`json:"id"`
Username
string
`json:"username" gorm:"unique;index" validate:"max=12"`
Username
string
`json:"username" gorm:"unique;index" validate:"max=12"`
Password
string
`json:"password" gorm:"not null;" validate:"min=8,max=20"`
Password
string
`json:"password" gorm:"not null;" validate:"min=8,max=20"`
OriginalPassword
string
`json:"original_password" gorm:"-:all"`
// this field is only for Password change verification, don't save it to database!
DisplayName
string
`json:"display_name" gorm:"index" validate:"max=20"`
DisplayName
string
`json:"display_name" gorm:"index" validate:"max=20"`
Role
int
`json:"role" gorm:"type:int;default:1"`
// admin, common
Role
int
`json:"role" gorm:"type:int;default:1"`
// admin, common
Status
int
`json:"status" gorm:"type:int;default:1"`
// enabled, disabled
Status
int
`json:"status" gorm:"type:int;default:1"`
// enabled, disabled
...
...
relay/channel/openai/adaptor.go
View file @
645b19b9
...
@@ -429,7 +429,7 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
...
@@ -429,7 +429,7 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom
if
info
.
IsStream
{
if
info
.
IsStream
{
err
,
usage
=
OaiResponsesStreamHandler
(
c
,
resp
,
info
)
err
,
usage
=
OaiResponsesStreamHandler
(
c
,
resp
,
info
)
}
else
{
}
else
{
err
,
usage
=
O
pen
aiResponsesHandler
(
c
,
resp
,
info
)
err
,
usage
=
OaiResponsesHandler
(
c
,
resp
,
info
)
}
}
default
:
default
:
if
info
.
IsStream
{
if
info
.
IsStream
{
...
...
relay/channel/openai/helper.go
View file @
645b19b9
...
@@ -187,3 +187,10 @@ func handleFinalResponse(c *gin.Context, info *relaycommon.RelayInfo, lastStream
...
@@ -187,3 +187,10 @@ func handleFinalResponse(c *gin.Context, info *relaycommon.RelayInfo, lastStream
}
}
}
}
}
}
func
sendResponsesStreamData
(
c
*
gin
.
Context
,
streamResponse
dto
.
ResponsesStreamResponse
,
data
string
)
{
if
data
==
""
{
return
}
helper
.
ResponseChunkData
(
c
,
streamResponse
,
data
)
}
relay/channel/openai/relay-openai.go
View file @
645b19b9
...
@@ -644,102 +644,3 @@ func OpenaiHandlerWithUsage(c *gin.Context, resp *http.Response, info *relaycomm
...
@@ -644,102 +644,3 @@ func OpenaiHandlerWithUsage(c *gin.Context, resp *http.Response, info *relaycomm
}
}
return
nil
,
&
usageResp
.
Usage
return
nil
,
&
usageResp
.
Usage
}
}
func
OpenaiResponsesHandler
(
c
*
gin
.
Context
,
resp
*
http
.
Response
,
info
*
relaycommon
.
RelayInfo
)
(
*
dto
.
OpenAIErrorWithStatusCode
,
*
dto
.
Usage
)
{
// read response body
var
responsesResponse
dto
.
OpenAIResponsesResponse
responseBody
,
err
:=
io
.
ReadAll
(
resp
.
Body
)
if
err
!=
nil
{
return
service
.
OpenAIErrorWrapper
(
err
,
"read_response_body_failed"
,
http
.
StatusInternalServerError
),
nil
}
err
=
resp
.
Body
.
Close
()
if
err
!=
nil
{
return
service
.
OpenAIErrorWrapper
(
err
,
"close_response_body_failed"
,
http
.
StatusInternalServerError
),
nil
}
err
=
common
.
DecodeJson
(
responseBody
,
&
responsesResponse
)
if
err
!=
nil
{
return
service
.
OpenAIErrorWrapper
(
err
,
"unmarshal_response_body_failed"
,
http
.
StatusInternalServerError
),
nil
}
if
responsesResponse
.
Error
!=
nil
{
return
&
dto
.
OpenAIErrorWithStatusCode
{
Error
:
dto
.
OpenAIError
{
Message
:
responsesResponse
.
Error
.
Message
,
Type
:
"openai_error"
,
Code
:
responsesResponse
.
Error
.
Code
,
},
StatusCode
:
resp
.
StatusCode
,
},
nil
}
// reset response body
resp
.
Body
=
io
.
NopCloser
(
bytes
.
NewBuffer
(
responseBody
))
// We shouldn't set the header before we parse the response body, because the parse part may fail.
// And then we will have to send an error response, but in this case, the header has already been set.
// So the httpClient will be confused by the response.
// For example, Postman will report error, and we cannot check the response at all.
for
k
,
v
:=
range
resp
.
Header
{
c
.
Writer
.
Header
()
.
Set
(
k
,
v
[
0
])
}
c
.
Writer
.
WriteHeader
(
resp
.
StatusCode
)
// copy response body
_
,
err
=
io
.
Copy
(
c
.
Writer
,
resp
.
Body
)
if
err
!=
nil
{
common
.
SysError
(
"error copying response body: "
+
err
.
Error
())
}
resp
.
Body
.
Close
()
// compute usage
usage
:=
dto
.
Usage
{}
usage
.
PromptTokens
=
responsesResponse
.
Usage
.
InputTokens
usage
.
CompletionTokens
=
responsesResponse
.
Usage
.
OutputTokens
usage
.
TotalTokens
=
responsesResponse
.
Usage
.
TotalTokens
return
nil
,
&
usage
}
func
OaiResponsesStreamHandler
(
c
*
gin
.
Context
,
resp
*
http
.
Response
,
info
*
relaycommon
.
RelayInfo
)
(
*
dto
.
OpenAIErrorWithStatusCode
,
*
dto
.
Usage
)
{
if
resp
==
nil
||
resp
.
Body
==
nil
{
common
.
LogError
(
c
,
"invalid response or response body"
)
return
service
.
OpenAIErrorWrapper
(
fmt
.
Errorf
(
"invalid response"
),
"invalid_response"
,
http
.
StatusInternalServerError
),
nil
}
var
usage
=
&
dto
.
Usage
{}
var
responseTextBuilder
strings
.
Builder
helper
.
StreamScannerHandler
(
c
,
resp
,
info
,
func
(
data
string
)
bool
{
// 检查当前数据是否包含 completed 状态和 usage 信息
var
streamResponse
dto
.
ResponsesStreamResponse
if
err
:=
common
.
DecodeJsonStr
(
data
,
&
streamResponse
);
err
==
nil
{
sendResponsesStreamData
(
c
,
streamResponse
,
data
)
switch
streamResponse
.
Type
{
case
"response.completed"
:
usage
.
PromptTokens
=
streamResponse
.
Response
.
Usage
.
InputTokens
usage
.
CompletionTokens
=
streamResponse
.
Response
.
Usage
.
OutputTokens
usage
.
TotalTokens
=
streamResponse
.
Response
.
Usage
.
TotalTokens
case
"response.output_text.delta"
:
// 处理输出文本
responseTextBuilder
.
WriteString
(
streamResponse
.
Delta
)
}
}
return
true
})
if
usage
.
CompletionTokens
==
0
{
// 计算输出文本的 token 数量
tempStr
:=
responseTextBuilder
.
String
()
if
len
(
tempStr
)
>
0
{
// 非正常结束,使用输出文本的 token 数量
completionTokens
,
_
:=
service
.
CountTextToken
(
tempStr
,
info
.
UpstreamModelName
)
usage
.
CompletionTokens
=
completionTokens
}
}
return
nil
,
usage
}
func
sendResponsesStreamData
(
c
*
gin
.
Context
,
streamResponse
dto
.
ResponsesStreamResponse
,
data
string
)
{
if
data
==
""
{
return
}
helper
.
ResponseChunkData
(
c
,
streamResponse
,
data
)
}
relay/channel/openai/relay_responses.go
0 → 100644
View file @
645b19b9
package
openai
import
(
"bytes"
"fmt"
"io"
"net/http"
"one-api/common"
"one-api/dto"
relaycommon
"one-api/relay/common"
"one-api/relay/helper"
"one-api/service"
"strings"
"github.com/gin-gonic/gin"
)
func
OaiResponsesHandler
(
c
*
gin
.
Context
,
resp
*
http
.
Response
,
info
*
relaycommon
.
RelayInfo
)
(
*
dto
.
OpenAIErrorWithStatusCode
,
*
dto
.
Usage
)
{
// read response body
var
responsesResponse
dto
.
OpenAIResponsesResponse
responseBody
,
err
:=
io
.
ReadAll
(
resp
.
Body
)
if
err
!=
nil
{
return
service
.
OpenAIErrorWrapper
(
err
,
"read_response_body_failed"
,
http
.
StatusInternalServerError
),
nil
}
err
=
resp
.
Body
.
Close
()
if
err
!=
nil
{
return
service
.
OpenAIErrorWrapper
(
err
,
"close_response_body_failed"
,
http
.
StatusInternalServerError
),
nil
}
err
=
common
.
DecodeJson
(
responseBody
,
&
responsesResponse
)
if
err
!=
nil
{
return
service
.
OpenAIErrorWrapper
(
err
,
"unmarshal_response_body_failed"
,
http
.
StatusInternalServerError
),
nil
}
if
responsesResponse
.
Error
!=
nil
{
return
&
dto
.
OpenAIErrorWithStatusCode
{
Error
:
dto
.
OpenAIError
{
Message
:
responsesResponse
.
Error
.
Message
,
Type
:
"openai_error"
,
Code
:
responsesResponse
.
Error
.
Code
,
},
StatusCode
:
resp
.
StatusCode
,
},
nil
}
// reset response body
resp
.
Body
=
io
.
NopCloser
(
bytes
.
NewBuffer
(
responseBody
))
// We shouldn't set the header before we parse the response body, because the parse part may fail.
// And then we will have to send an error response, but in this case, the header has already been set.
// So the httpClient will be confused by the response.
// For example, Postman will report error, and we cannot check the response at all.
for
k
,
v
:=
range
resp
.
Header
{
c
.
Writer
.
Header
()
.
Set
(
k
,
v
[
0
])
}
c
.
Writer
.
WriteHeader
(
resp
.
StatusCode
)
// copy response body
_
,
err
=
io
.
Copy
(
c
.
Writer
,
resp
.
Body
)
if
err
!=
nil
{
common
.
SysError
(
"error copying response body: "
+
err
.
Error
())
}
resp
.
Body
.
Close
()
// compute usage
usage
:=
dto
.
Usage
{}
usage
.
PromptTokens
=
responsesResponse
.
Usage
.
InputTokens
usage
.
CompletionTokens
=
responsesResponse
.
Usage
.
OutputTokens
usage
.
TotalTokens
=
responsesResponse
.
Usage
.
TotalTokens
// 解析 Tools 用量
for
_
,
tool
:=
range
responsesResponse
.
Tools
{
info
.
ResponsesUsageInfo
.
BuiltInTools
[
tool
.
Type
]
.
CallCount
++
}
return
nil
,
&
usage
}
func
OaiResponsesStreamHandler
(
c
*
gin
.
Context
,
resp
*
http
.
Response
,
info
*
relaycommon
.
RelayInfo
)
(
*
dto
.
OpenAIErrorWithStatusCode
,
*
dto
.
Usage
)
{
if
resp
==
nil
||
resp
.
Body
==
nil
{
common
.
LogError
(
c
,
"invalid response or response body"
)
return
service
.
OpenAIErrorWrapper
(
fmt
.
Errorf
(
"invalid response"
),
"invalid_response"
,
http
.
StatusInternalServerError
),
nil
}
var
usage
=
&
dto
.
Usage
{}
var
responseTextBuilder
strings
.
Builder
helper
.
StreamScannerHandler
(
c
,
resp
,
info
,
func
(
data
string
)
bool
{
// 检查当前数据是否包含 completed 状态和 usage 信息
var
streamResponse
dto
.
ResponsesStreamResponse
if
err
:=
common
.
DecodeJsonStr
(
data
,
&
streamResponse
);
err
==
nil
{
sendResponsesStreamData
(
c
,
streamResponse
,
data
)
switch
streamResponse
.
Type
{
case
"response.completed"
:
usage
.
PromptTokens
=
streamResponse
.
Response
.
Usage
.
InputTokens
usage
.
CompletionTokens
=
streamResponse
.
Response
.
Usage
.
OutputTokens
usage
.
TotalTokens
=
streamResponse
.
Response
.
Usage
.
TotalTokens
case
"response.output_text.delta"
:
// 处理输出文本
responseTextBuilder
.
WriteString
(
streamResponse
.
Delta
)
case
dto
.
ResponsesOutputTypeItemDone
:
// 函数调用处理
if
streamResponse
.
Item
!=
nil
{
switch
streamResponse
.
Item
.
Type
{
case
dto
.
BuildInCallWebSearchCall
:
info
.
ResponsesUsageInfo
.
BuiltInTools
[
dto
.
BuildInToolWebSearchPreview
]
.
CallCount
++
}
}
}
}
return
true
})
if
usage
.
CompletionTokens
==
0
{
// 计算输出文本的 token 数量
tempStr
:=
responseTextBuilder
.
String
()
if
len
(
tempStr
)
>
0
{
// 非正常结束,使用输出文本的 token 数量
completionTokens
,
_
:=
service
.
CountTextToken
(
tempStr
,
info
.
UpstreamModelName
)
usage
.
CompletionTokens
=
completionTokens
}
}
return
nil
,
usage
}
relay/channel/vertex/adaptor.go
View file @
645b19b9
...
@@ -11,8 +11,8 @@ import (
...
@@ -11,8 +11,8 @@ import (
"one-api/relay/channel/claude"
"one-api/relay/channel/claude"
"one-api/relay/channel/gemini"
"one-api/relay/channel/gemini"
"one-api/relay/channel/openai"
"one-api/relay/channel/openai"
"one-api/setting/model_setting"
relaycommon
"one-api/relay/common"
relaycommon
"one-api/relay/common"
"one-api/setting/model_setting"
"strings"
"strings"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin"
...
...
relay/common/relay_info.go
View file @
645b19b9
...
@@ -36,6 +36,7 @@ type ClaudeConvertInfo struct {
...
@@ -36,6 +36,7 @@ type ClaudeConvertInfo struct {
const
(
const
(
RelayFormatOpenAI
=
"openai"
RelayFormatOpenAI
=
"openai"
RelayFormatClaude
=
"claude"
RelayFormatClaude
=
"claude"
RelayFormatGemini
=
"gemini"
)
)
type
RerankerInfo
struct
{
type
RerankerInfo
struct
{
...
@@ -43,6 +44,16 @@ type RerankerInfo struct {
...
@@ -43,6 +44,16 @@ type RerankerInfo struct {
ReturnDocuments
bool
ReturnDocuments
bool
}
}
type
BuildInToolInfo
struct
{
ToolName
string
CallCount
int
SearchContextSize
string
}
type
ResponsesUsageInfo
struct
{
BuiltInTools
map
[
string
]
*
BuildInToolInfo
}
type
RelayInfo
struct
{
type
RelayInfo
struct
{
ChannelType
int
ChannelType
int
ChannelId
int
ChannelId
int
...
@@ -90,6 +101,7 @@ type RelayInfo struct {
...
@@ -90,6 +101,7 @@ type RelayInfo struct {
ThinkingContentInfo
ThinkingContentInfo
*
ClaudeConvertInfo
*
ClaudeConvertInfo
*
RerankerInfo
*
RerankerInfo
*
ResponsesUsageInfo
}
}
// 定义支持流式选项的通道类型
// 定义支持流式选项的通道类型
...
@@ -135,6 +147,31 @@ func GenRelayInfoRerank(c *gin.Context, req *dto.RerankRequest) *RelayInfo {
...
@@ -135,6 +147,31 @@ func GenRelayInfoRerank(c *gin.Context, req *dto.RerankRequest) *RelayInfo {
return
info
return
info
}
}
func
GenRelayInfoResponses
(
c
*
gin
.
Context
,
req
*
dto
.
OpenAIResponsesRequest
)
*
RelayInfo
{
info
:=
GenRelayInfo
(
c
)
info
.
RelayMode
=
relayconstant
.
RelayModeResponses
info
.
ResponsesUsageInfo
=
&
ResponsesUsageInfo
{
BuiltInTools
:
make
(
map
[
string
]
*
BuildInToolInfo
),
}
if
len
(
req
.
Tools
)
>
0
{
for
_
,
tool
:=
range
req
.
Tools
{
info
.
ResponsesUsageInfo
.
BuiltInTools
[
tool
.
Type
]
=
&
BuildInToolInfo
{
ToolName
:
tool
.
Type
,
CallCount
:
0
,
}
switch
tool
.
Type
{
case
dto
.
BuildInToolWebSearchPreview
:
if
tool
.
SearchContextSize
==
""
{
tool
.
SearchContextSize
=
"medium"
}
info
.
ResponsesUsageInfo
.
BuiltInTools
[
tool
.
Type
]
.
SearchContextSize
=
tool
.
SearchContextSize
}
}
}
info
.
IsStream
=
req
.
Stream
return
info
}
func
GenRelayInfo
(
c
*
gin
.
Context
)
*
RelayInfo
{
func
GenRelayInfo
(
c
*
gin
.
Context
)
*
RelayInfo
{
channelType
:=
c
.
GetInt
(
"channel_type"
)
channelType
:=
c
.
GetInt
(
"channel_type"
)
channelId
:=
c
.
GetInt
(
"channel_id"
)
channelId
:=
c
.
GetInt
(
"channel_id"
)
...
...
relay/helper/model_mapped.go
View file @
645b19b9
...
@@ -2,9 +2,11 @@ package helper
...
@@ -2,9 +2,11 @@ package helper
import
(
import
(
"encoding/json"
"encoding/json"
"errors"
"fmt"
"fmt"
"github.com/gin-gonic/gin"
"one-api/relay/common"
"one-api/relay/common"
"github.com/gin-gonic/gin"
)
)
func
ModelMappedHelper
(
c
*
gin
.
Context
,
info
*
common
.
RelayInfo
)
error
{
func
ModelMappedHelper
(
c
*
gin
.
Context
,
info
*
common
.
RelayInfo
)
error
{
...
@@ -16,9 +18,36 @@ func ModelMappedHelper(c *gin.Context, info *common.RelayInfo) error {
...
@@ -16,9 +18,36 @@ func ModelMappedHelper(c *gin.Context, info *common.RelayInfo) error {
if
err
!=
nil
{
if
err
!=
nil
{
return
fmt
.
Errorf
(
"unmarshal_model_mapping_failed"
)
return
fmt
.
Errorf
(
"unmarshal_model_mapping_failed"
)
}
}
if
modelMap
[
info
.
OriginModelName
]
!=
""
{
info
.
UpstreamModelName
=
modelMap
[
info
.
OriginModelName
]
// 支持链式模型重定向,最终使用链尾的模型
currentModel
:=
info
.
OriginModelName
visitedModels
:=
map
[
string
]
bool
{
currentModel
:
true
,
}
for
{
if
mappedModel
,
exists
:=
modelMap
[
currentModel
];
exists
&&
mappedModel
!=
""
{
// 模型重定向循环检测,避免无限循环
if
visitedModels
[
mappedModel
]
{
if
mappedModel
==
currentModel
{
if
currentModel
==
info
.
OriginModelName
{
info
.
IsModelMapped
=
false
return
nil
}
else
{
info
.
IsModelMapped
=
true
break
}
}
return
errors
.
New
(
"model_mapping_contains_cycle"
)
}
visitedModels
[
mappedModel
]
=
true
currentModel
=
mappedModel
info
.
IsModelMapped
=
true
info
.
IsModelMapped
=
true
}
else
{
break
}
}
if
info
.
IsModelMapped
{
info
.
UpstreamModelName
=
currentModel
}
}
}
}
return
nil
return
nil
...
...
relay/relay-responses.go
View file @
645b19b9
...
@@ -19,7 +19,7 @@ import (
...
@@ -19,7 +19,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin"
)
)
func
getAndValidateResponsesRequest
(
c
*
gin
.
Context
,
relayInfo
*
relaycommon
.
RelayInfo
)
(
*
dto
.
OpenAIResponsesRequest
,
error
)
{
func
getAndValidateResponsesRequest
(
c
*
gin
.
Context
)
(
*
dto
.
OpenAIResponsesRequest
,
error
)
{
request
:=
&
dto
.
OpenAIResponsesRequest
{}
request
:=
&
dto
.
OpenAIResponsesRequest
{}
err
:=
common
.
UnmarshalBodyReusable
(
c
,
request
)
err
:=
common
.
UnmarshalBodyReusable
(
c
,
request
)
if
err
!=
nil
{
if
err
!=
nil
{
...
@@ -31,13 +31,11 @@ func getAndValidateResponsesRequest(c *gin.Context, relayInfo *relaycommon.Relay
...
@@ -31,13 +31,11 @@ func getAndValidateResponsesRequest(c *gin.Context, relayInfo *relaycommon.Relay
if
len
(
request
.
Input
)
==
0
{
if
len
(
request
.
Input
)
==
0
{
return
nil
,
errors
.
New
(
"input is required"
)
return
nil
,
errors
.
New
(
"input is required"
)
}
}
relayInfo
.
IsStream
=
request
.
Stream
return
request
,
nil
return
request
,
nil
}
}
func
checkInputSensitive
(
textRequest
*
dto
.
OpenAIResponsesRequest
,
info
*
relaycommon
.
RelayInfo
)
([]
string
,
error
)
{
func
checkInputSensitive
(
textRequest
*
dto
.
OpenAIResponsesRequest
,
info
*
relaycommon
.
RelayInfo
)
([]
string
,
error
)
{
sensitiveWords
,
err
:=
service
.
CheckSensitiveInput
(
textRequest
.
Input
)
sensitiveWords
,
err
:=
service
.
CheckSensitiveInput
(
textRequest
.
Input
)
return
sensitiveWords
,
err
return
sensitiveWords
,
err
}
}
...
@@ -49,12 +47,14 @@ func getInputTokens(req *dto.OpenAIResponsesRequest, info *relaycommon.RelayInfo
...
@@ -49,12 +47,14 @@ func getInputTokens(req *dto.OpenAIResponsesRequest, info *relaycommon.RelayInfo
}
}
func
ResponsesHelper
(
c
*
gin
.
Context
)
(
openaiErr
*
dto
.
OpenAIErrorWithStatusCode
)
{
func
ResponsesHelper
(
c
*
gin
.
Context
)
(
openaiErr
*
dto
.
OpenAIErrorWithStatusCode
)
{
relayInfo
:=
relaycommon
.
GenRelayInfo
(
c
)
req
,
err
:=
getAndValidateResponsesRequest
(
c
)
req
,
err
:=
getAndValidateResponsesRequest
(
c
,
relayInfo
)
if
err
!=
nil
{
if
err
!=
nil
{
common
.
LogError
(
c
,
fmt
.
Sprintf
(
"getAndValidateResponsesRequest error: %s"
,
err
.
Error
()))
common
.
LogError
(
c
,
fmt
.
Sprintf
(
"getAndValidateResponsesRequest error: %s"
,
err
.
Error
()))
return
service
.
OpenAIErrorWrapperLocal
(
err
,
"invalid_responses_request"
,
http
.
StatusBadRequest
)
return
service
.
OpenAIErrorWrapperLocal
(
err
,
"invalid_responses_request"
,
http
.
StatusBadRequest
)
}
}
relayInfo
:=
relaycommon
.
GenRelayInfoResponses
(
c
,
req
)
if
setting
.
ShouldCheckPromptSensitive
()
{
if
setting
.
ShouldCheckPromptSensitive
()
{
sensitiveWords
,
err
:=
checkInputSensitive
(
req
,
relayInfo
)
sensitiveWords
,
err
:=
checkInputSensitive
(
req
,
relayInfo
)
if
err
!=
nil
{
if
err
!=
nil
{
...
...
relay/relay-text.go
View file @
645b19b9
...
@@ -18,6 +18,7 @@ import (
...
@@ -18,6 +18,7 @@ import (
"one-api/service"
"one-api/service"
"one-api/setting"
"one-api/setting"
"one-api/setting/model_setting"
"one-api/setting/model_setting"
"one-api/setting/operation_setting"
"strings"
"strings"
"time"
"time"
...
@@ -358,6 +359,34 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
...
@@ -358,6 +359,34 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
ratio
:=
dModelRatio
.
Mul
(
dGroupRatio
)
ratio
:=
dModelRatio
.
Mul
(
dGroupRatio
)
// openai web search 工具计费
var
dWebSearchQuota
decimal
.
Decimal
var
webSearchPrice
float64
if
relayInfo
.
ResponsesUsageInfo
!=
nil
{
if
webSearchTool
,
exists
:=
relayInfo
.
ResponsesUsageInfo
.
BuiltInTools
[
dto
.
BuildInToolWebSearchPreview
];
exists
&&
webSearchTool
.
CallCount
>
0
{
// 计算 web search 调用的配额 (配额 = 价格 * 调用次数 / 1000)
webSearchPrice
=
operation_setting
.
GetWebSearchPricePerThousand
(
modelName
,
webSearchTool
.
SearchContextSize
)
dWebSearchQuota
=
decimal
.
NewFromFloat
(
webSearchPrice
)
.
Mul
(
decimal
.
NewFromInt
(
int64
(
webSearchTool
.
CallCount
)))
.
Div
(
decimal
.
NewFromInt
(
1000
))
extraContent
+=
fmt
.
Sprintf
(
"Web Search 调用 %d 次,上下文大小 %s,调用花费 $%s"
,
webSearchTool
.
CallCount
,
webSearchTool
.
SearchContextSize
,
dWebSearchQuota
.
String
())
}
}
// file search tool 计费
var
dFileSearchQuota
decimal
.
Decimal
var
fileSearchPrice
float64
if
relayInfo
.
ResponsesUsageInfo
!=
nil
{
if
fileSearchTool
,
exists
:=
relayInfo
.
ResponsesUsageInfo
.
BuiltInTools
[
dto
.
BuildInToolFileSearch
];
exists
&&
fileSearchTool
.
CallCount
>
0
{
fileSearchPrice
=
operation_setting
.
GetFileSearchPricePerThousand
()
dFileSearchQuota
=
decimal
.
NewFromFloat
(
fileSearchPrice
)
.
Mul
(
decimal
.
NewFromInt
(
int64
(
fileSearchTool
.
CallCount
)))
.
Div
(
decimal
.
NewFromInt
(
1000
))
extraContent
+=
fmt
.
Sprintf
(
"File Search 调用 %d 次,调用花费 $%s"
,
fileSearchTool
.
CallCount
,
dFileSearchQuota
.
String
())
}
}
var
quotaCalculateDecimal
decimal
.
Decimal
var
quotaCalculateDecimal
decimal
.
Decimal
if
!
priceData
.
UsePrice
{
if
!
priceData
.
UsePrice
{
nonCachedTokens
:=
dPromptTokens
.
Sub
(
dCacheTokens
)
nonCachedTokens
:=
dPromptTokens
.
Sub
(
dCacheTokens
)
...
@@ -380,6 +409,9 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
...
@@ -380,6 +409,9 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
}
else
{
}
else
{
quotaCalculateDecimal
=
dModelPrice
.
Mul
(
dQuotaPerUnit
)
.
Mul
(
dGroupRatio
)
quotaCalculateDecimal
=
dModelPrice
.
Mul
(
dQuotaPerUnit
)
.
Mul
(
dGroupRatio
)
}
}
// 添加 responses tools call 调用的配额
quotaCalculateDecimal
=
quotaCalculateDecimal
.
Add
(
dWebSearchQuota
)
quotaCalculateDecimal
=
quotaCalculateDecimal
.
Add
(
dFileSearchQuota
)
quota
:=
int
(
quotaCalculateDecimal
.
Round
(
0
)
.
IntPart
())
quota
:=
int
(
quotaCalculateDecimal
.
Round
(
0
)
.
IntPart
())
totalTokens
:=
promptTokens
+
completionTokens
totalTokens
:=
promptTokens
+
completionTokens
...
@@ -430,6 +462,20 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
...
@@ -430,6 +462,20 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
other
[
"image_ratio"
]
=
imageRatio
other
[
"image_ratio"
]
=
imageRatio
other
[
"image_output"
]
=
imageTokens
other
[
"image_output"
]
=
imageTokens
}
}
if
!
dWebSearchQuota
.
IsZero
()
&&
relayInfo
.
ResponsesUsageInfo
!=
nil
{
if
webSearchTool
,
exists
:=
relayInfo
.
ResponsesUsageInfo
.
BuiltInTools
[
dto
.
BuildInToolWebSearchPreview
];
exists
{
other
[
"web_search"
]
=
true
other
[
"web_search_call_count"
]
=
webSearchTool
.
CallCount
other
[
"web_search_price"
]
=
webSearchPrice
}
}
if
!
dFileSearchQuota
.
IsZero
()
&&
relayInfo
.
ResponsesUsageInfo
!=
nil
{
if
fileSearchTool
,
exists
:=
relayInfo
.
ResponsesUsageInfo
.
BuiltInTools
[
dto
.
BuildInToolFileSearch
];
exists
{
other
[
"file_search"
]
=
true
other
[
"file_search_call_count"
]
=
fileSearchTool
.
CallCount
other
[
"file_search_price"
]
=
fileSearchPrice
}
}
model
.
RecordConsumeLog
(
ctx
,
relayInfo
.
UserId
,
relayInfo
.
ChannelId
,
promptTokens
,
completionTokens
,
logModel
,
model
.
RecordConsumeLog
(
ctx
,
relayInfo
.
UserId
,
relayInfo
.
ChannelId
,
promptTokens
,
completionTokens
,
logModel
,
tokenName
,
quota
,
logContent
,
relayInfo
.
TokenId
,
userQuota
,
int
(
useTimeSeconds
),
relayInfo
.
IsStream
,
relayInfo
.
Group
,
other
)
tokenName
,
quota
,
logContent
,
relayInfo
.
TokenId
,
userQuota
,
int
(
useTimeSeconds
),
relayInfo
.
IsStream
,
relayInfo
.
Group
,
other
)
}
}
setting/operation_setting/tools.go
0 → 100644
View file @
645b19b9
package
operation_setting
import
"strings"
const
(
// Web search
WebSearchHighTierModelPriceLow
=
30.00
WebSearchHighTierModelPriceMedium
=
35.00
WebSearchHighTierModelPriceHigh
=
50.00
WebSearchPriceLow
=
25.00
WebSearchPriceMedium
=
27.50
WebSearchPriceHigh
=
30.00
// File search
FileSearchPrice
=
2.5
)
func
GetWebSearchPricePerThousand
(
modelName
string
,
contextSize
string
)
float64
{
// 确定模型类型
// https://platform.openai.com/docs/pricing Web search 价格按模型类型和 search context size 收费
// gpt-4.1, gpt-4o, or gpt-4o-search-preview 更贵,gpt-4.1-mini, gpt-4o-mini, gpt-4o-mini-search-preview 更便宜
isHighTierModel
:=
(
strings
.
HasPrefix
(
modelName
,
"gpt-4.1"
)
||
strings
.
HasPrefix
(
modelName
,
"gpt-4o"
))
&&
!
strings
.
Contains
(
modelName
,
"mini"
)
// 确定 search context size 对应的价格
var
priceWebSearchPerThousandCalls
float64
switch
contextSize
{
case
"low"
:
if
isHighTierModel
{
priceWebSearchPerThousandCalls
=
WebSearchHighTierModelPriceLow
}
else
{
priceWebSearchPerThousandCalls
=
WebSearchPriceLow
}
case
"medium"
:
if
isHighTierModel
{
priceWebSearchPerThousandCalls
=
WebSearchHighTierModelPriceMedium
}
else
{
priceWebSearchPerThousandCalls
=
WebSearchPriceMedium
}
case
"high"
:
if
isHighTierModel
{
priceWebSearchPerThousandCalls
=
WebSearchHighTierModelPriceHigh
}
else
{
priceWebSearchPerThousandCalls
=
WebSearchPriceHigh
}
default
:
// search context size 默认为 medium
if
isHighTierModel
{
priceWebSearchPerThousandCalls
=
WebSearchHighTierModelPriceMedium
}
else
{
priceWebSearchPerThousandCalls
=
WebSearchPriceMedium
}
}
return
priceWebSearchPerThousandCalls
}
func
GetFileSearchPricePerThousand
()
float64
{
return
FileSearchPrice
}
web/src/components/LogsTable.js
View file @
645b19b9
...
@@ -618,7 +618,6 @@ const LogsTable = () => {
...
@@ -618,7 +618,6 @@ const LogsTable = () => {
<
/Paragraph
>
<
/Paragraph
>
);
);
}
}
let
content
=
other
?.
claude
let
content
=
other
?.
claude
?
renderClaudeModelPriceSimple
(
?
renderClaudeModelPriceSimple
(
other
.
model_ratio
,
other
.
model_ratio
,
...
@@ -935,6 +934,13 @@ const LogsTable = () => {
...
@@ -935,6 +934,13 @@ const LogsTable = () => {
other
.
model_price
,
other
.
model_price
,
other
.
group_ratio
,
other
.
group_ratio
,
other
?.
user_group_ratio
,
other
?.
user_group_ratio
,
false
,
1.0
,
undefined
,
other
.
web_search
||
false
,
other
.
web_search_call_count
||
0
,
other
.
file_search
||
false
,
other
.
file_search_call_count
||
0
,
),
),
});
});
}
}
...
@@ -995,6 +1001,12 @@ const LogsTable = () => {
...
@@ -995,6 +1001,12 @@ const LogsTable = () => {
other
?.
image
||
false
,
other
?.
image
||
false
,
other
?.
image_ratio
||
0
,
other
?.
image_ratio
||
0
,
other
?.
image_output
||
0
,
other
?.
image_output
||
0
,
other
?.
web_search
||
false
,
other
?.
web_search_call_count
||
0
,
other
?.
web_search_price
||
0
,
other
?.
file_search
||
false
,
other
?.
file_search_call_count
||
0
,
other
?.
file_search_price
||
0
,
);
);
}
}
expandDataLocal
.
push
({
expandDataLocal
.
push
({
...
...
web/src/components/PersonalSetting.js
View file @
645b19b9
...
@@ -57,6 +57,7 @@ const PersonalSetting = () => {
...
@@ -57,6 +57,7 @@ const PersonalSetting = () => {
email_verification_code
:
''
,
email_verification_code
:
''
,
email
:
''
,
email
:
''
,
self_account_deletion_confirmation
:
''
,
self_account_deletion_confirmation
:
''
,
original_password
:
''
,
set_new_password
:
''
,
set_new_password
:
''
,
set_new_password_confirmation
:
''
,
set_new_password_confirmation
:
''
,
});
});
...
@@ -239,11 +240,24 @@ const PersonalSetting = () => {
...
@@ -239,11 +240,24 @@ const PersonalSetting = () => {
};
};
const
changePassword
=
async
()
=>
{
const
changePassword
=
async
()
=>
{
if
(
inputs
.
original_password
===
''
)
{
showError
(
t
(
'请输入原密码!'
));
return
;
}
if
(
inputs
.
set_new_password
===
''
)
{
showError
(
t
(
'请输入新密码!'
));
return
;
}
if
(
inputs
.
original_password
===
inputs
.
set_new_password
)
{
showError
(
t
(
'新密码需要和原密码不一致!'
));
return
;
}
if
(
inputs
.
set_new_password
!==
inputs
.
set_new_password_confirmation
)
{
if
(
inputs
.
set_new_password
!==
inputs
.
set_new_password_confirmation
)
{
showError
(
t
(
'两次输入的密码不一致!'
));
showError
(
t
(
'两次输入的密码不一致!'
));
return
;
return
;
}
}
const
res
=
await
API
.
put
(
`/api/user/self`
,
{
const
res
=
await
API
.
put
(
`/api/user/self`
,
{
original_password
:
inputs
.
original_password
,
password
:
inputs
.
set_new_password
,
password
:
inputs
.
set_new_password
,
});
});
const
{
success
,
message
}
=
res
.
data
;
const
{
success
,
message
}
=
res
.
data
;
...
@@ -816,8 +830,8 @@ const PersonalSetting = () => {
...
@@ -816,8 +830,8 @@ const PersonalSetting = () => {
<
/div
>
<
/div
>
<
/Card
>
<
/Card
>
<
Card
style
=
{{
marginTop
:
10
}}
>
<
Card
style
=
{{
marginTop
:
10
}}
>
<
Tabs
type
=
"line"
defaultActiveKey
=
"notification"
>
<
Tabs
type
=
'line'
defaultActiveKey
=
'notification'
>
<
TabPane
tab
=
{
t
(
'通知设置'
)}
itemKey
=
"notification"
>
<
TabPane
tab
=
{
t
(
'通知设置'
)}
itemKey
=
'notification'
>
<
div
style
=
{{
marginTop
:
20
}}
>
<
div
style
=
{{
marginTop
:
20
}}
>
<
Typography
.
Text
strong
>
{
t
(
'通知方式'
)}
<
/Typography.Text
>
<
Typography
.
Text
strong
>
{
t
(
'通知方式'
)}
<
/Typography.Text
>
<
div
style
=
{{
marginTop
:
10
}}
>
<
div
style
=
{{
marginTop
:
10
}}
>
...
@@ -993,23 +1007,36 @@ const PersonalSetting = () => {
...
@@ -993,23 +1007,36 @@ const PersonalSetting = () => {
<
/Typography.Text
>
<
/Typography.Text
>
<
/div
>
<
/div
>
<
/TabPane
>
<
/TabPane
>
<
TabPane
tab
=
{
t
(
'价格设置'
)}
itemKey
=
"price"
>
<
TabPane
tab
=
{
t
(
'价格设置'
)}
itemKey
=
'price'
>
<
div
style
=
{{
marginTop
:
20
}}
>
<
div
style
=
{{
marginTop
:
20
}}
>
<
Typography
.
Text
strong
>
{
t
(
'接受未设置价格模型'
)}
<
/Typography.Text
>
<
Typography
.
Text
strong
>
{
t
(
'接受未设置价格模型'
)}
<
/Typography.Text
>
<
div
style
=
{{
marginTop
:
10
}}
>
<
div
style
=
{{
marginTop
:
10
}}
>
<
Checkbox
<
Checkbox
checked
=
{
notificationSettings
.
acceptUnsetModelRatioModel
}
checked
=
{
onChange
=
{
e
=>
handleNotificationSettingChange
(
'acceptUnsetModelRatioModel'
,
e
.
target
.
checked
)}
notificationSettings
.
acceptUnsetModelRatioModel
}
onChange
=
{(
e
)
=>
handleNotificationSettingChange
(
'acceptUnsetModelRatioModel'
,
e
.
target
.
checked
,
)
}
>
>
{
t
(
'接受未设置价格模型'
)}
{
t
(
'接受未设置价格模型'
)}
<
/Checkbox
>
<
/Checkbox
>
<
Typography
.
Text
type
=
"secondary"
style
=
{{
marginTop
:
8
,
display
:
'block'
}}
>
<
Typography
.
Text
{
t
(
'当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用'
)}
type
=
'secondary'
style
=
{{
marginTop
:
8
,
display
:
'block'
}}
>
{
t
(
'当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用'
,
)}
<
/Typography.Text
>
<
/Typography.Text
>
<
/div
>
<
/div
>
<
/div
>
<
/div
>
<
/TabPane
>
<
/TabPane
>
<
/Tabs
>
<
/Tabs
>
<
div
style
=
{{
marginTop
:
20
}}
>
<
div
style
=
{{
marginTop
:
20
}}
>
<
Button
type
=
'primary'
onClick
=
{
saveNotificationSettings
}
>
<
Button
type
=
'primary'
onClick
=
{
saveNotificationSettings
}
>
...
@@ -1118,6 +1145,16 @@ const PersonalSetting = () => {
...
@@ -1118,6 +1145,16 @@ const PersonalSetting = () => {
>
>
<div style={{ marginTop: 20 }}>
<div style={{ marginTop: 20 }}>
<Input
<Input
name='original_password'
placeholder={t('原密码')}
type='password'
value={inputs.original_password}
onChange={(value) =>
handleInputChange('original_password', value)
}
/>
<Input
style={{ marginTop: 20 }}
name='set_new_password'
name='set_new_password'
placeholder={t('新密码')}
placeholder={t('新密码')}
value={inputs.set_new_password}
value={inputs.set_new_password}
...
...
web/src/helpers/render.js
View file @
645b19b9
...
@@ -317,6 +317,12 @@ export function renderModelPrice(
...
@@ -317,6 +317,12 @@ export function renderModelPrice(
image
=
false
,
image
=
false
,
imageRatio
=
1.0
,
imageRatio
=
1.0
,
imageOutputTokens
=
0
,
imageOutputTokens
=
0
,
webSearch
=
false
,
webSearchCallCount
=
0
,
webSearchPrice
=
0
,
fileSearch
=
false
,
fileSearchCallCount
=
0
,
fileSearchPrice
=
0
,
)
{
)
{
if
(
modelPrice
!==
-
1
)
{
if
(
modelPrice
!==
-
1
)
{
return
i18next
.
t
(
return
i18next
.
t
(
...
@@ -339,14 +345,17 @@ export function renderModelPrice(
...
@@ -339,14 +345,17 @@ export function renderModelPrice(
// Calculate effective input tokens (non-cached + cached with ratio applied)
// Calculate effective input tokens (non-cached + cached with ratio applied)
let
effectiveInputTokens
=
let
effectiveInputTokens
=
inputTokens
-
cacheTokens
+
cacheTokens
*
cacheRatio
;
inputTokens
-
cacheTokens
+
cacheTokens
*
cacheRatio
;
// Handle image tokens if present
// Handle image tokens if present
if
(
image
&&
imageOutputTokens
>
0
)
{
if
(
image
&&
imageOutputTokens
>
0
)
{
effectiveInputTokens
=
inputTokens
-
imageOutputTokens
+
imageOutputTokens
*
imageRatio
;
effectiveInputTokens
=
inputTokens
-
imageOutputTokens
+
imageOutputTokens
*
imageRatio
;
}
}
let
price
=
let
price
=
(
effectiveInputTokens
/
1000000
)
*
inputRatioPrice
*
groupRatio
+
(
effectiveInputTokens
/
1000000
)
*
inputRatioPrice
*
groupRatio
+
(
completionTokens
/
1000000
)
*
completionRatioPrice
*
groupRatio
;
(
completionTokens
/
1000000
)
*
completionRatioPrice
*
groupRatio
+
(
webSearchCallCount
/
1000
)
*
webSearchPrice
+
(
fileSearchCallCount
/
1000
)
*
fileSearchPrice
;
return
(
return
(
<>
<>
...
@@ -391,9 +400,23 @@ export function renderModelPrice(
...
@@ -391,9 +400,23 @@ export function renderModelPrice(
)}
)}
<
/p
>
<
/p
>
)}
)}
{
webSearch
&&
webSearchCallCount
>
0
&&
(
<
p
>
{
i18next
.
t
(
'Web搜索价格:${{price}} / 1K 次'
,
{
price
:
webSearchPrice
,
})}
<
/p
>
)}
{
fileSearch
&&
fileSearchCallCount
>
0
&&
(
<
p
>
{
i18next
.
t
(
'文件搜索价格:${{price}} / 1K 次'
,
{
price
:
fileSearchPrice
,
})}
<
/p
>
)}
<
p
><
/p
>
<
p
><
/p
>
<
p
>
<
p
>
{
cacheTokens
>
0
&&
!
image
{
cacheTokens
>
0
&&
!
image
&&
!
webSearch
&&
!
fileSearch
?
i18next
.
t
(
?
i18next
.
t
(
'输入 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}'
,
'输入 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}'
,
{
{
...
@@ -407,7 +430,7 @@ export function renderModelPrice(
...
@@ -407,7 +430,7 @@ export function renderModelPrice(
total
:
price
.
toFixed
(
6
),
total
:
price
.
toFixed
(
6
),
},
},
)
)
:
image
&&
imageOutputTokens
>
0
:
image
&&
imageOutputTokens
>
0
&&
!
webSearch
&&
!
fileSearch
?
i18next
.
t
(
?
i18next
.
t
(
'输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}'
,
'输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}'
,
{
{
...
@@ -421,6 +444,50 @@ export function renderModelPrice(
...
@@ -421,6 +444,50 @@ export function renderModelPrice(
total
:
price
.
toFixed
(
6
),
total
:
price
.
toFixed
(
6
),
},
},
)
)
:
webSearch
&&
webSearchCallCount
>
0
&&
!
image
&&
!
fileSearch
?
i18next
.
t
(
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + Web搜索 {{webSearchCallCount}}次 / 1K 次 * ${{webSearchPrice}} = ${{total}}'
,
{
input
:
inputTokens
,
price
:
inputRatioPrice
,
completion
:
completionTokens
,
compPrice
:
completionRatioPrice
,
ratio
:
groupRatio
,
webSearchCallCount
,
webSearchPrice
,
total
:
price
.
toFixed
(
6
),
},
)
:
fileSearch
&&
fileSearchCallCount
>
0
&&
!
image
&&
!
webSearch
?
i18next
.
t
(
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + 文件搜索 {{fileSearchCallCount}}次 / 1K 次 * ${{fileSearchPrice}} = ${{total}}'
,
{
input
:
inputTokens
,
price
:
inputRatioPrice
,
completion
:
completionTokens
,
compPrice
:
completionRatioPrice
,
ratio
:
groupRatio
,
fileSearchCallCount
,
fileSearchPrice
,
total
:
price
.
toFixed
(
6
),
},
)
:
webSearch
&&
webSearchCallCount
>
0
&&
fileSearch
&&
fileSearchCallCount
>
0
&&
!
image
?
i18next
.
t
(
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + Web搜索 {{webSearchCallCount}}次 / 1K 次 * ${{webSearchPrice}} + 文件搜索 {{fileSearchCallCount}}次 / 1K 次 * ${{fileSearchPrice}} = ${{total}}'
,
{
input
:
inputTokens
,
price
:
inputRatioPrice
,
completion
:
completionTokens
,
compPrice
:
completionRatioPrice
,
ratio
:
groupRatio
,
webSearchCallCount
,
webSearchPrice
,
fileSearchCallCount
,
fileSearchPrice
,
total
:
price
.
toFixed
(
6
),
},
)
:
i18next
.
t
(
:
i18next
.
t
(
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}'
,
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}'
,
{
{
...
@@ -448,33 +515,56 @@ export function renderLogContent(
...
@@ -448,33 +515,56 @@ export function renderLogContent(
user_group_ratio
,
user_group_ratio
,
image
=
false
,
image
=
false
,
imageRatio
=
1.0
,
imageRatio
=
1.0
,
useUserGroupRatio
=
undefined
useUserGroupRatio
=
undefined
,
webSearch
=
false
,
webSearchCallCount
=
0
,
fileSearch
=
false
,
fileSearchCallCount
=
0
,
)
{
)
{
const
ratioLabel
=
useUserGroupRatio
?
i18next
.
t
(
'专属倍率'
)
:
i18next
.
t
(
'分组倍率'
);
const
ratioLabel
=
useUserGroupRatio
?
i18next
.
t
(
'专属倍率'
)
:
i18next
.
t
(
'分组倍率'
);
const
ratio
=
useUserGroupRatio
?
user_group_ratio
:
groupRatio
;
const
ratio
=
useUserGroupRatio
?
user_group_ratio
:
groupRatio
;
if
(
modelPrice
!==
-
1
)
{
if
(
modelPrice
!==
-
1
)
{
return
i18next
.
t
(
'模型价格 ${{price}},{{ratioType}} {{ratio}}'
,
{
return
i18next
.
t
(
'模型价格 ${{price}},{{ratioType}} {{ratio}}'
,
{
price
:
modelPrice
,
price
:
modelPrice
,
ratioType
:
ratioLabel
,
ratioType
:
ratioLabel
,
ratio
ratio
,
});
});
}
else
{
}
else
{
if
(
image
)
{
if
(
image
)
{
return
i18next
.
t
(
'模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},图片输入倍率 {{imageRatio}},{{ratioType}} {{ratio}}'
,
{
return
i18next
.
t
(
'模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},图片输入倍率 {{imageRatio}},{{ratioType}} {{ratio}}'
,
{
modelRatio
:
modelRatio
,
modelRatio
:
modelRatio
,
completionRatio
:
completionRatio
,
completionRatio
:
completionRatio
,
imageRatio
:
imageRatio
,
imageRatio
:
imageRatio
,
ratioType
:
ratioLabel
,
ratioType
:
ratioLabel
,
ratio
ratio
,
});
},
);
}
else
if
(
webSearch
)
{
return
i18next
.
t
(
'模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}},Web 搜索调用 {{webSearchCallCount}} 次'
,
{
modelRatio
:
modelRatio
,
completionRatio
:
completionRatio
,
ratioType
:
ratioLabel
,
ratio
,
webSearchCallCount
,
},
);
}
else
{
}
else
{
return
i18next
.
t
(
'模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}'
,
{
return
i18next
.
t
(
'模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}'
,
{
modelRatio
:
modelRatio
,
modelRatio
:
modelRatio
,
completionRatio
:
completionRatio
,
completionRatio
:
completionRatio
,
ratioType
:
ratioLabel
,
ratioType
:
ratioLabel
,
ratio
ratio
,
});
},
);
}
}
}
}
}
}
...
...
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