feat: enhance tiered billing functionality and UI components

- Introduced new fields for billing mode and expression in the Pricing model.
- Implemented dynamic pricing breakdown component to display tiered billing details.
- Updated various components to support and render tiered billing information.
- Enhanced pricing calculation logic to accommodate dynamic pricing scenarios.
- Added tests for new billing expression functionalities and UI components.
This commit is contained in:
CaIon
2026-03-16 18:57:14 +08:00
parent 91ed4e196a
commit f0589cc478
23 changed files with 1237 additions and 695 deletions
+52
View File
@@ -931,3 +931,55 @@ func TestTimeFunctions_MonthDayPattern(t *testing.T) {
t.Errorf("cost = %f, want 1000 or 500", cost)
}
}
// ---------------------------------------------------------------------------
// Image and audio token tests
// ---------------------------------------------------------------------------
func TestImageTokenVariable(t *testing.T) {
exprStr := `tier("base", p * 2 + c * 10 + img * 5)`
cost, _, err := billingexpr.RunExpr(exprStr, billingexpr.TokenParams{P: 1000, C: 500, Img: 200})
if err != nil {
t.Fatal(err)
}
// 1000*2 + 500*10 + 200*5 = 2000 + 5000 + 1000 = 8000
if math.Abs(cost-8000) > 1e-6 {
t.Errorf("cost = %f, want 8000", cost)
}
}
func TestAudioTokenVariables(t *testing.T) {
exprStr := `tier("base", p * 2 + c * 10 + ai * 50 + ao * 100)`
cost, _, err := billingexpr.RunExpr(exprStr, billingexpr.TokenParams{P: 1000, C: 500, AI: 100, AO: 50})
if err != nil {
t.Fatal(err)
}
// 1000*2 + 500*10 + 100*50 + 50*100 = 2000 + 5000 + 5000 + 5000 = 17000
if math.Abs(cost-17000) > 1e-6 {
t.Errorf("cost = %f, want 17000", cost)
}
}
func TestImageAudioAliases(t *testing.T) {
exprStr := `tier("base", prompt_tokens * 1 + image_tokens * 3 + audio_input_tokens * 5 + audio_output_tokens * 10)`
cost, _, err := billingexpr.RunExpr(exprStr, billingexpr.TokenParams{P: 100, Img: 50, AI: 20, AO: 10})
if err != nil {
t.Fatal(err)
}
// 100*1 + 50*3 + 20*5 + 10*10 = 100 + 150 + 100 + 100 = 450
if math.Abs(cost-450) > 1e-6 {
t.Errorf("cost = %f, want 450", cost)
}
}
func TestImageAudioZero(t *testing.T) {
exprStr := `tier("base", p * 2 + img * 5 + ai * 50 + ao * 100)`
cost, _, err := billingexpr.RunExpr(exprStr, billingexpr.TokenParams{P: 1000})
if err != nil {
t.Fatal(err)
}
// img, ai, ao default to 0
if math.Abs(cost-2000) > 1e-6 {
t.Errorf("cost = %f, want 2000", cost)
}
}
+6
View File
@@ -31,6 +31,12 @@ var compileEnvPrototype = map[string]interface{}{
"cache_read_tokens": float64(0),
"cache_create_tokens": float64(0),
"cache_create_1h_tokens": float64(0),
"img": float64(0),
"ai": float64(0),
"ao": float64(0),
"image_tokens": float64(0),
"audio_input_tokens": float64(0),
"audio_output_tokens": float64(0),
"tier": func(string, float64) float64 { return 0 },
"header": func(string) string { return "" },
"param": func(string) interface{} { return nil },
+6
View File
@@ -62,6 +62,12 @@ func runProgram(prog *vm.Program, params TokenParams, request RequestInput) (flo
"cache_read_tokens": params.CR,
"cache_create_tokens": params.CC,
"cache_create_1h_tokens": params.CC1h,
"img": params.Img,
"ai": params.AI,
"ao": params.AO,
"image_tokens": params.Img,
"audio_input_tokens": params.AI,
"audio_output_tokens": params.AO,
"tier": func(name string, value float64) float64 {
trace.MatchedTier = name
trace.Cost = value
+5 -2
View File
@@ -14,11 +14,14 @@ type RequestInput struct {
// Fields beyond P and C are optional — when absent they default to 0,
// which means cache-unaware expressions keep working unchanged.
type TokenParams struct {
P float64 // prompt tokens
C float64 // completion tokens
P float64 // prompt tokens (text)
C float64 // completion tokens (text)
CR float64 // cache read (hit) tokens
CC float64 // cache creation tokens (5-min TTL for Claude, generic for others)
CC1h float64 // cache creation tokens — 1-hour TTL (Claude only)
Img float64 // image input tokens
AI float64 // audio input tokens
AO float64 // audio output tokens
}
// TraceResult holds side-channel info captured by the tier() function