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
5b03b39d
authored
Mar 16, 2026
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: enhance tiered billing logic and improve variable handling in pricing calculations
parent
f6c0852d
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
432 additions
and
240 deletions
+432
-240
.gitignore
+2
-1
pkg/billingexpr/compile.go
+53
-6
relay/compatible_handler.go
+6
-11
service/quota.go
+10
-15
service/tiered_settle.go
+57
-0
service/tiered_settle_test.go
+182
-0
web/src/components/table/model-pricing/modal/components/DynamicPricingBreakdown.jsx
+21
-99
web/src/helpers/render.jsx
+71
-87
web/src/helpers/utils.jsx
+27
-19
web/src/pages/Setting/Ratio/components/TieredPricingEditor.jsx
+3
-2
No files found.
.gitignore
View file @
5b03b39d
...
...
@@ -29,6 +29,6 @@ data/
.gomodcache/
.gocache-temp
.gopath
.test
token_estimator_test.go
skills-lock.json
\ No newline at end of file
pkg/billingexpr/compile.go
View file @
5b03b39d
...
...
@@ -6,14 +6,20 @@ import (
"sync"
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/ast"
"github.com/expr-lang/expr/vm"
)
const
maxCacheSize
=
256
type
cachedEntry
struct
{
prog
*
vm
.
Program
usedVars
map
[
string
]
bool
}
var
(
cacheMu
sync
.
RWMutex
cache
=
make
(
map
[
string
]
*
vm
.
Program
,
64
)
cache
=
make
(
map
[
string
]
*
cachedEntry
,
64
)
)
// compileEnvPrototype is the type-checking prototype used at compile time.
...
...
@@ -67,9 +73,9 @@ func CompileFromCacheByHash(exprStr, hash string) (*vm.Program, error) {
func
compileFromCacheByHash
(
exprStr
,
hash
string
)
(
*
vm
.
Program
,
error
)
{
cacheMu
.
RLock
()
if
prog
,
ok
:=
cache
[
hash
];
ok
{
if
entry
,
ok
:=
cache
[
hash
];
ok
{
cacheMu
.
RUnlock
()
return
prog
,
nil
return
entry
.
prog
,
nil
}
cacheMu
.
RUnlock
()
...
...
@@ -78,20 +84,61 @@ func compileFromCacheByHash(exprStr, hash string) (*vm.Program, error) {
return
nil
,
fmt
.
Errorf
(
"expr compile error: %w"
,
err
)
}
vars
:=
extractUsedVars
(
prog
)
cacheMu
.
Lock
()
if
len
(
cache
)
>=
maxCacheSize
{
cache
=
make
(
map
[
string
]
*
vm
.
Program
,
64
)
cache
=
make
(
map
[
string
]
*
cachedEntry
,
64
)
}
cache
[
hash
]
=
prog
cache
[
hash
]
=
&
cachedEntry
{
prog
:
prog
,
usedVars
:
vars
}
cacheMu
.
Unlock
()
return
prog
,
nil
}
func
extractUsedVars
(
prog
*
vm
.
Program
)
map
[
string
]
bool
{
vars
:=
make
(
map
[
string
]
bool
)
node
:=
prog
.
Node
()
ast
.
Find
(
node
,
func
(
n
ast
.
Node
)
bool
{
if
id
,
ok
:=
n
.
(
*
ast
.
IdentifierNode
);
ok
{
vars
[
id
.
Value
]
=
true
}
return
false
})
return
vars
}
// UsedVars returns the set of identifier names referenced by an expression.
// The result is cached alongside the compiled program. Returns nil for empty input.
func
UsedVars
(
exprStr
string
)
map
[
string
]
bool
{
if
exprStr
==
""
{
return
nil
}
hash
:=
ExprHashString
(
exprStr
)
cacheMu
.
RLock
()
if
entry
,
ok
:=
cache
[
hash
];
ok
{
cacheMu
.
RUnlock
()
return
entry
.
usedVars
}
cacheMu
.
RUnlock
()
// Compile (and cache) to populate usedVars
if
_
,
err
:=
compileFromCacheByHash
(
exprStr
,
hash
);
err
!=
nil
{
return
nil
}
cacheMu
.
RLock
()
entry
,
ok
:=
cache
[
hash
]
cacheMu
.
RUnlock
()
if
ok
{
return
entry
.
usedVars
}
return
nil
}
// InvalidateCache clears the compiled-expression cache.
// Called when billing rules are updated.
func
InvalidateCache
()
{
cacheMu
.
Lock
()
cache
=
make
(
map
[
string
]
*
vm
.
Program
,
64
)
cache
=
make
(
map
[
string
]
*
cachedEntry
,
64
)
cacheMu
.
Unlock
()
}
relay/compatible_handler.go
View file @
5b03b39d
...
...
@@ -238,17 +238,13 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage
}
// Tiered billing: only determines quota, logging continues through normal path
isClaudeUsageSemantic
:=
relayInfo
.
GetFinalRequestRelayFormat
()
==
types
.
RelayFormatClaude
var
tieredUsedVars
map
[
string
]
bool
if
snap
:=
relayInfo
.
TieredBillingSnapshot
;
snap
!=
nil
{
tieredUsedVars
=
billingexpr
.
UsedVars
(
snap
.
ExprString
)
}
var
tieredResult
*
billingexpr
.
TieredResult
tieredOk
,
tieredQuota
,
tieredRes
:=
service
.
TryTieredSettle
(
relayInfo
,
billingexpr
.
TokenParams
{
P
:
float64
(
usage
.
PromptTokens
),
C
:
float64
(
usage
.
CompletionTokens
),
CR
:
float64
(
usage
.
PromptTokensDetails
.
CachedTokens
),
CC
:
float64
(
usage
.
PromptTokensDetails
.
CachedCreationTokens
-
usage
.
ClaudeCacheCreation1hTokens
),
CC1h
:
float64
(
usage
.
ClaudeCacheCreation1hTokens
),
Img
:
float64
(
usage
.
PromptTokensDetails
.
ImageTokens
),
AI
:
float64
(
usage
.
PromptTokensDetails
.
AudioTokens
),
AO
:
float64
(
usage
.
CompletionTokenDetails
.
AudioTokens
),
})
tieredOk
,
tieredQuota
,
tieredRes
:=
service
.
TryTieredSettle
(
relayInfo
,
service
.
BuildTieredTokenParams
(
usage
,
isClaudeUsageSemantic
,
tieredUsedVars
))
if
tieredOk
{
tieredResult
=
tieredRes
}
...
...
@@ -354,7 +350,6 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage
var
audioInputQuota
decimal
.
Decimal
var
audioInputPrice
float64
isClaudeUsageSemantic
:=
relayInfo
.
GetFinalRequestRelayFormat
()
==
types
.
RelayFormatClaude
if
!
relayInfo
.
PriceData
.
UsePrice
{
baseTokens
:=
dPromptTokens
// 减去 cached tokens
...
...
service/quota.go
View file @
5b03b39d
...
...
@@ -256,14 +256,12 @@ func PostClaudeConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo,
ObserveChannelAffinityUsageCacheByRelayFormat
(
ctx
,
usage
,
relayInfo
.
GetFinalRequestRelayFormat
())
}
var
tieredUsedVars
map
[
string
]
bool
if
snap
:=
relayInfo
.
TieredBillingSnapshot
;
snap
!=
nil
{
tieredUsedVars
=
billingexpr
.
UsedVars
(
snap
.
ExprString
)
}
var
tieredResult
*
billingexpr
.
TieredResult
tieredOk
,
tieredQuota
,
tieredRes
:=
TryTieredSettle
(
relayInfo
,
billingexpr
.
TokenParams
{
P
:
float64
(
usage
.
PromptTokens
),
C
:
float64
(
usage
.
CompletionTokens
),
CR
:
float64
(
usage
.
PromptTokensDetails
.
CachedTokens
),
CC
:
float64
(
usage
.
PromptTokensDetails
.
CachedCreationTokens
-
usage
.
ClaudeCacheCreation1hTokens
),
CC1h
:
float64
(
usage
.
ClaudeCacheCreation1hTokens
),
})
tieredOk
,
tieredQuota
,
tieredRes
:=
TryTieredSettle
(
relayInfo
,
BuildTieredTokenParams
(
usage
,
true
,
tieredUsedVars
))
if
tieredOk
{
tieredResult
=
tieredRes
}
...
...
@@ -394,14 +392,12 @@ func CalcOpenRouterCacheCreateTokens(usage dto.Usage, priceData types.PriceData)
func
PostAudioConsumeQuota
(
ctx
*
gin
.
Context
,
relayInfo
*
relaycommon
.
RelayInfo
,
usage
*
dto
.
Usage
,
extraContent
string
)
{
var
tieredUsedVars
map
[
string
]
bool
if
snap
:=
relayInfo
.
TieredBillingSnapshot
;
snap
!=
nil
{
tieredUsedVars
=
billingexpr
.
UsedVars
(
snap
.
ExprString
)
}
var
tieredResult
*
billingexpr
.
TieredResult
tieredOk
,
tieredQuota
,
tieredRes
:=
TryTieredSettle
(
relayInfo
,
billingexpr
.
TokenParams
{
P
:
float64
(
usage
.
PromptTokens
),
C
:
float64
(
usage
.
CompletionTokens
),
CR
:
float64
(
usage
.
PromptTokensDetails
.
CachedTokens
),
AI
:
float64
(
usage
.
PromptTokensDetails
.
AudioTokens
),
AO
:
float64
(
usage
.
CompletionTokenDetails
.
AudioTokens
),
})
tieredOk
,
tieredQuota
,
tieredRes
:=
TryTieredSettle
(
relayInfo
,
BuildTieredTokenParams
(
usage
,
false
,
tieredUsedVars
))
if
tieredOk
{
tieredResult
=
tieredRes
}
...
...
@@ -659,4 +655,3 @@ func checkAndSendSubscriptionQuotaNotify(relayInfo *relaycommon.RelayInfo) {
}
})
}
service/tiered_settle.go
View file @
5b03b39d
package
service
import
(
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/pkg/billingexpr"
relaycommon
"github.com/QuantumNous/new-api/relay/common"
)
...
...
@@ -8,6 +9,62 @@ import (
// TieredResultWrapper wraps billingexpr.TieredResult for use at the service layer.
type
TieredResultWrapper
=
billingexpr
.
TieredResult
// BuildTieredTokenParams constructs billingexpr.TokenParams from a dto.Usage,
// normalizing P and C so they mean "tokens not separately priced by the
// expression". Sub-categories (cache, image, audio) are only subtracted
// when the expression references them via their own variable.
//
// GPT-format APIs report prompt_tokens / completion_tokens as totals that
// include all sub-categories (cache, image, audio). Claude-format APIs
// report them as text-only. This function normalizes to text-only when
// sub-categories are separately priced.
func
BuildTieredTokenParams
(
usage
*
dto
.
Usage
,
isClaudeUsageSemantic
bool
,
usedVars
map
[
string
]
bool
)
billingexpr
.
TokenParams
{
p
:=
float64
(
usage
.
PromptTokens
)
c
:=
float64
(
usage
.
CompletionTokens
)
cr
:=
float64
(
usage
.
PromptTokensDetails
.
CachedTokens
)
ccTotal
:=
float64
(
usage
.
PromptTokensDetails
.
CachedCreationTokens
)
cc1h
:=
float64
(
usage
.
ClaudeCacheCreation1hTokens
)
img
:=
float64
(
usage
.
PromptTokensDetails
.
ImageTokens
)
ai
:=
float64
(
usage
.
PromptTokensDetails
.
AudioTokens
)
ao
:=
float64
(
usage
.
CompletionTokenDetails
.
AudioTokens
)
if
!
isClaudeUsageSemantic
{
if
usedVars
[
"cr"
]
||
usedVars
[
"cache_read_tokens"
]
{
p
-=
cr
}
if
usedVars
[
"cc"
]
||
usedVars
[
"cc1h"
]
||
usedVars
[
"cache_create_tokens"
]
||
usedVars
[
"cache_create_1h_tokens"
]
{
p
-=
ccTotal
}
if
usedVars
[
"img"
]
||
usedVars
[
"image_tokens"
]
{
p
-=
img
}
if
usedVars
[
"ai"
]
||
usedVars
[
"audio_input_tokens"
]
{
p
-=
ai
}
if
usedVars
[
"ao"
]
||
usedVars
[
"audio_output_tokens"
]
{
c
-=
ao
}
}
if
p
<
0
{
p
=
0
}
if
c
<
0
{
c
=
0
}
return
billingexpr
.
TokenParams
{
P
:
p
,
C
:
c
,
CR
:
cr
,
CC
:
ccTotal
-
cc1h
,
CC1h
:
cc1h
,
Img
:
img
,
AI
:
ai
,
AO
:
ao
,
}
}
// TryTieredSettle checks if the request uses tiered_expr billing and, if so,
// computes the actual quota using the frozen BillingSnapshot. Returns:
// - ok=true, quota, result when tiered billing applies
...
...
service/tiered_settle_test.go
View file @
5b03b39d
package
service
import
(
"math"
"testing"
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/pkg/billingexpr"
relaycommon
"github.com/QuantumNous/new-api/relay/common"
)
...
...
@@ -405,3 +407,183 @@ func TestTryTieredSettle_ErrorFallbackToEstimatedQuotaAfterGroup(t *testing.T) {
t
.
Fatal
(
"result should be nil on error fallback"
)
}
}
// ---------------------------------------------------------------------------
// BuildTieredTokenParams: token normalization and ratio parity tests
// ---------------------------------------------------------------------------
func
tieredQuota
(
exprStr
string
,
usage
*
dto
.
Usage
,
isClaudeSemantic
bool
,
groupRatio
float64
)
float64
{
usedVars
:=
billingexpr
.
UsedVars
(
exprStr
)
params
:=
BuildTieredTokenParams
(
usage
,
isClaudeSemantic
,
usedVars
)
cost
,
_
,
_
:=
billingexpr
.
RunExpr
(
exprStr
,
params
)
return
cost
/
1
_000_000
*
testQuotaPerUnit
*
groupRatio
}
func
ratioQuota
(
usage
*
dto
.
Usage
,
isClaudeSemantic
bool
,
modelRatio
,
completionRatio
,
cacheRatio
,
imageRatio
,
groupRatio
float64
)
float64
{
baseTokens
:=
float64
(
usage
.
PromptTokens
)
cacheTokens
:=
float64
(
usage
.
PromptTokensDetails
.
CachedTokens
)
ccTokens
:=
float64
(
usage
.
PromptTokensDetails
.
CachedCreationTokens
)
imgTokens
:=
float64
(
usage
.
PromptTokensDetails
.
ImageTokens
)
if
!
isClaudeSemantic
{
baseTokens
-=
cacheTokens
baseTokens
-=
ccTokens
baseTokens
-=
imgTokens
}
promptQuota
:=
baseTokens
+
cacheTokens
*
cacheRatio
+
imgTokens
*
imageRatio
completionQuota
:=
float64
(
usage
.
CompletionTokens
)
*
completionRatio
return
(
promptQuota
+
completionQuota
)
*
modelRatio
*
groupRatio
}
func
TestBuildTieredTokenParams_GPT_WithCache
(
t
*
testing
.
T
)
{
usage
:=
&
dto
.
Usage
{
PromptTokens
:
1000
,
CompletionTokens
:
500
,
PromptTokensDetails
:
dto
.
InputTokenDetails
{
CachedTokens
:
200
,
TextTokens
:
800
,
},
}
expr
:=
`tier("base", p * 2.5 + c * 15 + cr * 0.25)`
got
:=
tieredQuota
(
expr
,
usage
,
false
,
1.0
)
// P=800, C=500, CR=200 → (800*2.5 + 500*15 + 200*0.25) * 0.5 = 4775
want
:=
4775.0
if
math
.
Abs
(
got
-
want
)
>
0.01
{
t
.
Fatalf
(
"quota = %f, want %f"
,
got
,
want
)
}
}
func
TestBuildTieredTokenParams_GPT_NoCacheVar
(
t
*
testing
.
T
)
{
usage
:=
&
dto
.
Usage
{
PromptTokens
:
1000
,
CompletionTokens
:
500
,
PromptTokensDetails
:
dto
.
InputTokenDetails
{
CachedTokens
:
200
,
TextTokens
:
800
,
},
}
expr
:=
`tier("base", p * 2.5 + c * 15)`
got
:=
tieredQuota
(
expr
,
usage
,
false
,
1.0
)
// No cr → P=1000 (cache stays in P), C=500 → (1000*2.5 + 500*15) * 0.5 = 5000
want
:=
5000.0
if
math
.
Abs
(
got
-
want
)
>
0.01
{
t
.
Fatalf
(
"quota = %f, want %f"
,
got
,
want
)
}
}
func
TestBuildTieredTokenParams_GPT_WithImage
(
t
*
testing
.
T
)
{
usage
:=
&
dto
.
Usage
{
PromptTokens
:
1000
,
CompletionTokens
:
500
,
PromptTokensDetails
:
dto
.
InputTokenDetails
{
ImageTokens
:
200
,
TextTokens
:
800
,
},
}
expr
:=
`tier("base", p * 2 + c * 8 + img * 2.5)`
got
:=
tieredQuota
(
expr
,
usage
,
false
,
1.0
)
// P=800, C=500, Img=200 → (800*2 + 500*8 + 200*2.5) * 0.5 = 3050
want
:=
3050.0
if
math
.
Abs
(
got
-
want
)
>
0.01
{
t
.
Fatalf
(
"quota = %f, want %f"
,
got
,
want
)
}
}
func
TestBuildTieredTokenParams_Claude_WithCache
(
t
*
testing
.
T
)
{
usage
:=
&
dto
.
Usage
{
PromptTokens
:
800
,
CompletionTokens
:
500
,
PromptTokensDetails
:
dto
.
InputTokenDetails
{
CachedTokens
:
200
,
TextTokens
:
800
,
},
}
expr
:=
`tier("base", p * 3 + c * 15 + cr * 0.3)`
got
:=
tieredQuota
(
expr
,
usage
,
true
,
1.0
)
// Claude: P=800 (no subtraction), C=500, CR=200 → (800*3 + 500*15 + 200*0.3) * 0.5 = 4980
want
:=
4980.0
if
math
.
Abs
(
got
-
want
)
>
0.01
{
t
.
Fatalf
(
"quota = %f, want %f"
,
got
,
want
)
}
}
func
TestBuildTieredTokenParams_GPT_AudioOutput
(
t
*
testing
.
T
)
{
usage
:=
&
dto
.
Usage
{
PromptTokens
:
1000
,
CompletionTokens
:
600
,
CompletionTokenDetails
:
dto
.
OutputTokenDetails
{
AudioTokens
:
100
,
TextTokens
:
500
,
},
}
expr
:=
`tier("base", p * 2 + c * 10 + ao * 50)`
got
:=
tieredQuota
(
expr
,
usage
,
false
,
1.0
)
// C=600-100=500, AO=100 → (1000*2 + 500*10 + 100*50) * 0.5 = 6000
want
:=
6000.0
if
math
.
Abs
(
got
-
want
)
>
0.01
{
t
.
Fatalf
(
"quota = %f, want %f"
,
got
,
want
)
}
}
func
TestBuildTieredTokenParams_GPT_AudioOutputNoVar
(
t
*
testing
.
T
)
{
usage
:=
&
dto
.
Usage
{
PromptTokens
:
1000
,
CompletionTokens
:
600
,
CompletionTokenDetails
:
dto
.
OutputTokenDetails
{
AudioTokens
:
100
,
TextTokens
:
500
,
},
}
expr
:=
`tier("base", p * 2 + c * 10)`
got
:=
tieredQuota
(
expr
,
usage
,
false
,
1.0
)
// No ao → C=600 (audio stays in C) → (1000*2 + 600*10) * 0.5 = 4000
want
:=
4000.0
if
math
.
Abs
(
got
-
want
)
>
0.01
{
t
.
Fatalf
(
"quota = %f, want %f"
,
got
,
want
)
}
}
func
TestBuildTieredTokenParams_ParityWithRatio
(
t
*
testing
.
T
)
{
// GPT-5.4 prices: input=$2.5, output=$15, cacheRead=$0.25
// Ratio equivalents: modelRatio=1.25, completionRatio=6, cacheRatio=0.1
usage
:=
&
dto
.
Usage
{
PromptTokens
:
10000
,
CompletionTokens
:
2000
,
PromptTokensDetails
:
dto
.
InputTokenDetails
{
CachedTokens
:
3000
,
TextTokens
:
7000
,
},
}
expr
:=
`tier("base", p * 2.5 + c * 15 + cr * 0.25)`
for
_
,
gr
:=
range
[]
float64
{
1.0
,
1.5
,
2.0
,
0.5
}
{
tq
:=
tieredQuota
(
expr
,
usage
,
false
,
gr
)
rq
:=
ratioQuota
(
usage
,
false
,
1.25
,
6
,
0.1
,
0
,
gr
)
if
math
.
Abs
(
tq
-
rq
)
>
0.01
{
t
.
Fatalf
(
"groupRatio=%v: tiered=%f ratio=%f (mismatch)"
,
gr
,
tq
,
rq
)
}
}
}
func
TestBuildTieredTokenParams_ParityWithRatio_Image
(
t
*
testing
.
T
)
{
// gpt-image-1-mini prices: input=$2, output=$8, image=$2.5
// Ratio equivalents: modelRatio=1, completionRatio=4, imageRatio=1.25
usage
:=
&
dto
.
Usage
{
PromptTokens
:
5000
,
CompletionTokens
:
4000
,
PromptTokensDetails
:
dto
.
InputTokenDetails
{
ImageTokens
:
1000
,
TextTokens
:
4000
,
},
}
expr
:=
`tier("base", p * 2 + c * 8 + img * 2.5)`
tq
:=
tieredQuota
(
expr
,
usage
,
false
,
1.0
)
rq
:=
ratioQuota
(
usage
,
false
,
1.0
,
4
,
0
,
1.25
,
1.0
)
if
math
.
Abs
(
tq
-
rq
)
>
0.01
{
t
.
Fatalf
(
"tiered=%f ratio=%f (mismatch)"
,
tq
,
rq
)
}
}
web/src/components/table/model-pricing/modal/components/DynamicPricingBreakdown.jsx
View file @
5b03b39d
...
...
@@ -20,6 +20,7 @@ For commercial licensing, please contact support@quantumnous.com
import
React
from
'react'
;
import
{
Card
,
Avatar
,
Tag
,
Table
,
Typography
}
from
'@douyinfe/semi-ui'
;
import
{
IconPriceTag
}
from
'@douyinfe/semi-icons'
;
import
{
parseTiersFromExpr
}
from
'../../../../../helpers'
;
import
{
splitBillingExprAndRequestRules
,
tryParseRequestRuleExpr
,
...
...
@@ -36,15 +37,6 @@ const { Text } = Typography;
const
PRICE_SUFFIX
=
'$/1M tokens'
;
function
unitCostToPrice
(
uc
)
{
return
Number
(
uc
)
||
0
;
}
function
formatPrice
(
uc
)
{
const
p
=
unitCostToPrice
(
uc
);
return
p
?
`$
${
p
.
toFixed
(
4
)}
`
:
'-'
;
}
const
VAR_LABELS
=
{
p
:
'输入'
,
c
:
'输出'
};
const
OP_LABELS
=
{
'<'
:
'<'
,
'<='
:
'≤'
,
'>'
:
'>'
,
'>='
:
'≥'
};
const
TIME_FUNC_LABELS
=
{
hour
:
'小时'
,
minute
:
'分钟'
,
weekday
:
'星期'
,
month
:
'月份'
,
day
:
'日期'
};
...
...
@@ -71,54 +63,6 @@ function formatConditionSummary(conditions, t) {
.
join
(
' && '
);
}
function
tryParseTiers
(
baseExpr
)
{
if
(
!
baseExpr
)
return
null
;
try
{
const
cacheVars
=
[
'cr'
,
'cc'
,
'cc1h'
];
const
optCache
=
cacheVars
.
map
((
v
)
=>
`(?:\\s*\\+\\s*
${
v
}
\\s*\\*\\s*([\\d.eE+-]+))?`
).
join
(
''
);
const
bodyPat
=
`p\\s*\\*\\s*([\\d.eE+-]+)\\s*\\+\\s*c\\s*\\*\\s*([\\d.eE+-]+)
${
optCache
}
`
;
const
singleRe
=
new
RegExp
(
`^tier\\("([^"]*)",\\s*
${
bodyPat
}
\\)$`
);
const
simple
=
baseExpr
.
match
(
singleRe
);
if
(
simple
)
{
return
[{
label
:
simple
[
1
],
conditions
:
[],
inputPrice
:
unitCostToPrice
(
Number
(
simple
[
2
])),
outputPrice
:
unitCostToPrice
(
Number
(
simple
[
3
])),
cacheReadPrice
:
simple
[
4
]
?
unitCostToPrice
(
Number
(
simple
[
4
]))
:
null
,
cacheCreatePrice
:
simple
[
5
]
?
unitCostToPrice
(
Number
(
simple
[
5
]))
:
null
,
cacheCreate1hPrice
:
simple
[
6
]
?
unitCostToPrice
(
Number
(
simple
[
6
]))
:
null
,
}];
}
const
condGroup
=
`((?:(?:p|c)\\s*(?:<|<=|>|>=)\\s*[\\d.eE+]+)(?:\\s*&&\\s*(?:p|c)\\s*(?:<|<=|>|>=)\\s*[\\d.eE+]+)*)`
;
const
tierRe
=
new
RegExp
(
`(?:
${
condGroup
}
\\s*\\?\\s*)?tier\\("([^"]*)",\\s*
${
bodyPat
}
\\)`
,
'g'
);
const
tiers
=
[];
let
match
;
while
((
match
=
tierRe
.
exec
(
baseExpr
))
!==
null
)
{
const
condStr
=
match
[
1
]
||
''
;
const
conditions
=
[];
if
(
condStr
)
{
for
(
const
cp
of
condStr
.
split
(
/
\s
*&&
\s
*/
))
{
const
cm
=
cp
.
trim
().
match
(
/^
(
p|c
)\s
*
(
<|<=|>|>=
)\s
*
([\d
.eE+
]
+
)
$/
);
if
(
cm
)
conditions
.
push
({
var
:
cm
[
1
],
op
:
cm
[
2
],
value
:
Number
(
cm
[
3
])
});
}
}
tiers
.
push
({
label
:
match
[
2
],
conditions
,
inputPrice
:
unitCostToPrice
(
Number
(
match
[
3
])),
outputPrice
:
unitCostToPrice
(
Number
(
match
[
4
])),
cacheReadPrice
:
match
[
5
]
?
unitCostToPrice
(
Number
(
match
[
5
]))
:
null
,
cacheCreatePrice
:
match
[
6
]
?
unitCostToPrice
(
Number
(
match
[
6
]))
:
null
,
cacheCreate1hPrice
:
match
[
7
]
?
unitCostToPrice
(
Number
(
match
[
7
]))
:
null
,
});
}
return
tiers
.
length
>
0
?
tiers
:
null
;
}
catch
{
return
null
;
}
}
function
describeCondition
(
cond
,
t
)
{
if
(
cond
.
source
===
SOURCE_TIME
)
{
...
...
@@ -147,7 +91,7 @@ export default function DynamicPricingBreakdown({ billingExpr, t }) {
const
{
billingExpr
:
baseExpr
,
requestRuleExpr
:
ruleExpr
}
=
splitBillingExprAndRequestRules
(
billingExpr
||
''
);
const
tiers
=
tryParseTiers
(
baseExpr
);
const
tiers
=
parseTiersFromExpr
(
baseExpr
);
const
ruleGroups
=
tryParseRequestRuleExpr
(
ruleExpr
||
''
);
const
hasTiers
=
tiers
&&
tiers
.
length
>
0
;
...
...
@@ -169,6 +113,17 @@ export default function DynamicPricingBreakdown({ billingExpr, t }) {
);
}
const
priceFields
=
[
[
'inputPrice'
,
'输入价格'
],
[
'outputPrice'
,
'补全价格'
],
[
'cacheReadPrice'
,
'缓存读取'
],
[
'cacheCreatePrice'
,
'缓存创建'
],
[
'cacheCreate1hPrice'
,
'缓存创建-1h'
],
[
'imagePrice'
,
'图片输入'
],
[
'audioInputPrice'
,
'音频输入'
],
[
'audioOutputPrice'
,
'音频输出'
],
];
const
tierColumns
=
[
{
title
:
t
(
'档位'
),
...
...
@@ -182,54 +137,21 @@ export default function DynamicPricingBreakdown({ billingExpr, t }) {
</
div
>
),
},
{
title
:
`
${
t
(
'输入价格'
)}
(
${
PRICE_SUFFIX
}
)`
,
dataIndex
:
'inputPrice'
,
render
:
(
v
)
=>
<
Text
strong
>
$
{
v
.
toFixed
(
4
)
}
</
Text
>,
},
{
title
:
`
${
t
(
'输出价格'
)}
(
${
PRICE_SUFFIX
}
)`
,
dataIndex
:
'outputPrice'
,
render
:
(
v
)
=>
<
Text
strong
>
$
{
v
.
toFixed
(
4
)
}
</
Text
>,
},
...
priceFields
.
filter
(([
field
])
=>
hasTiers
&&
tiers
.
some
((
tier
)
=>
tier
[
field
]
>
0
))
.
map
(([
field
,
label
])
=>
({
title
:
`
${
t
(
label
)}
(
${
PRICE_SUFFIX
}
)`
,
dataIndex
:
field
,
render
:
(
v
)
=>
v
>
0
?
<
Text
strong
>
$
{
v
.
toFixed
(
4
)
}
</
Text
>
:
'-'
,
})),
];
const
hasCacheRead
=
hasTiers
&&
tiers
.
some
((
tier
)
=>
tier
.
cacheReadPrice
!=
null
);
const
hasCacheCreate
=
hasTiers
&&
tiers
.
some
((
tier
)
=>
tier
.
cacheCreatePrice
!=
null
);
const
hasCache1h
=
hasTiers
&&
tiers
.
some
((
tier
)
=>
tier
.
cacheCreate1hPrice
!=
null
);
if
(
hasCacheRead
)
{
tierColumns
.
push
({
title
:
`
${
t
(
'缓存读取'
)}
(
${
PRICE_SUFFIX
}
)`
,
dataIndex
:
'cacheReadPrice'
,
render
:
(
v
)
=>
v
!=
null
?
<
Text
>
$
{
v
.
toFixed
(
4
)
}
</
Text
>
:
'-'
,
});
}
if
(
hasCacheCreate
)
{
tierColumns
.
push
({
title
:
`
${
t
(
'缓存创建'
)}
(
${
PRICE_SUFFIX
}
)`
,
dataIndex
:
'cacheCreatePrice'
,
render
:
(
v
)
=>
v
!=
null
?
<
Text
>
$
{
v
.
toFixed
(
4
)
}
</
Text
>
:
'-'
,
});
}
if
(
hasCache1h
)
{
tierColumns
.
push
({
title
:
`
${
t
(
'缓存创建-1h'
)}
(
${
PRICE_SUFFIX
}
)`
,
dataIndex
:
'cacheCreate1hPrice'
,
render
:
(
v
)
=>
v
!=
null
?
<
Text
>
$
{
v
.
toFixed
(
4
)
}
</
Text
>
:
'-'
,
});
}
const
tierData
=
hasTiers
?
tiers
.
map
((
tier
,
i
)
=>
({
key
:
`tier-
${
i
}
`
,
label
:
tier
.
label
,
condSummary
:
formatConditionSummary
(
tier
.
conditions
,
t
),
inputPrice
:
tier
.
inputPrice
,
outputPrice
:
tier
.
outputPrice
,
cacheReadPrice
:
tier
.
cacheReadPrice
,
cacheCreatePrice
:
tier
.
cacheCreatePrice
,
cacheCreate1hPrice
:
tier
.
cacheCreate1hPrice
,
...
Object
.
fromEntries
(
priceFields
.
map
(([
field
])
=>
[
field
,
tier
[
field
]
||
0
])),
}))
:
[];
...
...
web/src/helpers/render.jsx
View file @
5b03b39d
...
...
@@ -2210,24 +2210,47 @@ export function renderLogContent(opts) {
}
}
function
parseTiersFromExpr
(
exprStr
)
{
const
TIER_VAR_KEYS
=
[
'p'
,
'c'
,
'cr'
,
'cc'
,
'cc1h'
,
'img'
,
'ai'
,
'ao'
];
const
TIER_VAR_TO_FIELD
=
{
p
:
'inputPrice'
,
c
:
'outputPrice'
,
cr
:
'cacheReadPrice'
,
cc
:
'cacheCreatePrice'
,
cc1h
:
'cacheCreate1hPrice'
,
img
:
'imagePrice'
,
ai
:
'audioInputPrice'
,
ao
:
'audioOutputPrice'
,
};
function
parseTierBody
(
bodyStr
)
{
const
coeffs
=
{};
const
re
=
new
RegExp
(
`\\b(
${
TIER_VAR_KEYS
.
join
(
'|'
)}
)\\s*\\*\\s*([\\d.eE+-]+)`
,
'g'
);
let
m
;
while
((
m
=
re
.
exec
(
bodyStr
))
!==
null
)
{
if
(
!
(
m
[
1
]
in
coeffs
))
coeffs
[
m
[
1
]]
=
Number
(
m
[
2
]);
}
const
tier
=
{};
for
(
const
[
varName
,
field
]
of
Object
.
entries
(
TIER_VAR_TO_FIELD
))
{
tier
[
field
]
=
coeffs
[
varName
]
||
0
;
}
return
tier
;
}
export
function
parseTiersFromExpr
(
exprStr
)
{
if
(
!
exprStr
)
return
[];
try
{
const
cacheVars
=
[
'cr'
,
'cc'
,
'cc1h'
];
const
optCache
=
cacheVars
.
map
((
v
)
=>
`(?:\\s*\\+\\s*
${
v
}
\\s*\\*\\s*([\\d.eE+-]+))?`
).
join
(
''
);
const
bodyPat
=
`p\\s*\\*\\s*([\\d.eE+-]+)\\s*\\+\\s*c\\s*\\*\\s*([\\d.eE+-]+)
${
optCache
}
`
;
const
tierRe
=
new
RegExp
(
`tier\\("([^"]*)",\\s*
${
bodyPat
}
\\)`
,
'g'
);
const
condGroup
=
`((?:(?:p|c)\\s*(?:<|<=|>|>=)\\s*[\\d.eE+]+)(?:\\s*&&\\s*(?:p|c)\\s*(?:<|<=|>|>=)\\s*[\\d.eE+]+)*)`
;
const
tierRe
=
new
RegExp
(
`(?:
${
condGroup
}
\\s*\\?\\s*)?tier\\("([^"]*)",\\s*([^)]+)\\)`
,
'g'
);
const
tiers
=
[];
let
m
;
while
((
m
=
tierRe
.
exec
(
exprStr
))
!==
null
)
{
tiers
.
push
({
label
:
m
[
1
],
inputPrice
:
Number
(
m
[
2
]),
outputPrice
:
Number
(
m
[
3
]),
cacheReadPrice
:
m
[
4
]
?
Number
(
m
[
4
])
:
0
,
cacheCreatePrice
:
m
[
5
]
?
Number
(
m
[
5
])
:
0
,
cacheCreate1hPrice
:
m
[
6
]
?
Number
(
m
[
6
])
:
0
,
});
const
condStr
=
m
[
1
]
||
''
;
const
conditions
=
[];
if
(
condStr
)
{
for
(
const
cp
of
condStr
.
split
(
/
\s
*&&
\s
*/
))
{
const
cm
=
cp
.
trim
().
match
(
/^
(
p|c
)\s
*
(
<|<=|>|>=
)\s
*
([\d
.eE+
]
+
)
$/
);
if
(
cm
)
conditions
.
push
({
var
:
cm
[
1
],
op
:
cm
[
2
],
value
:
Number
(
cm
[
3
])
});
}
}
const
tier
=
parseTierBody
(
m
[
3
]);
tier
.
label
=
m
[
2
];
tier
.
conditions
=
conditions
;
tiers
.
push
(
tier
);
}
return
tiers
;
}
catch
{
...
...
@@ -2258,45 +2281,24 @@ export function renderTieredModelPrice(opts) {
const
{
symbol
,
rate
}
=
getCurrencyConfig
();
const
gr
=
groupRatio
||
1
;
const
inputCost
=
(
inputTokens
/
1000000
)
*
tier
.
inputPrice
;
const
outputCost
=
(
completionTokens
/
1000000
)
*
tier
.
outputPrice
;
const
cacheReadCost
=
(
cacheTokens
/
1000000
)
*
tier
.
cacheReadPrice
;
const
hasSplitCacheCreation
=
cacheCreationTokens5m
>
0
||
cacheCreationTokens1h
>
0
;
let
cacheCreateCost
=
0
;
if
(
hasSplitCacheCreation
)
{
cacheCreateCost
=
(
cacheCreationTokens5m
/
1000000
)
*
tier
.
cacheCreatePrice
+
(
cacheCreationTokens1h
/
1000000
)
*
tier
.
cacheCreate1hPrice
;
}
else
if
(
cacheCreationTokens
>
0
)
{
cacheCreateCost
=
(
cacheCreationTokens
/
1000000
)
*
tier
.
cacheCreatePrice
;
}
const
totalBeforeGroup
=
inputCost
+
outputCost
+
cacheReadCost
+
cacheCreateCost
;
const
total
=
totalBeforeGroup
*
gr
;
const
priceLines
=
[
[
'inputPrice'
,
'输入价格'
],
[
'outputPrice'
,
'补全价格'
],
[
'cacheReadPrice'
,
'缓存读取价格'
],
[
'cacheCreatePrice'
,
'缓存创建价格'
],
[
'cacheCreate1hPrice'
,
'1h缓存创建价格'
],
[
'imagePrice'
,
'图片输入价格'
],
[
'audioInputPrice'
,
'音频输入价格'
],
[
'audioOutputPrice'
,
'音频输出价格'
],
];
const
lines
=
[
buildBillingText
(
'命中档位:{{tier}}'
,
{
tier
:
matchedTier
||
tier
.
label
}),
buildBillingPriceText
(
'输入价格:{{symbol}}{{price}} / 1M tokens'
,
{
symbol
,
usdAmount
:
tier
.
inputPrice
,
rate
}),
buildBillingPriceText
(
'输出价格:{{symbol}}{{price}} / 1M tokens'
,
{
symbol
,
usdAmount
:
tier
.
outputPrice
,
rate
}),
cacheTokens
>
0
&&
tier
.
cacheReadPrice
>
0
?
buildBillingPriceText
(
'缓存读取价格:{{symbol}}{{price}} / 1M tokens'
,
{
symbol
,
usdAmount
:
tier
.
cacheReadPrice
,
rate
})
:
null
,
hasSplitCacheCreation
&&
cacheCreationTokens5m
>
0
&&
tier
.
cacheCreatePrice
>
0
?
buildBillingPriceText
(
'5m缓存创建价格:{{symbol}}{{price}} / 1M tokens'
,
{
symbol
,
usdAmount
:
tier
.
cacheCreatePrice
,
rate
})
:
null
,
hasSplitCacheCreation
&&
cacheCreationTokens1h
>
0
&&
tier
.
cacheCreate1hPrice
>
0
?
buildBillingPriceText
(
'1h缓存创建价格:{{symbol}}{{price}} / 1M tokens'
,
{
symbol
,
usdAmount
:
tier
.
cacheCreate1hPrice
,
rate
})
:
null
,
!
hasSplitCacheCreation
&&
cacheCreationTokens
>
0
&&
tier
.
cacheCreatePrice
>
0
?
buildBillingPriceText
(
'缓存创建价格:{{symbol}}{{price}} / 1M tokens'
,
{
symbol
,
usdAmount
:
tier
.
cacheCreatePrice
,
rate
})
:
null
,
buildBillingText
(
'(输入 {{input}} tokens / 1M tokens * {{symbol}}{{inputPrice}} + 输出 {{output}} tokens / 1M tokens * {{symbol}}{{outputPrice}}) * 分组倍率 {{ratio}} = {{symbol}}{{total}}'
,
{
input
:
inputTokens
,
output
:
completionTokens
,
symbol
,
inputPrice
:
formatBillingDisplayPrice
(
tier
.
inputPrice
,
rate
),
outputPrice
:
formatBillingDisplayPrice
(
tier
.
outputPrice
,
rate
),
ratio
:
gr
,
total
:
formatBillingDisplayPrice
(
total
,
rate
),
},
),
...
priceLines
.
filter
(([
field
])
=>
tier
[
field
]
>
0
)
.
map
(([
field
,
label
])
=>
buildBillingPriceText
(
`
${
label
}
:{{symbol}}{{price}} / 1M tokens`
,
{
symbol
,
usdAmount
:
tier
[
field
],
rate
}),
),
];
return
renderBillingArticle
(
lines
);
...
...
@@ -2329,44 +2331,26 @@ export function renderTieredModelPriceSimple(opts) {
];
if
(
tier
&&
isPriceDisplayMode
(
displayMode
))
{
segments
.
push
({
tone
:
'secondary'
,
text
:
i18next
.
t
(
'输入 {{price}} / 1M tokens'
,
{
price
:
formatCompactDisplayPrice
(
tier
.
inputPrice
),
}),
});
if
(
cacheTokens
>
0
&&
tier
.
cacheReadPrice
>
0
)
{
segments
.
push
({
tone
:
'secondary'
,
text
:
i18next
.
t
(
'缓存读 {{price}} / 1M tokens'
,
{
price
:
formatCompactDisplayPrice
(
tier
.
cacheReadPrice
),
}),
});
}
const
hasSplitCacheCreation
=
cacheCreationTokens5m
>
0
||
cacheCreationTokens1h
>
0
;
if
(
hasSplitCacheCreation
&&
cacheCreationTokens5m
>
0
&&
tier
.
cacheCreatePrice
>
0
)
{
segments
.
push
({
tone
:
'secondary'
,
text
:
i18next
.
t
(
'5m缓存创建 {{price}} / 1M tokens'
,
{
price
:
formatCompactDisplayPrice
(
tier
.
cacheCreatePrice
),
}),
});
}
if
(
hasSplitCacheCreation
&&
cacheCreationTokens1h
>
0
&&
tier
.
cacheCreate1hPrice
>
0
)
{
segments
.
push
({
tone
:
'secondary'
,
text
:
i18next
.
t
(
'1h缓存创建 {{price}} / 1M tokens'
,
{
price
:
formatCompactDisplayPrice
(
tier
.
cacheCreate1hPrice
),
}),
});
}
if
(
!
hasSplitCacheCreation
&&
cacheCreationTokens
>
0
&&
tier
.
cacheCreatePrice
>
0
)
{
segments
.
push
({
tone
:
'secondary'
,
text
:
i18next
.
t
(
'缓存创建 {{price}} / 1M tokens'
,
{
price
:
formatCompactDisplayPrice
(
tier
.
cacheCreatePrice
),
}),
});
const
priceSegments
=
[
[
'inputPrice'
,
'输入'
],
[
'outputPrice'
,
'补全'
],
[
'cacheReadPrice'
,
'缓存读'
],
[
'cacheCreatePrice'
,
'缓存创建'
],
[
'cacheCreate1hPrice'
,
'1h缓存创建'
],
[
'imagePrice'
,
'图片输入'
],
[
'audioInputPrice'
,
'音频输入'
],
[
'audioOutputPrice'
,
'音频输出'
],
];
for
(
const
[
field
,
label
]
of
priceSegments
)
{
if
(
tier
[
field
]
>
0
)
{
segments
.
push
({
tone
:
'secondary'
,
text
:
i18next
.
t
(
'{{label}} {{price}} / 1M tokens'
,
{
label
:
i18next
.
t
(
label
),
price
:
formatCompactDisplayPrice
(
tier
[
field
]),
}),
});
}
}
}
...
...
web/src/helpers/utils.jsx
View file @
5b03b39d
...
...
@@ -904,9 +904,24 @@ export const formatDynamicPriceSummary = (billingExpr, t, groupRatio = 1) => {
const
tierMatches
=
billingExpr
.
match
(
/tier
\(
/g
)
||
[];
const
tierCount
=
tierMatches
.
length
;
const
firstTierMatch
=
billingExpr
.
match
(
/tier
\(
"
[^
"
]
*",
\s
*p
\s
*
\*\s
*
([\d
.eE+-
]
+
)\s
*
\+\s
*c
\s
*
\*\s
*
([\d
.eE+-
]
+
)(?:\s
*
\+\s
*cr
\s
*
\*\s
*
([\d
.eE+-
]
+
))?(?:\s
*
\+\s
*cc
\s
*
\*\s
*
([\d
.eE+-
]
+
))?
/
,
);
const
varCoeffs
=
{};
const
varRe
=
/
\b(
p|c|cr|cc|cc1h|img|ai|ao
)\s
*
\*\s
*
([\d
.eE+-
]
+
)
/g
;
let
vm
;
while
((
vm
=
varRe
.
exec
(
billingExpr
))
!==
null
)
{
if
(
!
(
vm
[
1
]
in
varCoeffs
))
varCoeffs
[
vm
[
1
]]
=
Number
(
vm
[
2
]);
}
const
hasCoeffs
=
'p'
in
varCoeffs
||
'c'
in
varCoeffs
;
const
varLabels
=
[
[
'p'
,
'输入价格'
],
[
'c'
,
'补全价格'
],
[
'cr'
,
'缓存读取价格'
],
[
'cc'
,
'缓存创建价格'
],
[
'cc1h'
,
'1h缓存创建价格'
],
[
'img'
,
'图片输入价格'
],
[
'ai'
,
'音频输入价格'
],
[
'ao'
,
'音频输出价格'
],
];
const
hasTimeCondition
=
/
\b(?:
hour|weekday|month|day
)\(
/
.
test
(
billingExpr
);
const
hasRequestCondition
=
/
\b(?:
param|header
)\(
/
.
test
(
billingExpr
);
...
...
@@ -921,26 +936,18 @@ export const formatDynamicPriceSummary = (billingExpr, t, groupRatio = 1) => {
return
(
<>
{
firstTierMatch
&&
(
{
hasCoeffs
&&
(
<>
<
span
style=
{
lineStyle
}
>
{
t
(
'输入价格'
)
}
$
{
(
Number
(
firstTierMatch
[
1
])
*
gr
).
toFixed
(
4
)
}{
unitSuffix
}
</
span
>
<
span
style=
{
lineStyle
}
>
{
t
(
'输出价格'
)
}
$
{
(
Number
(
firstTierMatch
[
2
])
*
gr
).
toFixed
(
4
)
}{
unitSuffix
}
</
span
>
{
firstTierMatch
[
3
]
&&
(
<
span
style=
{
lineStyle
}
>
{
t
(
'缓存读取价格'
)
}
$
{
(
Number
(
firstTierMatch
[
3
])
*
gr
).
toFixed
(
4
)
}{
unitSuffix
}
</
span
>
)
}
{
firstTierMatch
[
4
]
&&
(
<
span
style=
{
lineStyle
}
>
{
t
(
'缓存创建价格'
)
}
$
{
(
Number
(
firstTierMatch
[
4
])
*
gr
).
toFixed
(
4
)
}{
unitSuffix
}
</
span
>
{
varLabels
.
map
(([
key
,
label
])
=>
key
in
varCoeffs
?
(
<
span
key=
{
key
}
style=
{
lineStyle
}
>
{
t
(
label
)
}
$
{
(
varCoeffs
[
key
]
*
gr
).
toFixed
(
4
)
}{
unitSuffix
}
</
span
>
)
:
null
,
)
}
</>
)
}
{
(
tierCount
>
1
||
hasTimeCondition
||
hasRequestCondition
)
&&
(
<
span
style=
{
{
display
:
'flex'
,
gap
:
4
,
flexWrap
:
'wrap'
}
}
>
<
span
style=
{
{
...
...
@@ -970,6 +977,7 @@ export const formatDynamicPriceSummary = (billingExpr, t, groupRatio = 1) => {
</
span
>
))
}
</
span
>
)
}
</>
);
};
...
...
web/src/pages/Setting/Ratio/components/TieredPricingEditor.jsx
View file @
5b03b39d
...
...
@@ -764,7 +764,7 @@ const PRESET_GROUPS = [
presets
:
[
{
key
:
'flat'
,
label
:
'Flat'
,
expr
:
'tier("base", p * 2 + c * 4)'
},
{
key
:
'claude-opus'
,
label
:
'Claude Opus 4.6'
,
expr
:
'tier("base", p * 5 + c * 25 + cr * 0.5 + cc * 6.25 + cc1h * 10)'
},
{
key
:
'gpt-5.4'
,
label
:
'GPT-5.4'
,
expr
:
'tier("base", p * 2.5 + c * 1
0
+ cr * 0.25)'
},
{
key
:
'gpt-5.4'
,
label
:
'GPT-5.4'
,
expr
:
'tier("base", p * 2.5 + c * 1
5
+ cr * 0.25)'
},
],
},
{
...
...
@@ -778,6 +778,7 @@ const PRESET_GROUPS = [
{
group
:
'多模态'
,
presets
:
[
{
key
:
'gpt-image-1-mini'
,
label
:
'GPT-Image-1-Mini'
,
expr
:
'tier("base", p * 2 + c * 8 + img * 2.5)'
},
{
key
:
'qwen3-omni-flash'
,
label
:
'Qwen3-Omni-Flash'
,
expr
:
'tier("base", p * 0.43 + c * 3.06 + img * 0.78 + ai * 3.81 + ao * 15.11)'
},
],
},
...
...
@@ -791,7 +792,7 @@ const PRESET_GROUPS = [
},
{
key
:
'gpt-5.4-fast'
,
label
:
'GPT-5.4 Fast'
,
expr
:
'tier("base", p * 2.5 + c * 1
0
+ cr * 0.25)'
,
expr
:
'tier("base", p * 2.5 + c * 1
5
+ cr * 0.25)'
,
requestRules
:
[{
conditions
:
[{
source
:
SOURCE_PARAM
,
path
:
'service_tier'
,
mode
:
MATCH_EQ
,
value
:
'fast'
}],
multiplier
:
'2'
}],
},
],
...
...
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