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
0647872c
authored
Apr 24, 2025
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: support image edit model mapping
(cherry picked from commit 1a869d8ad77f262ee27675ec2deaf451b1743eb7)
parent
873ebcf0
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
232 additions
and
60 deletions
+232
-60
relay/channel/openai/adaptor.go
+109
-19
relay/relay-text.go
+5
-0
web/src/components/LogsTable.js
+3
-1
web/src/helpers/render.js
+105
-35
web/src/i18n/locales/en.json
+10
-5
No files found.
relay/channel/openai/adaptor.go
View file @
0647872c
...
...
@@ -22,9 +22,11 @@ import (
"one-api/relay/common_handler"
"one-api/relay/constant"
"one-api/service"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
"net/textproto"
)
type
Adaptor
struct
{
...
...
@@ -238,13 +240,8 @@ func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInf
func
(
a
*
Adaptor
)
ConvertImageRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
request
dto
.
ImageRequest
)
(
any
,
error
)
{
switch
info
.
RelayMode
{
case
constant
.
RelayModeImagesEdits
:
body
,
err
:=
common
.
GetRequestBody
(
c
)
if
err
!=
nil
{
return
nil
,
errors
.
New
(
"get request body fail"
)
}
return
bytes
.
NewReader
(
body
),
nil
/*
var requestBody bytes.Buffer
var
requestBody
bytes
.
Buffer
writer
:=
multipart
.
NewWriter
(
&
requestBody
)
writer
.
WriteField
(
"model"
,
request
.
Model
)
...
...
@@ -260,36 +257,129 @@ func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInf
}
}
// 添加文件字段
imageFiles := c.Request.MultipartForm.File["image[]"]
for _, file := range imageFiles {
part, err := writer.CreateFormFile("image[]", file.Filename)
// Parse the multipart form to handle both single image and multiple images
if
err
:=
c
.
Request
.
ParseMultipartForm
(
32
<<
20
);
err
!=
nil
{
// 32MB max memory
return
nil
,
errors
.
New
(
"failed to parse multipart form"
)
}
if
c
.
Request
.
MultipartForm
!=
nil
&&
c
.
Request
.
MultipartForm
.
File
!=
nil
{
// Check if "image" field exists in any form, including array notation
var
imageFiles
[]
*
multipart
.
FileHeader
var
exists
bool
// First check for standard "image" field
if
imageFiles
,
exists
=
c
.
Request
.
MultipartForm
.
File
[
"image"
];
!
exists
||
len
(
imageFiles
)
==
0
{
// If not found, check for "image[]" field
if
imageFiles
,
exists
=
c
.
Request
.
MultipartForm
.
File
[
"image[]"
];
!
exists
||
len
(
imageFiles
)
==
0
{
// If still not found, iterate through all fields to find any that start with "image["
foundArrayImages
:=
false
for
fieldName
,
files
:=
range
c
.
Request
.
MultipartForm
.
File
{
if
strings
.
HasPrefix
(
fieldName
,
"image["
)
&&
len
(
files
)
>
0
{
foundArrayImages
=
true
for
_
,
file
:=
range
files
{
imageFiles
=
append
(
imageFiles
,
file
)
}
}
}
// If no image fields found at all
if
!
foundArrayImages
&&
(
len
(
imageFiles
)
==
0
)
{
return
nil
,
errors
.
New
(
"image is required"
)
}
}
}
// Process all image files
for
i
,
fileHeader
:=
range
imageFiles
{
file
,
err
:=
fileHeader
.
Open
()
if
err
!=
nil
{
return nil, errors.New("create form file failed")
return
nil
,
fmt
.
Errorf
(
"failed to open image file %d: %w"
,
i
,
err
)
}
defer
file
.
Close
()
// If multiple images, use image[] as the field name
fieldName
:=
"image"
if
len
(
imageFiles
)
>
1
{
fieldName
=
"image[]"
}
// Determine MIME type based on file extension
mimeType
:=
detectImageMimeType
(
fileHeader
.
Filename
)
// Create a form file with the appropriate content type
h
:=
make
(
textproto
.
MIMEHeader
)
h
.
Set
(
"Content-Disposition"
,
fmt
.
Sprintf
(
`form-data; name="%s"; filename="%s"`
,
fieldName
,
fileHeader
.
Filename
))
h
.
Set
(
"Content-Type"
,
mimeType
)
part
,
err
:=
writer
.
CreatePart
(
h
)
if
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"create form part failed for image %d: %w"
,
i
,
err
)
}
if
_
,
err
:=
io
.
Copy
(
part
,
file
);
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"copy file failed for image %d: %w"
,
i
,
err
)
}
// 打开文件
src, err := file.Open()
}
// Handle mask file if present
if
maskFiles
,
exists
:=
c
.
Request
.
MultipartForm
.
File
[
"mask"
];
exists
&&
len
(
maskFiles
)
>
0
{
maskFile
,
err
:=
maskFiles
[
0
]
.
Open
()
if
err
!=
nil
{
return nil, errors.New("open file failed
")
return
nil
,
errors
.
New
(
"failed to open mask file
"
)
}
// 将文件数据写入 form part
_, err = io.Copy(part, src)
defer
maskFile
.
Close
()
// Determine MIME type for mask file
mimeType
:=
detectImageMimeType
(
maskFiles
[
0
]
.
Filename
)
// Create a form file with the appropriate content type
h
:=
make
(
textproto
.
MIMEHeader
)
h
.
Set
(
"Content-Disposition"
,
fmt
.
Sprintf
(
`form-data; name="mask"; filename="%s"`
,
maskFiles
[
0
]
.
Filename
))
h
.
Set
(
"Content-Type"
,
mimeType
)
maskPart
,
err
:=
writer
.
CreatePart
(
h
)
if
err
!=
nil
{
return nil, errors.New("copy file failed")
return
nil
,
errors
.
New
(
"create form file failed for mask"
)
}
if
_
,
err
:=
io
.
Copy
(
maskPart
,
maskFile
);
err
!=
nil
{
return
nil
,
errors
.
New
(
"copy mask file failed"
)
}
src.Close()
}
}
else
{
return
nil
,
errors
.
New
(
"no multipart form data found"
)
}
// 关闭 multipart 编写器以设置分界线
writer
.
Close
()
c
.
Request
.
Header
.
Set
(
"Content-Type"
,
writer
.
FormDataContentType
())
return bytes.NewReader(requestBody.Bytes()), nil
*/
return
bytes
.
NewReader
(
requestBody
.
Bytes
()),
nil
default
:
return
request
,
nil
}
}
// detectImageMimeType determines the MIME type based on the file extension
func
detectImageMimeType
(
filename
string
)
string
{
ext
:=
strings
.
ToLower
(
filepath
.
Ext
(
filename
))
switch
ext
{
case
".jpg"
,
".jpeg"
:
return
"image/jpeg"
case
".png"
:
return
"image/png"
case
".webp"
:
return
"image/webp"
default
:
// Try to detect from extension if possible
if
strings
.
HasPrefix
(
ext
,
".jp"
)
{
return
"image/jpeg"
}
// Default to png as a fallback
return
"image/png"
}
}
func
(
a
*
Adaptor
)
DoRequest
(
c
*
gin
.
Context
,
info
*
relaycommon
.
RelayInfo
,
requestBody
io
.
Reader
)
(
any
,
error
)
{
if
info
.
RelayMode
==
constant
.
RelayModeAudioTranscription
||
info
.
RelayMode
==
constant
.
RelayModeAudioTranslation
||
...
...
relay/relay-text.go
View file @
0647872c
...
...
@@ -425,6 +425,11 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
logContent
+=
", "
+
extraContent
}
other
:=
service
.
GenerateTextOtherInfo
(
ctx
,
relayInfo
,
modelRatio
,
groupRatio
,
completionRatio
,
cacheTokens
,
cacheRatio
,
modelPrice
)
if
imageTokens
!=
0
{
other
[
"image"
]
=
true
other
[
"image_ratio"
]
=
imageRatio
other
[
"image_output"
]
=
imageTokens
}
model
.
RecordConsumeLog
(
ctx
,
relayInfo
.
UserId
,
relayInfo
.
ChannelId
,
promptTokens
,
completionTokens
,
logModel
,
tokenName
,
quota
,
logContent
,
relayInfo
.
TokenId
,
userQuota
,
int
(
useTimeSeconds
),
relayInfo
.
IsStream
,
relayInfo
.
Group
,
other
)
}
web/src/components/LogsTable.js
View file @
0647872c
...
...
@@ -987,7 +987,9 @@ const LogsTable = () => {
other
?.
group_ratio
,
other
?.
cache_tokens
||
0
,
other
?.
cache_ratio
||
1.0
,
);
other
?.
image
||
false
,
other
?.
image_ratio
||
0
,
other
?.
image_output
||
0
,);
}
expandDataLocal
.
push
({
key
:
t
(
'计费过程'
),
...
...
web/src/helpers/render.js
View file @
0647872c
...
...
@@ -314,6 +314,9 @@ export function renderModelPrice(
groupRatio
,
cacheTokens
=
0
,
cacheRatio
=
1.0
,
image
=
false
,
imageRatio
=
1.0
,
imageOutputTokens
=
0
,
)
{
if
(
modelPrice
!==
-
1
)
{
return
i18next
.
t
(
...
...
@@ -331,10 +334,15 @@ export function renderModelPrice(
let
inputRatioPrice
=
modelRatio
*
2.0
;
let
completionRatioPrice
=
modelRatio
*
2.0
*
completionRatio
;
let
cacheRatioPrice
=
modelRatio
*
2.0
*
cacheRatio
;
let
imageRatioPrice
=
modelRatio
*
2.0
*
imageRatio
;
// Calculate effective input tokens (non-cached + cached with ratio applied)
cons
t
effectiveInputTokens
=
le
t
effectiveInputTokens
=
inputTokens
-
cacheTokens
+
cacheTokens
*
cacheRatio
;
// Handle image tokens if present
if
(
image
&&
imageOutputTokens
>
0
)
{
effectiveInputTokens
=
inputTokens
-
imageOutputTokens
+
imageOutputTokens
*
imageRatio
;
}
let
price
=
(
effectiveInputTokens
/
1000000
)
*
inputRatioPrice
*
groupRatio
+
...
...
@@ -344,13 +352,13 @@ export function renderModelPrice(
<>
<
article
>
<
p
>
{
i18next
.
t
(
'
提示
价格:${{price}} / 1M tokens'
,
{
{
i18next
.
t
(
'
输入
价格:${{price}} / 1M tokens'
,
{
price
:
inputRatioPrice
,
})}
<
/p
>
<
p
>
{
i18next
.
t
(
'
补全
价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})'
,
'
输出
价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})'
,
{
price
:
inputRatioPrice
,
total
:
completionRatioPrice
,
...
...
@@ -370,11 +378,24 @@ export function renderModelPrice(
)}
<
/p
>
)}
{
image
&&
imageOutputTokens
>
0
&&
(
<
p
>
{
i18next
.
t
(
'图片输入价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (图片倍率: {{imageRatio}})'
,
{
price
:
imageRatioPrice
,
ratio
:
groupRatio
,
total
:
imageRatioPrice
*
groupRatio
,
imageRatio
:
imageRatio
,
},
)}
<
/p
>
)}
<
p
><
/p
>
<
p
>
{
cacheTokens
>
0
{
cacheTokens
>
0
&&
!
image
?
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}}'
,
{
nonCacheInput
:
inputTokens
-
cacheTokens
,
cacheInput
:
cacheTokens
,
...
...
@@ -386,8 +407,22 @@ export function renderModelPrice(
total
:
price
.
toFixed
(
6
),
},
)
:
image
&&
imageOutputTokens
>
0
?
i18next
.
t
(
'输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}'
,
{
nonImageInput
:
inputTokens
-
imageOutputTokens
,
imageInput
:
imageOutputTokens
,
imageRatio
:
imageRatio
,
price
:
inputRatioPrice
,
completion
:
completionTokens
,
compPrice
:
completionRatioPrice
,
ratio
:
groupRatio
,
total
:
price
.
toFixed
(
6
),
},
)
:
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}}'
,
{
input
:
inputTokens
,
price
:
inputRatioPrice
,
...
...
@@ -405,12 +440,53 @@ export function renderModelPrice(
}
}
export
function
renderLogContent
(
modelRatio
,
completionRatio
,
modelPrice
=
-
1
,
groupRatio
,
user_group_ratio
,
image
=
false
,
imageRatio
=
1.0
,
)
{
const
useUserGroupRatio
=
isValidGroupRatio
(
user_group_ratio
);
const
ratioLabel
=
useUserGroupRatio
?
i18next
.
t
(
'专属倍率'
)
:
i18next
.
t
(
'分组倍率'
);
const
ratio
=
useUserGroupRatio
?
user_group_ratio
:
groupRatio
;
if
(
modelPrice
!==
-
1
)
{
return
i18next
.
t
(
'模型价格 ${{price}},{{ratioType}} {{ratio}}'
,
{
price
:
modelPrice
,
ratioType
:
ratioLabel
,
ratio
});
}
else
{
if
(
image
)
{
return
i18next
.
t
(
'模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},图片输入倍率 {{imageRatio}},{{ratioType}} {{ratio}}'
,
{
modelRatio
:
modelRatio
,
completionRatio
:
completionRatio
,
imageRatio
:
imageRatio
,
ratioType
:
ratioLabel
,
ratio
});
}
else
{
return
i18next
.
t
(
'模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}'
,
{
modelRatio
:
modelRatio
,
completionRatio
:
completionRatio
,
ratioType
:
ratioLabel
,
ratio
});
}
}
}
export
function
renderModelPriceSimple
(
modelRatio
,
modelPrice
=
-
1
,
groupRatio
,
cacheTokens
=
0
,
cacheRatio
=
1.0
,
image
=
false
,
imageRatio
=
1.0
,
)
{
if
(
modelPrice
!==
-
1
)
{
return
i18next
.
t
(
'价格:${{price}} * 分组:{{ratio}}'
,
{
...
...
@@ -418,7 +494,28 @@ export function renderModelPriceSimple(
ratio
:
groupRatio
,
});
}
else
{
if
(
cacheTokens
!==
0
)
{
if
(
image
&&
cacheTokens
!==
0
)
{
return
i18next
.
t
(
'模型: {{ratio}} * {{ratioType}}: {{groupRatio}} * 缓存倍率: {{cacheRatio}} * 图片输入倍率: {{imageRatio}}'
,
{
ratio
:
modelRatio
,
ratioType
:
ratioLabel
,
groupRatio
:
groupRatio
,
cacheRatio
:
cacheRatio
,
imageRatio
:
imageRatio
,
},
);
}
else
if
(
image
)
{
return
i18next
.
t
(
'模型: {{ratio}} * {{ratioType}}: {{groupRatio}} * 图片输入倍率: {{imageRatio}}'
,
{
ratio
:
modelRatio
,
ratioType
:
ratioLabel
,
groupRatio
:
groupRatio
,
imageRatio
:
imageRatio
,
},
);
}
else
if
(
cacheTokens
!==
0
)
{
return
i18next
.
t
(
'模型: {{ratio}} * 分组: {{groupRatio}} * 缓存: {{cacheRatio}}'
,
{
...
...
@@ -882,7 +979,7 @@ export function renderClaudeLogContent(
});
}
else
{
return
i18next
.
t
(
'模型倍率 {{modelRatio}},
补全
倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},缓存创建倍率 {{cacheCreationRatio}},{{ratioType}} {{ratio}}'
,
'模型倍率 {{modelRatio}},
输出
倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},缓存创建倍率 {{cacheCreationRatio}},{{ratioType}} {{ratio}}'
,
{
modelRatio
:
modelRatio
,
completionRatio
:
completionRatio
,
...
...
@@ -933,30 +1030,3 @@ export function renderClaudeModelPriceSimple(
}
}
}
export
function
renderLogContent
(
modelRatio
,
completionRatio
,
modelPrice
=
-
1
,
groupRatio
,
)
{
const
ratioLabel
=
false
?
i18next
.
t
(
'专属倍率'
)
:
i18next
.
t
(
'分组倍率'
);
if
(
modelPrice
!==
-
1
)
{
return
i18next
.
t
(
'模型价格 ${{price}},{{ratioType}} {{ratio}}'
,
{
price
:
modelPrice
,
ratioType
:
ratioLabel
,
ratio
:
groupRatio
,
});
}
else
{
return
i18next
.
t
(
'模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},{{ratioType}} {{ratio}}'
,
{
modelRatio
:
modelRatio
,
completionRatio
:
completionRatio
,
ratioType
:
ratioLabel
,
ratio
:
groupRatio
,
},
);
}
}
web/src/i18n/locales/en.json
View file @
0647872c
...
...
@@ -679,7 +679,10 @@
"当前分组可用"
:
"Available in current group"
,
"当前分组不可用"
:
"The current group is unavailable"
,
"提示:"
:
"input:"
,
"输入:"
:
"input:"
,
"补全:"
:
"output:"
,
"输出:"
:
"output:"
,
"图片输出:"
:
"Image output:"
,
"模型价格:"
:
"Model price:"
,
"模型:"
:
"Model:"
,
"分组:"
:
"Grouping:"
,
...
...
@@ -1054,14 +1057,16 @@
"等级"
:
"grade"
,
"钉钉"
:
"DingTalk"
,
"模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}"
:
"Model price: ${{price}} * Group ratio: {{ratio}} = ${{total}}"
,
"提示:${{price}} * {{ratio}} = ${{total}} / 1M tokens"
:
"Prompt: ${{price}} * {{ratio}} = ${{total}} / 1M tokens"
,
"补全:${{price}} * {{ratio}} = ${{total}} / 1M tokens"
:
"Completion: ${{price}} * {{ratio}} = ${{total}} / 1M tokens"
,
"音频提示:${{price}} * {{ratio}} * {{audioRatio}} = ${{total}} / 1M tokens"
:
"Audio prompt: ${{price}} * {{ratio}} * {{audioRatio}} = ${{total}} / 1M tokens"
,
"输入:${{price}} * {{ratio}} = ${{total}} / 1M tokens"
:
"Prompt: ${{price}} * {{ratio}} = ${{total}} / 1M tokens"
,
"输出:${{price}} * {{ratio}} = ${{total}} / 1M tokens"
:
"Completion: ${{price}} * {{ratio}} = ${{total}} / 1M tokens"
,
"图片输入:${{price}} * {{ratio}} = ${{total}} / 1M tokens (图片倍率: {{imageRatio}})"
:
"Image input: ${{price}} * {{ratio}} = ${{total}} / 1M tokens (Image ratio: {{imageRatio}})"
,
"音频输入:${{price}} * {{ratio}} * {{audioRatio}} = ${{total}} / 1M tokens"
:
"Audio prompt: ${{price}} * {{ratio}} * {{audioRatio}} = ${{total}} / 1M tokens"
,
"音频提示 {{input}} tokens / 1M tokens * ${{price}} * {{audioRatio}} + 音频补全 {{completion}} tokens / 1M tokens * ${{price}} * {{audioRatio}} * {{audioCompRatio}}"
:
"Audio prompt {{input}} tokens / 1M tokens * ${{price}} * {{audioRatio}} + Audio completion {{completion}} tokens / 1M tokens * ${{price}} * {{audioRatio}} * {{audioCompRatio}}"
,
"音频补全:${{price}} * {{ratio}} * {{audioRatio}} * {{audioCompRatio}} = ${{total}} / 1M tokens"
:
"Audio completion: ${{price}} * {{ratio}} * {{audioRatio}} * {{audioCompRatio}} = ${{total}} / 1M tokens"
,
"音频输出:${{price}} * {{ratio}} * {{audioRatio}} * {{audioCompRatio}} = ${{total}} / 1M tokens"
:
"Audio completion: ${{price}} * {{ratio}} * {{audioRatio}} * {{audioCompRatio}} = ${{total}} / 1M tokens"
,
"输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}"
:
"Input {{nonImageInput}} tokens + Image input {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}} + Output {{completion}} tokens / 1M tokens * ${{compPrice}} * Group {{ratio}} = ${{total}}"
,
"(文字 + 音频)* 分组倍率 {{ratio}} = ${{total}}"
:
"(Text + Audio) * Group ratio {{ratio}} = ${{total}}"
,
"文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} +"
:
"Text prompt {{input}} tokens / 1M tokens * ${{price}} + Text completion {{completion}} tokens / 1M tokens * ${{compPrice}} +"
,
"
提示 {{input}} tokens / 1M tokens * ${{price}} + 补全
{{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}"
:
"Prompt {{input}} tokens / 1M tokens * ${{price}} + Completion {{completion}} tokens / 1M tokens * ${{compPrice}} * Group {{ratio}} = ${{total}}"
,
"
输入 {{input}} tokens / 1M tokens * ${{price}} + 输出
{{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}"
:
"Prompt {{input}} tokens / 1M tokens * ${{price}} + Completion {{completion}} tokens / 1M tokens * ${{compPrice}} * Group {{ratio}} = ${{total}}"
,
"价格:${{price}} * 分组:{{ratio}}"
:
"Price: ${{price}} * Group: {{ratio}}"
,
"模型: {{ratio}} * 分组: {{groupRatio}}"
:
"Model: {{ratio}} * Group: {{groupRatio}}"
,
"统计额度"
:
"Statistical quota"
,
...
...
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