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
Unverified
Commit
fc1259f5
authored
Jul 07, 2026
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
refactor(price): improve handling of other ratios in PriceData
parent
394b023d
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
255 additions
and
55 deletions
+255
-55
controller/relay.go
+1
-1
relay/chat_completions_via_responses_test.go
+45
-0
relay/image_handler.go
+1
-1
relay/relay_task.go
+15
-25
service/task_billing.go
+18
-10
service/task_billing_test.go
+99
-0
service/text_quota.go
+2
-11
types/price_data.go
+74
-7
No files found.
controller/relay.go
View file @
fc1259f5
...
...
@@ -588,7 +588,7 @@ func RelayTask(c *gin.Context) {
ModelPrice
:
relayInfo
.
PriceData
.
ModelPrice
,
GroupRatio
:
relayInfo
.
PriceData
.
GroupRatioInfo
.
GroupRatio
,
ModelRatio
:
relayInfo
.
PriceData
.
ModelRatio
,
OtherRatios
:
relayInfo
.
PriceData
.
OtherRatios
,
OtherRatios
:
relayInfo
.
PriceData
.
OtherRatios
()
,
OriginModelName
:
relayInfo
.
OriginModelName
,
PerCallBilling
:
common
.
StringsContains
(
constant
.
TaskPricePatches
,
relayInfo
.
OriginModelName
)
||
relayInfo
.
PriceData
.
UsePrice
,
}
...
...
relay/chat_completions_via_responses_test.go
View file @
fc1259f5
package
relay
import
(
"math"
"testing"
relaycommon
"github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func
TestIsResponsesEventStreamContentType
(
t
*
testing
.
T
)
{
...
...
@@ -24,3 +28,44 @@ func TestIsResponsesEventStreamContentType(t *testing.T) {
})
}
}
func
TestRecalcQuotaFromRatiosIgnoresInvalidMultipliers
(
t
*
testing
.
T
)
{
info
:=
&
relaycommon
.
RelayInfo
{
PriceData
:
types
.
PriceData
{
Quota
:
100
,
},
}
info
.
PriceData
.
AddOtherRatio
(
"duration"
,
2
)
quota
,
ok
:=
recalcQuotaFromRatios
(
info
,
map
[
string
]
float64
{
"duration"
:
3
,
"zero"
:
0
,
"negative"
:
-
1
,
"nan"
:
math
.
NaN
(),
"inf"
:
math
.
Inf
(
1
),
})
require
.
True
(
t
,
ok
)
assert
.
Equal
(
t
,
150
,
quota
)
assert
.
True
(
t
,
info
.
PriceData
.
HasOtherRatio
(
"duration"
))
}
func
TestRecalcQuotaFromRatiosRejectsAllInvalidAdjustedRatios
(
t
*
testing
.
T
)
{
info
:=
&
relaycommon
.
RelayInfo
{
PriceData
:
types
.
PriceData
{
Quota
:
100
,
},
}
info
.
PriceData
.
AddOtherRatio
(
"duration"
,
2
)
quota
,
ok
:=
recalcQuotaFromRatios
(
info
,
map
[
string
]
float64
{
"zero"
:
0
,
"negative"
:
-
1
,
"nan"
:
math
.
NaN
(),
"inf"
:
math
.
Inf
(
1
),
})
require
.
False
(
t
,
ok
)
assert
.
Equal
(
t
,
0
,
quota
)
assert
.
True
(
t
,
info
.
PriceData
.
HasOtherRatio
(
"duration"
))
}
relay/image_handler.go
View file @
fc1259f5
...
...
@@ -128,7 +128,7 @@ func ImageHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *type
// Adaptors may have already set a more accurate count from the
// upstream response; only set the default when they haven't.
if
info
.
PriceData
.
UsePrice
{
// only price model use N ratio
if
_
,
hasN
:=
info
.
PriceData
.
OtherRatios
[
"n"
];
!
hasN
{
if
!
info
.
PriceData
.
HasOtherRatio
(
"n"
)
{
info
.
PriceData
.
AddOtherRatio
(
"n"
,
float64
(
imageN
))
}
}
...
...
relay/relay_task.go
View file @
fc1259f5
...
...
@@ -196,12 +196,7 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (*TaskSubmitRe
// 6. 将 OtherRatios 应用到基础额度(饱和转换,防止溢出成负数)
if
!
common
.
StringsContains
(
constant
.
TaskPricePatches
,
modelName
)
{
quotaWithRatios
:=
float64
(
info
.
PriceData
.
Quota
)
for
_
,
ra
:=
range
info
.
PriceData
.
OtherRatios
{
if
ra
!=
1.0
{
quotaWithRatios
*=
ra
}
}
quotaWithRatios
:=
info
.
PriceData
.
ApplyOtherRatiosToFloat
(
float64
(
info
.
PriceData
.
Quota
))
quota
,
clamp
:=
common
.
QuotaFromFloatChecked
(
quotaWithRatios
)
info
.
PriceData
.
Quota
=
quota
noteTaskQuotaClamp
(
info
,
clamp
)
...
...
@@ -232,7 +227,7 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (*TaskSubmitRe
}
// 10. 返回 OtherRatios 给下游(header 必须在 DoResponse 写 body 之前设置)
otherRatios
:=
info
.
PriceData
.
OtherRatios
otherRatios
:=
info
.
PriceData
.
OtherRatios
()
if
otherRatios
==
nil
{
otherRatios
=
map
[
string
]
float64
{}
}
...
...
@@ -248,10 +243,12 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (*TaskSubmitRe
// 11. 提交后计费调整:让适配器根据上游实际返回调整 OtherRatios
finalQuota
:=
info
.
PriceData
.
Quota
if
adjustedRatios
:=
adaptor
.
AdjustBillingOnSubmit
(
info
,
taskData
);
len
(
adjustedRatios
)
>
0
{
// 基于调整后的 ratios 重新计算 quota
finalQuota
=
recalcQuotaFromRatios
(
info
,
adjustedRatios
)
info
.
PriceData
.
OtherRatios
=
adjustedRatios
info
.
PriceData
.
Quota
=
finalQuota
if
adjustedQuota
,
ok
:=
recalcQuotaFromRatios
(
info
,
adjustedRatios
);
ok
{
// 基于调整后的 ratios 重新计算 quota
finalQuota
=
adjustedQuota
info
.
PriceData
.
ReplaceOtherRatios
(
adjustedRatios
)
info
.
PriceData
.
Quota
=
finalQuota
}
}
return
&
TaskSubmitResult
{
...
...
@@ -264,25 +261,18 @@ func RelayTaskSubmit(c *gin.Context, info *relaycommon.RelayInfo) (*TaskSubmitRe
// recalcQuotaFromRatios 根据 adjustedRatios 重新计算 quota。
// 公式: baseQuota × ∏(ratio) — 其中 baseQuota 是不含 OtherRatios 的基础额度。
func
recalcQuotaFromRatios
(
info
*
relaycommon
.
RelayInfo
,
ratios
map
[
string
]
float64
)
int
{
func
recalcQuotaFromRatios
(
info
*
relaycommon
.
RelayInfo
,
ratios
map
[
string
]
float64
)
(
int
,
bool
)
{
// 从 PriceData 获取不含 OtherRatios 的基础价格
baseQuota
:=
float64
(
info
.
PriceData
.
Quota
)
// 先除掉原有的 OtherRatios 恢复基础额度
for
_
,
ra
:=
range
info
.
PriceData
.
OtherRatios
{
if
ra
!=
1.0
&&
ra
>
0
{
baseQuota
/=
ra
}
baseQuota
:=
info
.
PriceData
.
RemoveOtherRatiosFromFloat
(
float64
(
info
.
PriceData
.
Quota
))
priceData
:=
info
.
PriceData
if
!
priceData
.
ReplaceOtherRatios
(
ratios
)
{
return
0
,
false
}
// 应用新的 ratios
result
:=
baseQuota
for
_
,
ra
:=
range
ratios
{
if
ra
!=
1.0
{
result
*=
ra
}
}
result
:=
priceData
.
ApplyOtherRatiosToFloat
(
baseQuota
)
quota
,
clamp
:=
common
.
QuotaFromFloatChecked
(
result
)
noteTaskQuotaClamp
(
info
,
clamp
)
return
quota
return
quota
,
true
}
// noteTaskQuotaClamp records the first quota saturation event onto the task's
...
...
service/task_billing.go
View file @
fc1259f5
...
...
@@ -11,6 +11,7 @@ import (
"github.com/QuantumNous/new-api/model"
relaycommon
"github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/setting/ratio_setting"
"github.com/QuantumNous/new-api/types"
"github.com/gin-gonic/gin"
)
...
...
@@ -23,9 +24,9 @@ func LogTaskConsumption(c *gin.Context, info *relaycommon.RelayInfo) {
if
common
.
StringsContains
(
constant
.
TaskPricePatches
,
info
.
OriginModelName
)
{
logContent
=
fmt
.
Sprintf
(
"%s,按次计费"
,
logContent
)
}
else
{
if
len
(
info
.
PriceData
.
O
therRatios
)
>
0
{
if
otherRatios
:=
info
.
PriceData
.
OtherRatios
();
len
(
o
therRatios
)
>
0
{
var
contents
[]
string
for
key
,
ra
:=
range
info
.
PriceData
.
O
therRatios
{
for
key
,
ra
:=
range
o
therRatios
{
if
1.0
!=
ra
{
contents
=
append
(
contents
,
fmt
.
Sprintf
(
"%s: %.2f"
,
key
,
ra
))
}
...
...
@@ -126,8 +127,8 @@ func taskBillingOther(task *model.Task) map[string]interface{} {
other
[
"model_ratio"
]
=
bc
.
ModelRatio
}
other
[
"group_ratio"
]
=
bc
.
GroupRatio
if
len
(
bc
.
OtherRatios
)
>
0
{
for
k
,
v
:=
range
bc
.
OtherRatios
{
if
priceData
:=
taskBillingContextPriceData
(
bc
);
priceData
!=
nil
{
for
k
,
v
:=
range
priceData
.
OtherRatios
()
{
other
[
k
]
=
v
}
}
...
...
@@ -140,6 +141,17 @@ func taskBillingOther(task *model.Task) map[string]interface{} {
return
other
}
func
taskBillingContextPriceData
(
bc
*
model
.
TaskBillingContext
)
*
types
.
PriceData
{
if
bc
==
nil
||
len
(
bc
.
OtherRatios
)
==
0
{
return
nil
}
priceData
:=
&
types
.
PriceData
{}
if
!
priceData
.
ReplaceOtherRatios
(
bc
.
OtherRatios
)
{
return
nil
}
return
priceData
}
// taskModelName 从 BillingContext 或 Properties 中获取模型名称。
func
taskModelName
(
task
*
model
.
Task
)
string
{
if
bc
:=
task
.
PrivateData
.
BillingContext
;
bc
!=
nil
&&
bc
.
OriginModelName
!=
""
{
...
...
@@ -294,12 +306,8 @@ func RecalculateTaskQuotaByTokens(ctx context.Context, task *model.Task, totalTo
// 计算 OtherRatios 乘积(视频折扣、时长等)
otherMultiplier
:=
1.0
if
bc
:=
task
.
PrivateData
.
BillingContext
;
bc
!=
nil
{
for
_
,
r
:=
range
bc
.
OtherRatios
{
if
r
!=
1.0
&&
r
>
0
{
otherMultiplier
*=
r
}
}
if
priceData
:=
taskBillingContextPriceData
(
task
.
PrivateData
.
BillingContext
);
priceData
!=
nil
{
otherMultiplier
=
priceData
.
OtherRatioMultiplier
()
}
// 计算实际应扣费额度: totalTokens * modelRatio * groupRatio * otherMultiplier(饱和转换,防止溢出成负数)
...
...
service/task_billing_test.go
View file @
fc1259f5
...
...
@@ -3,6 +3,7 @@ package service
import
(
"context"
"encoding/json"
"math"
"net/http"
"os"
"testing"
...
...
@@ -11,7 +12,9 @@ import (
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
relaycommon
"github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/types"
"github.com/glebarez/sqlite"
"github.com/shopspring/decimal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
...
...
@@ -139,6 +142,102 @@ func makeTask(userId, channelId, quota, tokenId int, billingSource string, subsc
}
}
func
TestPriceDataOtherRatiosFilterAndSnapshot
(
t
*
testing
.
T
)
{
priceData
:=
types
.
PriceData
{}
priceData
.
AddOtherRatio
(
"zero"
,
0
)
priceData
.
AddOtherRatio
(
"negative"
,
-
0.5
)
priceData
.
AddOtherRatio
(
"nan"
,
math
.
NaN
())
priceData
.
AddOtherRatio
(
"inf"
,
math
.
Inf
(
1
))
priceData
.
AddOtherRatio
(
"one"
,
1
)
priceData
.
AddOtherRatio
(
"positive"
,
2.5
)
ratios
:=
priceData
.
OtherRatios
()
require
.
Len
(
t
,
ratios
,
2
)
assert
.
Equal
(
t
,
1.0
,
ratios
[
"one"
])
assert
.
Equal
(
t
,
2.5
,
ratios
[
"positive"
])
assert
.
True
(
t
,
priceData
.
HasOtherRatio
(
"one"
))
assert
.
False
(
t
,
priceData
.
HasOtherRatio
(
"zero"
))
ratios
[
"positive"
]
=
99
ratios
[
"new"
]
=
3
nextSnapshot
:=
priceData
.
OtherRatios
()
assert
.
Equal
(
t
,
2.5
,
nextSnapshot
[
"positive"
])
assert
.
NotContains
(
t
,
nextSnapshot
,
"new"
)
}
func
TestPriceDataReplaceAndApplyOtherRatios
(
t
*
testing
.
T
)
{
priceData
:=
types
.
PriceData
{}
replaced
:=
priceData
.
ReplaceOtherRatios
(
map
[
string
]
float64
{
"zero"
:
0
,
"negative"
:
-
3
,
"nan"
:
math
.
NaN
(),
"inf"
:
math
.
Inf
(
1
),
"one"
:
1
,
"duration"
:
2
,
"size"
:
1.5
,
})
require
.
True
(
t
,
replaced
)
assert
.
Equal
(
t
,
3.0
,
priceData
.
OtherRatioMultiplier
())
assert
.
Equal
(
t
,
30.0
,
priceData
.
ApplyOtherRatiosToFloat
(
10
))
assert
.
Equal
(
t
,
10.0
,
priceData
.
RemoveOtherRatiosFromFloat
(
30
))
assert
.
True
(
t
,
decimal
.
NewFromInt
(
30
)
.
Equal
(
priceData
.
ApplyOtherRatiosToDecimal
(
decimal
.
NewFromInt
(
10
))))
replaced
=
priceData
.
ReplaceOtherRatios
(
map
[
string
]
float64
{
"zero"
:
0
,
"nan"
:
math
.
NaN
(),
})
require
.
False
(
t
,
replaced
)
assert
.
Nil
(
t
,
priceData
.
OtherRatios
())
assert
.
Equal
(
t
,
1.0
,
priceData
.
OtherRatioMultiplier
())
}
func
TestTaskBillingOtherFiltersHistoricalOtherRatios
(
t
*
testing
.
T
)
{
task
:=
makeTask
(
1
,
1
,
100
,
0
,
BillingSourceWallet
,
0
)
task
.
PrivateData
.
BillingContext
.
OtherRatios
=
map
[
string
]
float64
{
"seconds"
:
2
,
"identity"
:
1
,
"zero"
:
0
,
"negative"
:
-
1
,
"nan"
:
math
.
NaN
(),
"inf"
:
math
.
Inf
(
1
),
}
other
:=
taskBillingOther
(
task
)
assert
.
Equal
(
t
,
2.0
,
other
[
"seconds"
])
assert
.
Equal
(
t
,
1.0
,
other
[
"identity"
])
assert
.
NotContains
(
t
,
other
,
"zero"
)
assert
.
NotContains
(
t
,
other
,
"negative"
)
assert
.
NotContains
(
t
,
other
,
"nan"
)
assert
.
NotContains
(
t
,
other
,
"inf"
)
}
func
TestTaskBillingContextPriceDataFiltersMultiplier
(
t
*
testing
.
T
)
{
priceData
:=
taskBillingContextPriceData
(
&
model
.
TaskBillingContext
{
OtherRatios
:
map
[
string
]
float64
{
"seconds"
:
2
,
"size"
:
3
,
"identity"
:
1
,
"zero"
:
0
,
"negative"
:
-
1
,
"nan"
:
math
.
NaN
(),
"inf"
:
math
.
Inf
(
1
),
},
})
require
.
NotNil
(
t
,
priceData
)
assert
.
Equal
(
t
,
6.0
,
priceData
.
OtherRatioMultiplier
())
assert
.
Equal
(
t
,
map
[
string
]
float64
{
"seconds"
:
2
,
"size"
:
3
,
"identity"
:
1
,
},
priceData
.
OtherRatios
())
}
// ---------------------------------------------------------------------------
// Read-back helpers
// ---------------------------------------------------------------------------
...
...
service/text_quota.go
View file @
fc1259f5
...
...
@@ -296,12 +296,7 @@ func calculateTextQuotaSummary(ctx *gin.Context, relayInfo *relaycommon.RelayInf
quotaCalculateDecimal
:=
promptQuota
.
Add
(
completionQuota
)
.
Mul
(
ratio
)
quotaCalculateDecimal
=
quotaCalculateDecimal
.
Add
(
summary
.
ToolCallSurchargeQuota
)
quotaCalculateDecimal
=
quotaCalculateDecimal
.
Add
(
audioInputQuota
)
if
len
(
relayInfo
.
PriceData
.
OtherRatios
)
>
0
{
for
_
,
otherRatio
:=
range
relayInfo
.
PriceData
.
OtherRatios
{
quotaCalculateDecimal
=
quotaCalculateDecimal
.
Mul
(
decimal
.
NewFromFloat
(
otherRatio
))
}
}
quotaCalculateDecimal
=
relayInfo
.
PriceData
.
ApplyOtherRatiosToDecimal
(
quotaCalculateDecimal
)
if
!
ratio
.
IsZero
()
&&
quotaCalculateDecimal
.
LessThanOrEqual
(
decimal
.
Zero
)
{
quotaCalculateDecimal
=
decimal
.
NewFromInt
(
1
)
...
...
@@ -313,11 +308,7 @@ func calculateTextQuotaSummary(ctx *gin.Context, relayInfo *relaycommon.RelayInf
quotaCalculateDecimal
:=
dModelPrice
.
Mul
(
dQuotaPerUnit
)
.
Mul
(
dGroupRatio
)
quotaCalculateDecimal
=
quotaCalculateDecimal
.
Add
(
summary
.
ToolCallSurchargeQuota
)
quotaCalculateDecimal
=
quotaCalculateDecimal
.
Add
(
audioInputQuota
)
if
len
(
relayInfo
.
PriceData
.
OtherRatios
)
>
0
{
for
_
,
otherRatio
:=
range
relayInfo
.
PriceData
.
OtherRatios
{
quotaCalculateDecimal
=
quotaCalculateDecimal
.
Mul
(
decimal
.
NewFromFloat
(
otherRatio
))
}
}
quotaCalculateDecimal
=
relayInfo
.
PriceData
.
ApplyOtherRatiosToDecimal
(
quotaCalculateDecimal
)
quota
,
clamp
:=
common
.
QuotaFromDecimalChecked
(
quotaCalculateDecimal
)
summary
.
Quota
=
quota
noteQuotaClamp
(
relayInfo
,
clamp
)
...
...
types/price_data.go
View file @
fc1259f5
...
...
@@ -3,6 +3,8 @@ package types
import
(
"fmt"
"math"
"github.com/shopspring/decimal"
)
type
GroupRatioInfo
struct
{
...
...
@@ -23,7 +25,7 @@ type PriceData struct {
ImageRatio
float64
AudioRatio
float64
AudioCompletionRatio
float64
O
therRatios
map
[
string
]
float64
o
therRatios
map
[
string
]
float64
UsePrice
bool
Quota
int
// 按次计费的最终额度(MJ / Task)
QuotaToPreConsume
int
// 按量计费的预消耗额度
...
...
@@ -31,15 +33,80 @@ type PriceData struct {
}
func
(
p
*
PriceData
)
AddOtherRatio
(
key
string
,
ratio
float64
)
{
if
p
.
OtherRatios
==
nil
{
p
.
OtherRatios
=
make
(
map
[
string
]
float64
)
if
!
isValidOtherRatio
(
ratio
)
{
return
}
if
p
.
otherRatios
==
nil
{
p
.
otherRatios
=
make
(
map
[
string
]
float64
)
}
p
.
otherRatios
[
key
]
=
ratio
}
func
(
p
*
PriceData
)
ReplaceOtherRatios
(
ratios
map
[
string
]
float64
)
bool
{
p
.
otherRatios
=
nil
for
key
,
ratio
:=
range
ratios
{
p
.
AddOtherRatio
(
key
,
ratio
)
}
return
len
(
p
.
otherRatios
)
>
0
}
func
(
p
*
PriceData
)
HasOtherRatio
(
key
string
)
bool
{
ratio
,
ok
:=
p
.
otherRatios
[
key
]
return
ok
&&
isValidOtherRatio
(
ratio
)
}
func
(
p
*
PriceData
)
OtherRatios
()
map
[
string
]
float64
{
if
len
(
p
.
otherRatios
)
==
0
{
return
nil
}
ratios
:=
make
(
map
[
string
]
float64
,
len
(
p
.
otherRatios
))
for
key
,
ratio
:=
range
p
.
otherRatios
{
if
isValidOtherRatio
(
ratio
)
{
ratios
[
key
]
=
ratio
}
}
if
len
(
ratios
)
==
0
{
return
nil
}
return
ratios
}
func
(
p
*
PriceData
)
OtherRatioMultiplier
()
float64
{
multiplier
:=
1.0
for
_
,
ratio
:=
range
p
.
otherRatios
{
if
isValidOtherRatio
(
ratio
)
&&
ratio
!=
1.0
{
multiplier
*=
ratio
}
}
return
multiplier
}
func
(
p
*
PriceData
)
ApplyOtherRatiosToFloat
(
value
float64
)
float64
{
return
value
*
p
.
OtherRatioMultiplier
()
}
func
(
p
*
PriceData
)
ApplyOtherRatiosToDecimal
(
value
decimal
.
Decimal
)
decimal
.
Decimal
{
for
_
,
ratio
:=
range
p
.
otherRatios
{
if
isValidOtherRatio
(
ratio
)
&&
ratio
!=
1.0
{
value
=
value
.
Mul
(
decimal
.
NewFromFloat
(
ratio
))
}
}
return
value
}
func
(
p
*
PriceData
)
RemoveOtherRatiosFromFloat
(
value
float64
)
float64
{
for
_
,
ratio
:=
range
p
.
otherRatios
{
if
isValidOtherRatio
(
ratio
)
&&
ratio
!=
1.0
{
value
/=
ratio
}
}
return
value
}
func
isValidOtherRatio
(
ratio
float64
)
bool
{
// NaN/Inf would poison every downstream quota multiplication
// (int(NaN * quota) wraps to a negative charge).
if
!
(
ratio
>
0
)
||
math
.
IsInf
(
ratio
,
1
)
{
return
}
p
.
OtherRatios
[
key
]
=
ratio
return
ratio
>
0
&&
!
math
.
IsInf
(
ratio
,
1
)
}
func
(
p
*
PriceData
)
ToSetting
()
string
{
...
...
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