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
ad900bbb
authored
Jul 11, 2026
by
t0ng7u
Browse files
Options
Browse Files
Download
Plain Diff
Merge remote-tracking branch 'origin/main'
parents
308e3e34
269e4ff3
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
437 additions
and
80 deletions
+437
-80
common/quota_math.go
+44
-13
common/quota_math_test.go
+18
-0
dto/openai_image.go
+9
-4
pkg/billingexpr/round.go
+5
-0
relay/channel/openai/image_stream_test.go
+0
-0
relay/channel/openai/relay_image.go
+99
-46
relay/helper/openai_image_request_test.go
+11
-0
relay/helper/price.go
+30
-5
relay/helper/price_test.go
+135
-1
relay/image_handler.go
+0
-10
service/billing.go
+17
-0
service/quota_saturation_test.go
+39
-0
service/text_quota_test.go
+28
-0
types/request_meta.go
+2
-1
No files found.
common/quota_math.go
View file @
ad900bbb
...
...
@@ -16,11 +16,14 @@ const (
MinQuota
=
math
.
MinInt32
)
// QuotaClampKind identifies why a quota conversion had to be saturated.
type
QuotaClampKind
string
// Clamp kinds reported by QuotaClamp.Kind.
const
(
QuotaClampOverflow
=
"overflow"
QuotaClampUnderflow
=
"underflow"
QuotaClampNaN
=
"nan"
QuotaClampOverflow
QuotaClampKind
=
"overflow"
QuotaClampUnderflow
QuotaClampKind
=
"underflow"
QuotaClampNaN
QuotaClampKind
=
"nan"
)
// QuotaClamp describes a single saturation event: a quota conversion whose
...
...
@@ -28,10 +31,19 @@ const (
// therefore clamped. It is surfaced to billing callers so the event can be
// recorded on the related consume/task log for admin auditing.
type
QuotaClamp
struct
{
Op
string
`json:"op"`
// "QuotaFromFloat" | "QuotaRound" | "QuotaFromDecimal"
Kind
string
`json:"kind"`
// "overflow" | "underflow" | "nan"
Original
float64
`json:"original"`
// best-effort pre-clamp value (decimal -> float64 approx)
Clamped
int
`json:"clamped"`
// the saturated result actually used
Op
string
`json:"op"`
// "QuotaFromFloat" | "QuotaRound" | "QuotaFromDecimal"
Kind
QuotaClampKind
`json:"kind"`
// "overflow" | "underflow" | "nan"
Original
float64
`json:"original"`
// best-effort pre-clamp value (decimal -> float64 approx)
Clamped
int
`json:"clamped"`
// the saturated result actually used
}
// Error lets the same typed value serve both as the settlement audit marker
// and as the fail-fast error returned by strict pre-consume conversions.
func
(
c
*
QuotaClamp
)
Error
()
string
{
if
c
==
nil
{
return
""
}
return
fmt
.
Sprintf
(
"quota conversion (%s) %s: original=%g, clamped=%d"
,
c
.
Op
,
c
.
Kind
,
c
.
Original
,
c
.
Clamped
)
}
// AuditMap renders the clamp as the marker stored under a log's
...
...
@@ -58,19 +70,26 @@ func (c *QuotaClamp) AuditMap() map[string]interface{} {
// record the event (e.g. on the consume log); the returned pointer is nil for
// in-range values.
func
saturateQuota
(
value
float64
,
op
string
)
(
int
,
*
QuotaClamp
)
{
var
clamp
*
QuotaClamp
switch
{
case
math
.
IsNaN
(
value
)
:
SysError
(
fmt
.
Sprintf
(
"quota conversion (%s) received NaN, falling back to 0"
,
op
))
return
0
,
&
QuotaClamp
{
Op
:
op
,
Kind
:
QuotaClampNaN
,
Original
:
value
,
Clamped
:
0
}
clamp
=
&
QuotaClamp
{
Op
:
op
,
Kind
:
QuotaClampNaN
,
Original
:
value
,
Clamped
:
0
}
case
value
>=
MaxQuota
:
SysError
(
fmt
.
Sprintf
(
"quota conversion (%s) overflow: %g exceeds max quota, clamped to %d"
,
op
,
value
,
MaxQuota
))
return
MaxQuota
,
&
QuotaClamp
{
Op
:
op
,
Kind
:
QuotaClampOverflow
,
Original
:
value
,
Clamped
:
MaxQuota
}
clamp
=
&
QuotaClamp
{
Op
:
op
,
Kind
:
QuotaClampOverflow
,
Original
:
value
,
Clamped
:
MaxQuota
}
case
value
<=
MinQuota
:
SysError
(
fmt
.
Sprintf
(
"quota conversion (%s) underflow: %g below min quota, clamped to %d"
,
op
,
value
,
MinQuota
))
return
MinQuota
,
&
QuotaClamp
{
Op
:
op
,
Kind
:
QuotaClampUnderflow
,
Original
:
value
,
Clamped
:
MinQuota
}
clamp
=
&
QuotaClamp
{
Op
:
op
,
Kind
:
QuotaClampUnderflow
,
Original
:
value
,
Clamped
:
MinQuota
}
default
:
return
int
(
value
),
nil
}
SysError
(
clamp
.
Error
())
return
clamp
.
Clamped
,
clamp
}
func
strictQuota
(
quota
int
,
clamp
*
QuotaClamp
)
(
int
,
error
)
{
if
clamp
!=
nil
{
return
0
,
clamp
}
return
quota
,
nil
}
// QuotaFromFloat converts a computed quota value to int, truncating toward
...
...
@@ -87,6 +106,12 @@ func QuotaFromFloatChecked(value float64) (int, *QuotaClamp) {
return
saturateQuota
(
value
,
"QuotaFromFloat"
)
}
// QuotaFromFloatStrict converts an in-range value and returns a typed
// *QuotaClamp error instead of allowing a saturated result to reach billing.
func
QuotaFromFloatStrict
(
value
float64
)
(
int
,
error
)
{
return
strictQuota
(
QuotaFromFloatChecked
(
value
))
}
// QuotaRound converts a float64 quota value to int using half-away-from-zero
// rounding, with saturation. Every tiered billing path (pre-consume,
// settlement, breakdown validation, log fields) MUST use this to avoid +-1
...
...
@@ -102,6 +127,12 @@ func QuotaRoundChecked(value float64) (int, *QuotaClamp) {
return
saturateQuota
(
math
.
Round
(
value
),
"QuotaRound"
)
}
// QuotaRoundStrict rounds an in-range value and returns a typed *QuotaClamp
// error instead of allowing a saturated result to reach billing.
func
QuotaRoundStrict
(
value
float64
)
(
int
,
error
)
{
return
strictQuota
(
QuotaRoundChecked
(
value
))
}
// QuotaFromDecimal converts a computed quota decimal to int with saturation.
// The decimal is rounded (half away from zero) before conversion.
func
QuotaFromDecimal
(
d
decimal
.
Decimal
)
int
{
...
...
common/quota_math_test.go
View file @
ad900bbb
...
...
@@ -6,6 +6,7 @@ import (
"github.com/shopspring/decimal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// 2000 quota per call * n=18446744073686646784 overflows int64; the constant
...
...
@@ -78,6 +79,23 @@ func TestQuotaFromFloatChecked(t *testing.T) {
}
}
func
TestQuotaFromFloatStrictReturnsTypedClampError
(
t
*
testing
.
T
)
{
quota
,
err
:=
QuotaFromFloatStrict
(
42.9
)
require
.
NoError
(
t
,
err
)
assert
.
Equal
(
t
,
42
,
quota
)
quota
,
err
=
QuotaFromFloatStrict
(
overflowingProduct
)
assert
.
Zero
(
t
,
quota
)
var
clamp
*
QuotaClamp
require
.
ErrorAs
(
t
,
err
,
&
clamp
)
assert
.
Equal
(
t
,
QuotaClampOverflow
,
clamp
.
Kind
)
assert
.
Equal
(
t
,
MaxQuota
,
clamp
.
Clamped
)
assert
.
ErrorContains
(
t
,
err
,
"QuotaFromFloat"
)
assert
.
ErrorContains
(
t
,
err
,
"overflow"
)
assert
.
ErrorContains
(
t
,
err
,
"original="
)
assert
.
ErrorContains
(
t
,
err
,
"clamped=2147483647"
)
}
// TestQuotaRoundChecked verifies the rounding entry point reports clamps the
// same way.
func
TestQuotaRoundChecked
(
t
*
testing
.
T
)
{
...
...
dto/openai_image.go
View file @
ad900bbb
...
...
@@ -155,14 +155,19 @@ func (i *ImageRequest) GetTokenCountMeta() *types.TokenCountMeta {
}
}
// n is NOT included here; it is handled via OtherRatio("n") in
// image_handler.go (default) or channel adaptors (actual count).
// Including n here caused double-counting for channels that also
// set OtherRatio("n") (e.g. Ali/Bailian).
imageN
:=
uint
(
1
)
if
i
.
N
!=
nil
&&
*
i
.
N
>
0
{
imageN
=
*
i
.
N
}
// Keep n separate from ImagePriceRatio so size/quality and count remain
// independent billing dimensions. Fixed-price pre-consume stores this on
// PriceData, and image settlement reuses or replaces the same "n" ratio.
return
&
types
.
TokenCountMeta
{
CombineText
:
i
.
Prompt
,
MaxTokens
:
1584
,
ImagePriceRatio
:
sizeRatio
*
qualityRatio
,
BillingRatios
:
map
[
string
]
float64
{
"n"
:
float64
(
imageN
)},
}
}
...
...
pkg/billingexpr/round.go
View file @
ad900bbb
...
...
@@ -12,3 +12,8 @@ import "github.com/QuantumNous/new-api/common"
func
QuotaRound
(
f
float64
)
int
{
return
common
.
QuotaRound
(
f
)
}
// QuotaRoundStrict rejects an unrepresentable pre-consume estimate.
func
QuotaRoundStrict
(
f
float64
)
(
int
,
error
)
{
return
common
.
QuotaRoundStrict
(
f
)
}
relay/channel/openai/image_stream_test.go
View file @
ad900bbb
This diff is collapsed.
Click to expand it.
relay/channel/openai/relay_image.go
View file @
ad900bbb
This diff is collapsed.
Click to expand it.
relay/helper/openai_image_request_test.go
View file @
ad900bbb
...
...
@@ -109,6 +109,16 @@ func TestGetAndValidOpenAIImageRequestNBounds(t *testing.T) {
wantN
:
dto
.
MaxImageN
,
},
{
name
:
"explicit n is accepted"
,
body
:
`{"model":"gpt-image-1","prompt":"a cat","n":3}`
,
wantN
:
3
,
},
{
name
:
"zero n defaults to 1"
,
body
:
`{"model":"gpt-image-1","prompt":"a cat","n":0}`
,
wantN
:
1
,
},
{
name
:
"absent n defaults to 1"
,
body
:
`{"model":"gpt-image-1","prompt":"a cat"}`
,
wantN
:
1
,
...
...
@@ -127,6 +137,7 @@ func TestGetAndValidOpenAIImageRequestNBounds(t *testing.T) {
require
.
NoError
(
t
,
err
)
require
.
NotNil
(
t
,
req
.
N
)
require
.
Equal
(
t
,
tt
.
wantN
,
*
req
.
N
)
require
.
Equal
(
t
,
float64
(
tt
.
wantN
),
req
.
GetTokenCountMeta
()
.
BillingRatios
[
"n"
])
})
}
...
...
relay/helper/price.go
View file @
ad900bbb
...
...
@@ -117,12 +117,15 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
audioRatio
=
ratio_setting
.
GetAudioRatio
(
info
.
OriginModelName
)
audioCompletionRatio
=
ratio_setting
.
GetAudioCompletionRatio
(
info
.
OriginModelName
)
ratio
:=
modelRatio
*
groupRatioInfo
.
GroupRatio
preConsumedQuota
=
common
.
QuotaFromFloat
(
float64
(
preConsumedTokens
)
*
ratio
)
quota
,
err
:=
common
.
QuotaFromFloatStrict
(
float64
(
preConsumedTokens
)
*
ratio
)
if
err
!=
nil
{
return
types
.
PriceData
{},
err
}
preConsumedQuota
=
quota
}
else
{
if
meta
.
ImagePriceRatio
!=
0
{
modelPrice
=
modelPrice
*
meta
.
ImagePriceRatio
}
preConsumedQuota
=
common
.
QuotaFromFloat
(
modelPrice
*
common
.
QuotaPerUnit
*
groupRatioInfo
.
GroupRatio
)
}
// check if free model pre-consume is disabled
...
...
@@ -160,6 +163,17 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
CacheCreation1hRatio
:
cacheCreationRatio1h
,
QuotaToPreConsume
:
preConsumedQuota
,
}
if
usePrice
{
for
name
,
ratio
:=
range
meta
.
BillingRatios
{
priceData
.
AddOtherRatio
(
name
,
ratio
)
}
quotaToPreConsume
:=
priceData
.
ApplyOtherRatiosToFloat
(
modelPrice
*
common
.
QuotaPerUnit
*
groupRatioInfo
.
GroupRatio
)
quota
,
err
:=
common
.
QuotaFromFloatStrict
(
quotaToPreConsume
)
if
err
!=
nil
{
return
types
.
PriceData
{},
err
}
priceData
.
QuotaToPreConsume
=
quota
}
if
common
.
DebugEnabled
{
logger
.
LogDebug
(
c
,
"model_price_helper result: %s"
,
priceData
.
ToSetting
())
...
...
@@ -199,7 +213,11 @@ func ModelPriceHelperPerCall(c *gin.Context, info *relaycommon.RelayInfo) (types
freeModel
:=
false
if
usePrice
{
quota
=
common
.
QuotaFromFloat
(
modelPrice
*
common
.
QuotaPerUnit
*
groupRatioInfo
.
GroupRatio
)
var
err
error
quota
,
err
=
common
.
QuotaFromFloatStrict
(
modelPrice
*
common
.
QuotaPerUnit
*
groupRatioInfo
.
GroupRatio
)
if
err
!=
nil
{
return
types
.
PriceData
{},
err
}
if
!
operation_setting
.
GetQuotaSetting
()
.
EnableFreeModelPreConsume
{
if
groupRatioInfo
.
GroupRatio
==
0
||
modelPrice
==
0
{
quota
=
0
...
...
@@ -208,7 +226,11 @@ func ModelPriceHelperPerCall(c *gin.Context, info *relaycommon.RelayInfo) (types
}
}
else
{
// 按量计费:以模型倍率的一半作为预扣额度
quota
=
common
.
QuotaFromFloat
(
modelRatio
/
2
*
common
.
QuotaPerUnit
*
groupRatioInfo
.
GroupRatio
)
var
err
error
quota
,
err
=
common
.
QuotaFromFloatStrict
(
modelRatio
/
2
*
common
.
QuotaPerUnit
*
groupRatioInfo
.
GroupRatio
)
if
err
!=
nil
{
return
types
.
PriceData
{},
err
}
modelPrice
=
-
1
if
!
operation_setting
.
GetQuotaSetting
()
.
EnableFreeModelPreConsume
{
if
groupRatioInfo
.
GroupRatio
==
0
||
modelRatio
==
0
{
...
...
@@ -270,7 +292,10 @@ func modelPriceHelperTiered(c *gin.Context, info *relaycommon.RelayInfo, promptT
// Expression coefficients are $/1M tokens prices; convert to quota the same way per-call billing does.
quotaBeforeGroup
:=
rawCost
/
1
_000_000
*
common
.
QuotaPerUnit
preConsumedQuota
:=
billingexpr
.
QuotaRound
(
quotaBeforeGroup
*
groupRatioInfo
.
GroupRatio
)
preConsumedQuota
,
err
:=
billingexpr
.
QuotaRoundStrict
(
quotaBeforeGroup
*
groupRatioInfo
.
GroupRatio
)
if
err
!=
nil
{
return
types
.
PriceData
{},
err
}
freeModel
:=
false
if
!
operation_setting
.
GetQuotaSetting
()
.
EnableFreeModelPreConsume
{
...
...
relay/helper/price_test.go
View file @
ad900bbb
...
...
@@ -10,6 +10,7 @@ import (
relaycommon
"github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/setting/billing_setting"
"github.com/QuantumNous/new-api/setting/config"
"github.com/QuantumNous/new-api/setting/ratio_setting"
"github.com/QuantumNous/new-api/types"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
...
...
@@ -52,7 +53,9 @@ func TestModelPriceHelperTieredUsesPreloadedRequestInput(t *testing.T) {
},
}
priceData
,
err
:=
ModelPriceHelper
(
ctx
,
info
,
1000
,
&
types
.
TokenCountMeta
{})
priceData
,
err
:=
ModelPriceHelper
(
ctx
,
info
,
1000
,
&
types
.
TokenCountMeta
{
BillingRatios
:
map
[
string
]
float64
{
"n"
:
3
},
})
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
1500
,
priceData
.
QuotaToPreConsume
)
require
.
NotNil
(
t
,
info
.
TieredBillingSnapshot
)
...
...
@@ -138,3 +141,134 @@ func TestModelPriceHelperTieredPreConsumeMaxTokensFallback(t *testing.T) {
})
}
}
func
TestModelPriceHelperTieredRejectsPreConsumeOverflow
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
saved
:=
map
[
string
]
string
{}
require
.
NoError
(
t
,
config
.
GlobalConfig
.
SaveToDB
(
func
(
key
,
value
string
)
error
{
saved
[
key
]
=
value
return
nil
}))
t
.
Cleanup
(
func
()
{
require
.
NoError
(
t
,
config
.
GlobalConfig
.
LoadFromDB
(
saved
))
})
require
.
NoError
(
t
,
config
.
GlobalConfig
.
LoadFromDB
(
map
[
string
]
string
{
"billing_setting.billing_mode"
:
`{"tiered-overflow-model":"tiered_expr"}`
,
"billing_setting.billing_expr"
:
`{"tiered-overflow-model":"tier(\"overflow\", p * 1000000000000000)"}`
,
"group_ratio_setting.group_ratio"
:
`{"default":1}`
,
}))
recorder
:=
httptest
.
NewRecorder
()
ctx
,
_
:=
gin
.
CreateTestContext
(
recorder
)
ctx
.
Request
=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/v1/chat/completions"
,
nil
)
ctx
.
Set
(
"group"
,
"default"
)
info
:=
&
relaycommon
.
RelayInfo
{
OriginModelName
:
"tiered-overflow-model"
,
UserGroup
:
"default"
,
UsingGroup
:
"default"
,
BillingRequestInput
:
&
billingexpr
.
RequestInput
{
Body
:
[]
byte
(
`{}`
),
},
}
_
,
err
:=
ModelPriceHelper
(
ctx
,
info
,
1000
,
&
types
.
TokenCountMeta
{})
var
clamp
*
common
.
QuotaClamp
require
.
ErrorAs
(
t
,
err
,
&
clamp
)
require
.
Equal
(
t
,
"QuotaRound"
,
clamp
.
Op
)
require
.
Equal
(
t
,
common
.
QuotaClampOverflow
,
clamp
.
Kind
)
}
func
TestModelPriceHelperRequestBillingRatiosOnlyApplyToFixedPrice
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
savedModelPrices
:=
ratio_setting
.
ModelPrice2JSONString
()
savedModelRatios
:=
ratio_setting
.
ModelRatio2JSONString
()
t
.
Cleanup
(
func
()
{
require
.
NoError
(
t
,
ratio_setting
.
UpdateModelPriceByJSONString
(
savedModelPrices
))
require
.
NoError
(
t
,
ratio_setting
.
UpdateModelRatioByJSONString
(
savedModelRatios
))
})
modelPrices
,
err
:=
common
.
Marshal
(
map
[
string
]
float64
{
"fixed-image-price"
:
0.04
,
"fractional-image-price"
:
0.0000012
,
"overflow-image-price"
:
float64
(
common
.
MaxQuota
)
/
common
.
QuotaPerUnit
/
2
,
})
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
ratio_setting
.
UpdateModelPriceByJSONString
(
string
(
modelPrices
)))
modelRatios
,
err
:=
common
.
Marshal
(
map
[
string
]
float64
{
"ratio-image-price"
:
15
})
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
ratio_setting
.
UpdateModelRatioByJSONString
(
string
(
modelRatios
)))
tests
:=
[]
struct
{
name
string
model
string
wantQuota
int
wantUsePrice
bool
wantImageCount
bool
}{
{
name
:
"fixed price applies image count"
,
model
:
"fixed-image-price"
,
wantQuota
:
180000
,
wantUsePrice
:
true
,
wantImageCount
:
true
,
},
{
name
:
"ratio price ignores request billing ratios"
,
model
:
"ratio-image-price"
,
wantQuota
:
15000
,
wantUsePrice
:
false
,
},
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
ctx
,
_
:=
gin
.
CreateTestContext
(
httptest
.
NewRecorder
())
ctx
.
Set
(
"group"
,
"default"
)
info
:=
&
relaycommon
.
RelayInfo
{
OriginModelName
:
tt
.
model
,
UserGroup
:
"default"
,
UsingGroup
:
"default"
,
}
meta
:=
&
types
.
TokenCountMeta
{
ImagePriceRatio
:
3
,
BillingRatios
:
map
[
string
]
float64
{
"n"
:
3
},
}
priceData
,
err
:=
ModelPriceHelper
(
ctx
,
info
,
1000
,
meta
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
tt
.
wantQuota
,
priceData
.
QuotaToPreConsume
)
require
.
Equal
(
t
,
tt
.
wantUsePrice
,
priceData
.
UsePrice
)
require
.
Equal
(
t
,
tt
.
wantImageCount
,
priceData
.
HasOtherRatio
(
"n"
))
require
.
Equal
(
t
,
priceData
.
OtherRatios
(),
info
.
PriceData
.
OtherRatios
())
})
}
newInfo
:=
func
(
model
string
)
(
*
gin
.
Context
,
*
relaycommon
.
RelayInfo
)
{
ctx
,
_
:=
gin
.
CreateTestContext
(
httptest
.
NewRecorder
())
ctx
.
Set
(
"group"
,
"default"
)
return
ctx
,
&
relaycommon
.
RelayInfo
{
OriginModelName
:
model
,
UserGroup
:
"default"
,
UsingGroup
:
"default"
,
}
}
meta
:=
&
types
.
TokenCountMeta
{
BillingRatios
:
map
[
string
]
float64
{
"n"
:
3
}}
ctx
,
info
:=
newInfo
(
"fractional-image-price"
)
priceData
,
err
:=
ModelPriceHelper
(
ctx
,
info
,
0
,
meta
)
require
.
NoError
(
t
,
err
)
// 0.0000012 * 500000 * 3 = 1.8, then truncate once to 1.
require
.
Equal
(
t
,
1
,
priceData
.
QuotaToPreConsume
)
ctx
,
info
=
newInfo
(
"overflow-image-price"
)
_
,
err
=
ModelPriceHelper
(
ctx
,
info
,
0
,
meta
)
var
clamp
*
common
.
QuotaClamp
require
.
ErrorAs
(
t
,
err
,
&
clamp
)
require
.
Equal
(
t
,
"QuotaFromFloat"
,
clamp
.
Op
)
require
.
Equal
(
t
,
common
.
QuotaClampOverflow
,
clamp
.
Kind
)
require
.
Nil
(
t
,
info
.
Billing
)
}
relay/image_handler.go
View file @
ad900bbb
...
...
@@ -123,16 +123,6 @@ func ImageHelper(c *gin.Context, info *relaycommon.RelayInfo) (newAPIError *type
imageN
=
*
request
.
N
}
// n is handled via OtherRatio so it is applied exactly once in quota
// calculation (both price-based and ratio-based paths).
// 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
!
info
.
PriceData
.
HasOtherRatio
(
"n"
)
{
info
.
PriceData
.
AddOtherRatio
(
"n"
,
float64
(
imageN
))
}
}
if
usage
.
(
*
dto
.
Usage
)
.
TotalTokens
==
0
{
usage
.
(
*
dto
.
Usage
)
.
TotalTokens
=
1
}
...
...
service/billing.go
View file @
ad900bbb
...
...
@@ -2,6 +2,7 @@ package service
import
(
"fmt"
"net/http"
"github.com/QuantumNous/new-api/logger"
relaycommon
"github.com/QuantumNous/new-api/relay/common"
...
...
@@ -17,6 +18,22 @@ const (
// PreConsumeBilling 根据用户计费偏好创建 BillingSession 并执行预扣费。
// 会话存储在 relayInfo.Billing 上,供后续 Settle / Refund 使用。
func
PreConsumeBilling
(
c
*
gin
.
Context
,
preConsumedQuota
int
,
relayInfo
*
relaycommon
.
RelayInfo
)
*
types
.
NewAPIError
{
if
relayInfo
!=
nil
&&
relayInfo
.
QuotaClamp
!=
nil
{
return
types
.
NewErrorWithStatusCode
(
relayInfo
.
QuotaClamp
,
types
.
ErrorCodeModelPriceError
,
http
.
StatusBadRequest
,
types
.
ErrOptionWithSkipRetry
(),
)
}
if
preConsumedQuota
<
0
{
return
types
.
NewErrorWithStatusCode
(
fmt
.
Errorf
(
"pre-consume quota cannot be negative: %d"
,
preConsumedQuota
),
types
.
ErrorCodeModelPriceError
,
http
.
StatusBadRequest
,
types
.
ErrOptionWithSkipRetry
(),
)
}
session
,
apiErr
:=
NewBillingSession
(
c
,
relayInfo
,
preConsumedQuota
)
if
apiErr
!=
nil
{
return
apiErr
...
...
service/quota_saturation_test.go
View file @
ad900bbb
package
service
import
(
"net/http"
"testing"
"github.com/QuantumNous/new-api/common"
relaycommon
"github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/types"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
...
...
@@ -72,3 +74,40 @@ func TestAttachQuotaSaturationNoClampNoMarker(t *testing.T) {
_
,
hasAdmin
:=
other
[
"admin_info"
]
require
.
False
(
t
,
hasAdmin
,
"no admin_info should be added when there is no clamp"
)
}
func
TestPreConsumeBillingRejectsSaturatedQuotaBeforeDeduction
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
c
,
_
:=
gin
.
CreateTestContext
(
nil
)
info
:=
&
relaycommon
.
RelayInfo
{
QuotaClamp
:
&
common
.
QuotaClamp
{
Op
:
"QuotaFromFloat"
,
Kind
:
common
.
QuotaClampOverflow
,
Original
:
1e30
,
Clamped
:
common
.
MaxQuota
,
},
}
apiErr
:=
PreConsumeBilling
(
c
,
common
.
MaxQuota
,
info
)
require
.
NotNil
(
t
,
apiErr
)
require
.
Equal
(
t
,
types
.
ErrorCodeModelPriceError
,
apiErr
.
GetErrorCode
())
require
.
Equal
(
t
,
http
.
StatusBadRequest
,
apiErr
.
StatusCode
)
require
.
Same
(
t
,
info
.
QuotaClamp
,
apiErr
.
Err
)
var
clamp
*
common
.
QuotaClamp
require
.
ErrorAs
(
t
,
apiErr
,
&
clamp
)
require
.
Same
(
t
,
info
.
QuotaClamp
,
clamp
)
require
.
Nil
(
t
,
info
.
Billing
)
}
func
TestPreConsumeBillingRejectsNegativeQuotaBeforeDeduction
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
c
,
_
:=
gin
.
CreateTestContext
(
nil
)
info
:=
&
relaycommon
.
RelayInfo
{}
apiErr
:=
PreConsumeBilling
(
c
,
-
1
,
info
)
require
.
NotNil
(
t
,
apiErr
)
require
.
Equal
(
t
,
types
.
ErrorCodeModelPriceError
,
apiErr
.
GetErrorCode
())
require
.
Equal
(
t
,
http
.
StatusBadRequest
,
apiErr
.
StatusCode
)
require
.
Nil
(
t
,
info
.
Billing
)
}
service/text_quota_test.go
View file @
ad900bbb
...
...
@@ -490,3 +490,31 @@ func TestTryTieredSettleNoClampInRange(t *testing.T) {
require
.
NotNil
(
t
,
result
)
require
.
Nil
(
t
,
relayInfo
.
QuotaClamp
,
"in-range settlement must not record a clamp"
)
}
func
TestCalculateTextQuotaSummaryFixedPriceAppliesImageCountOnceAndAllowsOverride
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
ctx
,
_
:=
gin
.
CreateTestContext
(
httptest
.
NewRecorder
())
priceData
:=
types
.
PriceData
{
ModelPrice
:
0.12
,
UsePrice
:
true
,
GroupRatioInfo
:
types
.
GroupRatioInfo
{
GroupRatio
:
1
,
},
}
priceData
.
AddOtherRatio
(
"n"
,
3
)
relayInfo
:=
&
relaycommon
.
RelayInfo
{
OriginModelName
:
"dall-e-3"
,
PriceData
:
priceData
,
StartTime
:
time
.
Now
(),
}
usage
:=
&
dto
.
Usage
{
PromptTokens
:
1
,
TotalTokens
:
1
}
summary
:=
calculateTextQuotaSummary
(
ctx
,
relayInfo
,
usage
)
require
.
Equal
(
t
,
180000
,
summary
.
Quota
)
// An adaptor-reported actual count replaces the requested count rather
// than multiplying it a second time.
relayInfo
.
PriceData
.
AddOtherRatio
(
"n"
,
2
)
summary
=
calculateTextQuotaSummary
(
ctx
,
relayInfo
,
usage
)
require
.
Equal
(
t
,
120000
,
summary
.
Quota
)
}
types/request_meta.go
View file @
ad900bbb
...
...
@@ -26,7 +26,8 @@ type TokenCountMeta struct {
Files
[]
*
FileMeta
`json:"files,omitempty"`
// List of files, each with type and content
MaxTokens
int
`json:"max_tokens,omitempty"`
// Maximum tokens allowed in the request
ImagePriceRatio
float64
`json:"image_ratio,omitempty"`
// Ratio for image size, if applicable
ImagePriceRatio
float64
`json:"image_ratio,omitempty"`
// Ratio for image size, if applicable
BillingRatios
map
[
string
]
float64
`json:"billing_ratios,omitempty"`
// Validated request multipliers used by pre-consume billing
//IsStreaming bool `json:"is_streaming,omitempty"` // Indicates if the request is streaming
}
...
...
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