Files
new-api/pkg/billingexpr/billingexpr_test.go
T
CaIon 064ed943e1 feat(billing): support fixed per-request expression pricing
Add tier(name, fixed(usd)) pricing leaves to conditional token expressions,
with static validation, token/request billing metadata, and safe settlement
when successful requests have missing or zero token usage. Preserve group
and request multipliers, separate tool charges, and explicit zero prices.
Reject fixed expressions on unsupported Realtime and task-usage entrances.

Expose per-token and per-call tier editing, preserve both drafts, and show
request units consistently in pricing summaries and consume logs. Retain
matched-branch metadata and update all seven frontend locales.

Validation passed:
- go test ./pkg/billingexpr ./relay/helper ./service ./setting/billing_setting ./relay -count=1
- Frontend pricing, model-pricing, editor and usage-log regression suites
- Typecheck, scoped lint, protected-header formatting, i18n validation and build
- go test ./service -run TestFixedPriceBillingDatabaseMatrix -count=1 -v
  with TEST_FIXED_MYSQL_DSN and TEST_FIXED_POSTGRES_DSN set to isolated local
  test databases: SQLite 3.50.4, MySQL 8.0.46, PostgreSQL 16.15.
  All 39 database cases passed, including reserve/settle/refund, zero price,
  missing usage, branch changes and insufficient-quota handling.

No production database schema or migration changes.
2026-09-09 15:05:37 +08:00

1191 lines
39 KiB
Go

package billingexpr_test
import (
"math"
"os"
"testing"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/pkg/billingexpr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFixedPriceBranches(t *testing.T) {
const expression = `(len <= 32000 ? tier("short", fixed(0.01)) : tier("long", p * 2 + c * 8)) * (param("fast") == true ? 2 : 1)`
for _, tc := range []struct {
name string
params billingexpr.TokenParams
cost float64
tier string
}{
{"zero usage still charges once", billingexpr.TokenParams{}, 20000, "short"},
{"fixed branch ignores token prices", billingexpr.TokenParams{P: 32000, C: 9000, Len: 32000}, 20000, "short"},
{"token branch uses actual usage", billingexpr.TokenParams{P: 32001, C: 100, Len: 32001}, 129604, "long"},
} {
t.Run(tc.name, func(t *testing.T) {
cost, trace, err := billingexpr.RunExprWithRequest(expression, tc.params, billingexpr.RequestInput{Body: []byte(`{"fast":true}`)})
require.NoError(t, err)
assert.Equal(t, tc.cost, cost)
assert.Equal(t, tc.tier, trace.MatchedTier)
require.Len(t, trace.RequestRules, 1)
assert.True(t, trace.RequestRules[0].Matched)
})
}
cost, _, err := billingexpr.RunExpr(`tier("free", fixed(0))`, billingexpr.TokenParams{P: 1000})
require.NoError(t, err)
assert.Zero(t, cost)
}
func TestFixedPriceRejectsInvalidLeavesIncludingUnselectedBranches(t *testing.T) {
for _, expression := range []string{
`true ? tier("ok", p) : tier("bad", fixed(-0.01))`,
`tier("bad", fixed(p))`,
`tier("bad", fixed(0.01 + 0.02))`,
`tier("bad", fixed(1e308))`,
`tier("bad", fixed(0.01) + p * 2)`,
`tier("bad", p * fixed(0.01))`,
`tier("a", fixed(0.01)) + tier("b", p * 2)`,
`fixed(0.01)`,
`tier("bad", fixed(0.01)) * p`,
`tier("bad", fixed(0.01)) * (param("fast") == true ? -2 : 1)`,
} {
t.Run(expression, func(t *testing.T) {
_, err := billingexpr.CompileFromCache(expression)
require.Error(t, err)
})
}
}
func TestFixedPriceQuotaBoundariesAndUnsupportedTaskSnapshots(t *testing.T) {
for _, tc := range []struct {
name, expression string
group float64
want int
clamped bool
}{
{"normal group", `tier("request", fixed(0.01))`, 1.5, 7500, false},
{"free group", `tier("request", fixed(0.01))`, 0, 0, false},
{"explicit free price", `tier("free", fixed(0))`, 1, 0, false},
{"oversized price saturates instead of crediting", `tier("huge", fixed(1e20))`, 1, math.MaxInt32, true},
} {
t.Run(tc.name, func(t *testing.T) {
snap := &billingexpr.BillingSnapshot{ExprString: tc.expression, ExprHash: billingexpr.ExprHashString(tc.expression), GroupRatio: tc.group, QuotaPerUnit: 500000}
result, err := billingexpr.ComputeTieredQuota(snap, billingexpr.TokenParams{})
require.NoError(t, err)
assert.Equal(t, tc.want, result.ActualQuotaAfterGroup)
assert.Equal(t, billingexpr.BillingUnitRequest, result.BillingUnit)
assert.Equal(t, tc.clamped, result.Clamp != nil)
_, err = billingexpr.QuotaRoundStrict(result.ActualQuotaBeforeGroup * tc.group)
if tc.clamped {
require.Error(t, err)
} else {
require.NoError(t, err)
}
snap.TaskUsageBilling = true
_, err = billingexpr.ComputeTieredQuota(snap, billingexpr.TokenParams{})
require.ErrorContains(t, err, "task usage")
})
}
const optimized = `true ? tier("tokens", p) : tier("request", fixed(0.01))`
assert.True(t, billingexpr.UsesFixedPricing(optimized))
_, trace, err := billingexpr.RunExpr(optimized, billingexpr.TokenParams{P: 10})
require.NoError(t, err)
assert.Equal(t, billingexpr.BillingUnitToken, trace.BillingUnit)
assert.Nil(t, trace.FixedPrice)
}
// ---------------------------------------------------------------------------
// Claude-style: fixed tiers, input > 200K changes both input & output price
// ---------------------------------------------------------------------------
const claudeExpr = `p <= 200000 ? tier("standard", p * 1.5 + c * 7.5) : tier("long_context", p * 3.0 + c * 11.25)`
func TestClaude_StandardTier(t *testing.T) {
cost, trace, err := billingexpr.RunExpr(claudeExpr, billingexpr.TokenParams{P: 100000, C: 5000})
if err != nil {
t.Fatal(err)
}
want := 100000*1.5 + 5000*7.5
if math.Abs(cost-want) > 1e-6 {
t.Errorf("cost = %f, want %f", cost, want)
}
if trace.MatchedTier != "standard" {
t.Errorf("tier = %q, want %q", trace.MatchedTier, "standard")
}
}
func TestClaude_LongContextTier(t *testing.T) {
cost, trace, err := billingexpr.RunExpr(claudeExpr, billingexpr.TokenParams{P: 300000, C: 10000})
if err != nil {
t.Fatal(err)
}
want := 300000*3.0 + 10000*11.25
if math.Abs(cost-want) > 1e-6 {
t.Errorf("cost = %f, want %f", cost, want)
}
if trace.MatchedTier != "long_context" {
t.Errorf("tier = %q, want %q", trace.MatchedTier, "long_context")
}
}
func TestClaude_BoundaryExact(t *testing.T) {
cost, trace, err := billingexpr.RunExpr(claudeExpr, billingexpr.TokenParams{P: 200000, C: 1000})
if err != nil {
t.Fatal(err)
}
want := 200000*1.5 + 1000*7.5
if math.Abs(cost-want) > 1e-6 {
t.Errorf("cost = %f, want %f", cost, want)
}
if trace.MatchedTier != "standard" {
t.Errorf("tier = %q, want %q", trace.MatchedTier, "standard")
}
}
// ---------------------------------------------------------------------------
// GLM-style: multi-condition tiers with both input and output dimensions
// ---------------------------------------------------------------------------
const glmExpr = `
(
p < 32000 && c < 200 ? tier("tier1_short", (p)*2 + c*8) :
p < 32000 && c >= 200 ? tier("tier2_long_output", (p)*3 + c*14) :
tier("tier3_long_input", (p)*4 + c*16)
) / 1000000
`
func TestGLM_Tier1(t *testing.T) {
cost, trace, err := billingexpr.RunExpr(glmExpr, billingexpr.TokenParams{P: 15000, C: 100})
if err != nil {
t.Fatal(err)
}
want := (15000.0*2 + 100.0*8) / 1000000
if math.Abs(cost-want) > 1e-10 {
t.Errorf("cost = %f, want %f", cost, want)
}
if trace.MatchedTier != "tier1_short" {
t.Errorf("tier = %q, want %q", trace.MatchedTier, "tier1_short")
}
}
func TestGLM_Tier2(t *testing.T) {
cost, trace, err := billingexpr.RunExpr(glmExpr, billingexpr.TokenParams{P: 15000, C: 500})
if err != nil {
t.Fatal(err)
}
want := (15000.0*3 + 500.0*14) / 1000000
if math.Abs(cost-want) > 1e-10 {
t.Errorf("cost = %f, want %f", cost, want)
}
if trace.MatchedTier != "tier2_long_output" {
t.Errorf("tier = %q, want %q", trace.MatchedTier, "tier2_long_output")
}
}
func TestGLM_Tier3(t *testing.T) {
cost, trace, err := billingexpr.RunExpr(glmExpr, billingexpr.TokenParams{P: 50000, C: 100})
if err != nil {
t.Fatal(err)
}
want := (50000.0*4 + 100.0*16) / 1000000
if math.Abs(cost-want) > 1e-10 {
t.Errorf("cost = %f, want %f", cost, want)
}
if trace.MatchedTier != "tier3_long_input" {
t.Errorf("tier = %q, want %q", trace.MatchedTier, "tier3_long_input")
}
}
// ---------------------------------------------------------------------------
// Simple flat-rate (no tier() call)
// ---------------------------------------------------------------------------
func TestSimpleExpr_NoTier(t *testing.T) {
cost, trace, err := billingexpr.RunExpr("p * 0.5 + c * 1.0", billingexpr.TokenParams{P: 1000, C: 500})
if err != nil {
t.Fatal(err)
}
want := 1000*0.5 + 500*1.0
if math.Abs(cost-want) > 1e-6 {
t.Errorf("cost = %f, want %f", cost, want)
}
if trace.MatchedTier != "" {
t.Errorf("tier should be empty, got %q", trace.MatchedTier)
}
}
// ---------------------------------------------------------------------------
// Math helper functions
// ---------------------------------------------------------------------------
func TestMathHelpers(t *testing.T) {
cost, _, err := billingexpr.RunExpr("max(p, c) * 0.5 + min(p, c) * 0.1", billingexpr.TokenParams{P: 300, C: 500})
if err != nil {
t.Fatal(err)
}
want := 500*0.5 + 300*0.1
if math.Abs(cost-want) > 1e-6 {
t.Errorf("cost = %f, want %f", cost, want)
}
}
func TestRequestProbeHelpers(t *testing.T) {
cost, _, err := billingexpr.RunExprWithRequest(
`p * 0.5 + c * 1.0 * (param("service_tier") == "fast" ? 2 : 1)`,
billingexpr.TokenParams{P: 1000, C: 500},
billingexpr.RequestInput{
Body: []byte(`{"service_tier":"fast"}`),
},
)
if err != nil {
t.Fatal(err)
}
want := 1000*0.5 + 500*1.0*2
if math.Abs(cost-want) > 1e-6 {
t.Errorf("cost = %f, want %f", cost, want)
}
}
func TestHeaderProbeHelper(t *testing.T) {
cost, _, err := billingexpr.RunExprWithRequest(
`p * 0.5 + c * 1.0 * (has(header("anthropic-beta"), "fast-mode") ? 2 : 1)`,
billingexpr.TokenParams{P: 1000, C: 500},
billingexpr.RequestInput{
Headers: map[string]string{
"Anthropic-Beta": "fast-mode-2026-02-01",
},
},
)
if err != nil {
t.Fatal(err)
}
want := 1000*0.5 + 500*1.0*2
if math.Abs(cost-want) > 1e-6 {
t.Errorf("cost = %f, want %f", cost, want)
}
}
func TestParamProbeNestedBool(t *testing.T) {
cost, _, err := billingexpr.RunExprWithRequest(
`p * (param("stream_options.fast_mode") == true ? 1.5 : 1.0)`,
billingexpr.TokenParams{P: 100},
billingexpr.RequestInput{
Body: []byte(`{"stream_options":{"fast_mode":true}}`),
},
)
if err != nil {
t.Fatal(err)
}
want := 150.0
if math.Abs(cost-want) > 1e-6 {
t.Errorf("cost = %f, want %f", cost, want)
}
}
func TestParamProbeArrayLength(t *testing.T) {
cost, _, err := billingexpr.RunExprWithRequest(
`p * (param("messages.#") > 20 ? 1.2 : 1.0)`,
billingexpr.TokenParams{P: 100},
billingexpr.RequestInput{
Body: []byte(`{"messages":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]}`),
},
)
if err != nil {
t.Fatal(err)
}
want := 120.0
if math.Abs(cost-want) > 1e-6 {
t.Errorf("cost = %f, want %f", cost, want)
}
}
func TestRequestProbeMissingFieldReturnsNil(t *testing.T) {
cost, _, err := billingexpr.RunExprWithRequest(
`param("missing.value") == nil ? 2 : 1`,
billingexpr.TokenParams{},
billingexpr.RequestInput{
Body: []byte(`{"service_tier":"standard"}`),
},
)
if err != nil {
t.Fatal(err)
}
if cost != 2 {
t.Errorf("cost = %f, want 2", cost)
}
}
func TestRequestProbeMultipleRulesTraceAllFactors(t *testing.T) {
exprStr := `(tier("base", p * 2)) * (param("service_tier") == "fast" ? 2 : 1) * (has(header("anthropic-beta"), "fast-mode-2026-02-01") ? 2.5 : 1)`
cost, trace, err := billingexpr.RunExprWithRequest(
exprStr,
billingexpr.TokenParams{P: 10},
billingexpr.RequestInput{
Headers: map[string]string{
"Anthropic-Beta": "fast-mode-2026-02-01",
},
Body: []byte(`{"service_tier":"fast"}`),
},
)
require.NoError(t, err)
assert.InDelta(t, 100, cost, 1e-6)
assert.Equal(t, "base", trace.MatchedTier)
assert.Equal(t, []billingexpr.RequestRuleTrace{
{Cond: `param("service_tier") == "fast"`, Multiplier: 2, Matched: true},
{Cond: `has(header("anthropic-beta"), "fast-mode-2026-02-01")`, Multiplier: 2.5, Matched: true},
}, trace.RequestRules)
}
func TestRequestProbeTraceIncludesUnmatchedFactors(t *testing.T) {
exprStr := `(tier("base", p * 2)) * (param("service_tier") == "fast" ? 2 : 1) * (has(header("anthropic-beta"), "fast-mode") ? 2.5 : 1)`
cost, trace, err := billingexpr.RunExprWithRequest(
exprStr,
billingexpr.TokenParams{P: 10},
billingexpr.RequestInput{Body: []byte(`{"service_tier":"fast"}`)},
)
require.NoError(t, err)
assert.InDelta(t, 40, cost, 1e-6)
assert.Equal(t, []billingexpr.RequestRuleTrace{
{Cond: `param("service_tier") == "fast"`, Multiplier: 2, Matched: true},
{Cond: `has(header("anthropic-beta"), "fast-mode")`, Multiplier: 2.5, Matched: false},
}, trace.RequestRules)
}
func TestRequestProbeTracePreservesIntegerConditionalType(t *testing.T) {
cost, trace, err := billingexpr.RunExprWithRequest(
`5 % (param("service_tier") == "fast" ? 2 : 1)`,
billingexpr.TokenParams{},
billingexpr.RequestInput{Body: []byte(`{"service_tier":"fast"}`)},
)
require.NoError(t, err)
assert.Equal(t, float64(1), cost)
assert.Equal(t, []billingexpr.RequestRuleTrace{
{Cond: `param("service_tier") == "fast"`, Multiplier: 2, Matched: true},
}, trace.RequestRules)
}
func TestRequestProbeNonUnitFallbackIsNotTraced(t *testing.T) {
cost, trace, err := billingexpr.RunExprWithRequest(
`10 * (param("service_tier") == "fast" ? 2 : 1.5)`,
billingexpr.TokenParams{},
billingexpr.RequestInput{Body: []byte(`{"service_tier":"standard"}`)},
)
require.NoError(t, err)
assert.InDelta(t, 15, cost, 1e-6)
assert.Empty(t, trace.RequestRules)
}
func TestRequestProbeInternalTraceFunctionIsReserved(t *testing.T) {
_, err := billingexpr.CompileFromCache(`_trace(0, true, 5.0)`)
require.ErrorContains(t, err, `identifier "_trace" is reserved for internal use`)
}
func TestCeilFloor(t *testing.T) {
cost, _, err := billingexpr.RunExpr("ceil(p / 1000) * 0.5", billingexpr.TokenParams{P: 1500})
if err != nil {
t.Fatal(err)
}
want := math.Ceil(1500.0/1000) * 0.5
if math.Abs(cost-want) > 1e-6 {
t.Errorf("cost = %f, want %f", cost, want)
}
}
// ---------------------------------------------------------------------------
// Zero tokens
// ---------------------------------------------------------------------------
func TestZeroTokens(t *testing.T) {
cost, _, err := billingexpr.RunExpr(claudeExpr, billingexpr.TokenParams{})
if err != nil {
t.Fatal(err)
}
if cost != 0 {
t.Errorf("cost should be 0 for zero tokens, got %f", cost)
}
}
// ---------------------------------------------------------------------------
// Rounding
// ---------------------------------------------------------------------------
func TestQuotaRound(t *testing.T) {
tests := []struct {
in float64
want int
}{
{0, 0},
{0.4, 0},
{0.5, 1},
{0.6, 1},
{1.5, 2},
{-0.5, -1},
{-0.6, -1},
{999.4999, 999},
{999.5, 1000},
{1e9 + 0.5, 1e9 + 1},
// Oversized expression results saturate at the single-request limit (delegated to
// common.QuotaRound); full saturation coverage lives in common.
{3.6893488147419103e19, common.MaxQuota},
}
for _, tt := range tests {
got := billingexpr.QuotaRound(tt.in)
if got != tt.want {
t.Errorf("QuotaRound(%f) = %d, want %d", tt.in, got, tt.want)
}
}
}
// ---------------------------------------------------------------------------
// Settlement
// ---------------------------------------------------------------------------
func TestComputeTieredQuota_Basic(t *testing.T) {
snap := &billingexpr.BillingSnapshot{
BillingMode: "tiered_expr",
ExprString: claudeExpr,
ExprHash: billingexpr.ExprHashString(claudeExpr),
GroupRatio: 1.0,
EstimatedPromptTokens: 100000,
EstimatedCompletionTokens: 5000,
EstimatedQuotaBeforeGroup: (100000*1.5 + 5000*7.5) / 1_000_000 * 500_000,
EstimatedQuotaAfterGroup: billingexpr.QuotaRound((100000*1.5 + 5000*7.5) / 1_000_000 * 500_000),
EstimatedTier: "standard",
QuotaPerUnit: 500_000,
}
result, err := billingexpr.ComputeTieredQuota(snap, billingexpr.TokenParams{P: 300000, C: 10000})
if err != nil {
t.Fatal(err)
}
wantBefore := (300000*3.0 + 10000*11.25) / 1_000_000 * 500_000
if math.Abs(result.ActualQuotaBeforeGroup-wantBefore) > 1e-6 {
t.Errorf("before group: got %f, want %f", result.ActualQuotaBeforeGroup, wantBefore)
}
if result.MatchedTier != "long_context" {
t.Errorf("tier = %q, want %q", result.MatchedTier, "long_context")
}
if !result.CrossedTier {
t.Error("expected crossed_tier=true (estimated standard, actual long_context)")
}
}
func TestComputeTieredQuota_SameTier(t *testing.T) {
snap := &billingexpr.BillingSnapshot{
BillingMode: "tiered_expr",
ExprString: claudeExpr,
ExprHash: billingexpr.ExprHashString(claudeExpr),
GroupRatio: 1.5,
EstimatedPromptTokens: 50000,
EstimatedCompletionTokens: 1000,
EstimatedQuotaBeforeGroup: (50000*1.5 + 1000*7.5) / 1_000_000 * 500_000,
EstimatedQuotaAfterGroup: billingexpr.QuotaRound((50000*1.5 + 1000*7.5) / 1_000_000 * 500_000 * 1.5),
EstimatedTier: "standard",
QuotaPerUnit: 500_000,
}
result, err := billingexpr.ComputeTieredQuota(snap, billingexpr.TokenParams{P: 80000, C: 2000})
if err != nil {
t.Fatal(err)
}
wantBefore := (80000*1.5 + 2000*7.5) / 1_000_000 * 500_000
wantAfter := billingexpr.QuotaRound(wantBefore * 1.5)
if result.ActualQuotaAfterGroup != wantAfter {
t.Errorf("after group: got %d, want %d", result.ActualQuotaAfterGroup, wantAfter)
}
if result.CrossedTier {
t.Error("expected crossed_tier=false (both standard)")
}
}
// ---------------------------------------------------------------------------
// Compile errors
// ---------------------------------------------------------------------------
func TestCompileError(t *testing.T) {
_, _, err := billingexpr.RunExpr("invalid +-+ syntax", billingexpr.TokenParams{})
if err == nil {
t.Error("expected compile error")
}
}
// ---------------------------------------------------------------------------
// Compile Cache
// ---------------------------------------------------------------------------
func TestCompileCache_SameResult(t *testing.T) {
r1, _, err := billingexpr.RunExpr("p * 0.5", billingexpr.TokenParams{P: 100})
if err != nil {
t.Fatal(err)
}
r2, _, err := billingexpr.RunExpr("p * 0.5", billingexpr.TokenParams{P: 100})
if err != nil {
t.Fatal(err)
}
if r1 != r2 {
t.Errorf("cached and uncached results differ: %f != %f", r1, r2)
}
}
func TestInvalidateCache(t *testing.T) {
billingexpr.InvalidateCache()
r1, _, _ := billingexpr.RunExpr("p * 0.5", billingexpr.TokenParams{P: 100})
billingexpr.InvalidateCache()
r2, _, _ := billingexpr.RunExpr("p * 0.5", billingexpr.TokenParams{P: 100})
if r1 != r2 {
t.Errorf("post-invalidate results differ: %f != %f", r1, r2)
}
}
// ---------------------------------------------------------------------------
// Hash
// ---------------------------------------------------------------------------
func TestExprHashString_Deterministic(t *testing.T) {
h1 := billingexpr.ExprHashString("p * 0.5")
h2 := billingexpr.ExprHashString("p * 0.5")
if h1 != h2 {
t.Error("hash should be deterministic")
}
h3 := billingexpr.ExprHashString("p * 0.6")
if h1 == h3 {
t.Error("different expressions should have different hashes")
}
}
// ---------------------------------------------------------------------------
// Cache variables: present
// ---------------------------------------------------------------------------
const claudeWithCacheExpr = `p <= 200000 ? tier("standard", p * 1.5 + c * 7.5 + cr * 0.15 + cc * 1.875) : tier("long_context", p * 3.0 + c * 11.25 + cr * 0.3 + cc * 3.75)`
func TestCachePresent_StandardTier(t *testing.T) {
params := billingexpr.TokenParams{P: 100000, C: 5000, CR: 50000, CC: 10000}
cost, trace, err := billingexpr.RunExpr(claudeWithCacheExpr, params)
if err != nil {
t.Fatal(err)
}
want := 100000*1.5 + 5000*7.5 + 50000*0.15 + 10000*1.875
if math.Abs(cost-want) > 1e-6 {
t.Errorf("cost = %f, want %f", cost, want)
}
if trace.MatchedTier != "standard" {
t.Errorf("tier = %q, want %q", trace.MatchedTier, "standard")
}
}
func TestCachePresent_LongContextTier(t *testing.T) {
params := billingexpr.TokenParams{P: 300000, C: 10000, CR: 100000, CC: 20000}
cost, trace, err := billingexpr.RunExpr(claudeWithCacheExpr, params)
if err != nil {
t.Fatal(err)
}
want := 300000*3.0 + 10000*11.25 + 100000*0.3 + 20000*3.75
if math.Abs(cost-want) > 1e-6 {
t.Errorf("cost = %f, want %f", cost, want)
}
if trace.MatchedTier != "long_context" {
t.Errorf("tier = %q, want %q", trace.MatchedTier, "long_context")
}
}
// ---------------------------------------------------------------------------
// Cache variables: absent (all zero) — same expression still works
// ---------------------------------------------------------------------------
func TestCacheAbsent_ZeroCacheTokens(t *testing.T) {
params := billingexpr.TokenParams{P: 100000, C: 5000}
cost, trace, err := billingexpr.RunExpr(claudeWithCacheExpr, params)
if err != nil {
t.Fatal(err)
}
want := 100000*1.5 + 5000*7.5
if math.Abs(cost-want) > 1e-6 {
t.Errorf("cost = %f, want %f (cache terms should be 0)", cost, want)
}
if trace.MatchedTier != "standard" {
t.Errorf("tier = %q, want %q", trace.MatchedTier, "standard")
}
}
// ---------------------------------------------------------------------------
// Mixed cache fields: cc and cc1h non-zero
// ---------------------------------------------------------------------------
const claudeCacheSplitExpr = `tier("default", p * 1.5 + c * 7.5 + cr * 0.15 + cc * 2.0 + cc1h * 3.0)`
func TestMixedCacheFields(t *testing.T) {
params := billingexpr.TokenParams{P: 100000, C: 5000, CR: 10000, CC: 5000, CC1h: 2000}
cost, _, err := billingexpr.RunExpr(claudeCacheSplitExpr, params)
if err != nil {
t.Fatal(err)
}
want := 100000*1.5 + 5000*7.5 + 10000*0.15 + 5000*2.0 + 2000*3.0
if math.Abs(cost-want) > 1e-6 {
t.Errorf("cost = %f, want %f", cost, want)
}
}
func TestMixedCacheFields_AllCacheZero(t *testing.T) {
params := billingexpr.TokenParams{P: 100000, C: 5000}
cost, _, err := billingexpr.RunExpr(claudeCacheSplitExpr, params)
if err != nil {
t.Fatal(err)
}
want := 100000*1.5 + 5000*7.5
if math.Abs(cost-want) > 1e-6 {
t.Errorf("cost = %f, want %f (all cache zero)", cost, want)
}
}
// ---------------------------------------------------------------------------
// Backward compatibility: p+c only expressions still work with TokenParams
// ---------------------------------------------------------------------------
func TestBackwardCompat_OldExprWithTokenParams(t *testing.T) {
params := billingexpr.TokenParams{P: 100000, C: 5000, CR: 99999, CC: 88888}
cost, trace, err := billingexpr.RunExpr(claudeExpr, params)
if err != nil {
t.Fatal(err)
}
want := 100000*1.5 + 5000*7.5
if math.Abs(cost-want) > 1e-6 {
t.Errorf("cost = %f, want %f (old expr ignores cache fields)", cost, want)
}
if trace.MatchedTier != "standard" {
t.Errorf("tier = %q, want %q", trace.MatchedTier, "standard")
}
}
// ---------------------------------------------------------------------------
// Settlement with cache tokens
// ---------------------------------------------------------------------------
func TestComputeTieredQuota_WithCache(t *testing.T) {
snap := &billingexpr.BillingSnapshot{
BillingMode: "tiered_expr",
ExprString: claudeWithCacheExpr,
ExprHash: billingexpr.ExprHashString(claudeWithCacheExpr),
GroupRatio: 1.0,
EstimatedPromptTokens: 100000,
EstimatedCompletionTokens: 5000,
EstimatedQuotaBeforeGroup: (100000*1.5 + 5000*7.5) / 1_000_000 * 500_000,
EstimatedQuotaAfterGroup: billingexpr.QuotaRound((100000*1.5 + 5000*7.5) / 1_000_000 * 500_000),
EstimatedTier: "standard",
QuotaPerUnit: 500_000,
}
params := billingexpr.TokenParams{P: 100000, C: 5000, CR: 50000, CC: 10000}
result, err := billingexpr.ComputeTieredQuota(snap, params)
if err != nil {
t.Fatal(err)
}
wantBefore := (100000*1.5 + 5000*7.5 + 50000*0.15 + 10000*1.875) / 1_000_000 * 500_000
if math.Abs(result.ActualQuotaBeforeGroup-wantBefore) > 1e-6 {
t.Errorf("before group: got %f, want %f", result.ActualQuotaBeforeGroup, wantBefore)
}
if result.MatchedTier != "standard" {
t.Errorf("tier = %q, want %q", result.MatchedTier, "standard")
}
if result.CrossedTier {
t.Error("expected crossed_tier=false (same tier)")
}
}
func TestComputeTieredQuota_WithCacheCrossTier(t *testing.T) {
snap := &billingexpr.BillingSnapshot{
BillingMode: "tiered_expr",
ExprString: claudeWithCacheExpr,
ExprHash: billingexpr.ExprHashString(claudeWithCacheExpr),
GroupRatio: 2.0,
EstimatedPromptTokens: 100000,
EstimatedCompletionTokens: 5000,
EstimatedQuotaBeforeGroup: (100000*1.5 + 5000*7.5) / 1_000_000 * 500_000,
EstimatedQuotaAfterGroup: billingexpr.QuotaRound((100000*1.5 + 5000*7.5) / 1_000_000 * 500_000 * 2.0),
EstimatedTier: "standard",
QuotaPerUnit: 500_000,
}
params := billingexpr.TokenParams{P: 300000, C: 10000, CR: 50000, CC: 10000}
result, err := billingexpr.ComputeTieredQuota(snap, params)
if err != nil {
t.Fatal(err)
}
wantBefore := (300000*3.0 + 10000*11.25 + 50000*0.3 + 10000*3.75) / 1_000_000 * 500_000
wantAfter := billingexpr.QuotaRound(wantBefore * 2.0)
if math.Abs(result.ActualQuotaBeforeGroup-wantBefore) > 1e-6 {
t.Errorf("before group: got %f, want %f", result.ActualQuotaBeforeGroup, wantBefore)
}
if result.ActualQuotaAfterGroup != wantAfter {
t.Errorf("after group: got %d, want %d", result.ActualQuotaAfterGroup, wantAfter)
}
if !result.CrossedTier {
t.Error("expected crossed_tier=true (estimated standard, actual long_context)")
}
}
// ---------------------------------------------------------------------------
// Settlement-level tests for ComputeTieredQuota
// ---------------------------------------------------------------------------
func TestComputeTieredQuota_BasicSettlement(t *testing.T) {
exprStr := `tier("default", p + c)`
snap := &billingexpr.BillingSnapshot{
BillingMode: "tiered_expr",
ExprString: exprStr,
ExprHash: billingexpr.ExprHashString(exprStr),
GroupRatio: 1.0,
QuotaPerUnit: 500_000,
}
result, err := billingexpr.ComputeTieredQuota(snap, billingexpr.TokenParams{P: 3000, C: 2000})
if err != nil {
t.Fatal(err)
}
// exprOutput = 5000; quota = 5000 / 1M * 500K = 2500
if math.Abs(result.ActualQuotaBeforeGroup-2500) > 1e-6 {
t.Errorf("before group = %f, want 2500", result.ActualQuotaBeforeGroup)
}
if result.ActualQuotaAfterGroup != 2500 {
t.Errorf("after group = %d, want 2500", result.ActualQuotaAfterGroup)
}
if result.MatchedTier != "default" {
t.Errorf("tier = %q, want default", result.MatchedTier)
}
}
func TestComputeTieredQuota_WithGroupRatio(t *testing.T) {
exprStr := `tier("default", p + c)`
snap := &billingexpr.BillingSnapshot{
BillingMode: "tiered_expr",
ExprString: exprStr,
ExprHash: billingexpr.ExprHashString(exprStr),
GroupRatio: 2.0,
QuotaPerUnit: 500_000,
}
result, err := billingexpr.ComputeTieredQuota(snap, billingexpr.TokenParams{P: 1000, C: 500})
if err != nil {
t.Fatal(err)
}
// exprOutput = 1500; quotaBeforeGroup = 750; afterGroup = round(750 * 2.0) = 1500
if result.ActualQuotaAfterGroup != 1500 {
t.Errorf("after group = %d, want 1500", result.ActualQuotaAfterGroup)
}
}
func TestComputeTieredQuota_ZeroTokens(t *testing.T) {
exprStr := `tier("default", p * 2 + c * 10)`
snap := &billingexpr.BillingSnapshot{
BillingMode: "tiered_expr",
ExprString: exprStr,
ExprHash: billingexpr.ExprHashString(exprStr),
GroupRatio: 1.0,
QuotaPerUnit: 500_000,
}
result, err := billingexpr.ComputeTieredQuota(snap, billingexpr.TokenParams{})
if err != nil {
t.Fatal(err)
}
if result.ActualQuotaAfterGroup != 0 {
t.Errorf("after group = %d, want 0", result.ActualQuotaAfterGroup)
}
}
func TestComputeTieredQuota_RoundingEdge(t *testing.T) {
exprStr := `tier("default", p * 0.5)` // 3 * 0.5 = 1.5 (expr); 1.5 / 1M * 500K = 0.75; round(0.75) = 1
snap := &billingexpr.BillingSnapshot{
BillingMode: "tiered_expr",
ExprString: exprStr,
ExprHash: billingexpr.ExprHashString(exprStr),
GroupRatio: 1.0,
QuotaPerUnit: 500_000,
}
result, err := billingexpr.ComputeTieredQuota(snap, billingexpr.TokenParams{P: 3})
if err != nil {
t.Fatal(err)
}
// 3 * 0.5 = 1.5 (expr); quota = 1.5 / 1M * 500K = 0.75; round(0.75) = 1
if result.ActualQuotaAfterGroup != 1 {
t.Errorf("after group = %d, want 1 (round 0.75 up)", result.ActualQuotaAfterGroup)
}
}
func TestComputeTieredQuota_RoundingEdgeDown(t *testing.T) {
exprStr := `tier("default", p * 0.4)` // 3 * 0.4 = 1.2 (expr); 1.2 / 1M * 500K = 0.6; round(0.6) = 1
snap := &billingexpr.BillingSnapshot{
BillingMode: "tiered_expr",
ExprString: exprStr,
ExprHash: billingexpr.ExprHashString(exprStr),
GroupRatio: 1.0,
QuotaPerUnit: 500_000,
}
result, err := billingexpr.ComputeTieredQuota(snap, billingexpr.TokenParams{P: 3})
if err != nil {
t.Fatal(err)
}
// 3 * 0.4 = 1.2 (expr); quota = 1.2 / 1M * 500K = 0.6; round(0.6) = 1
if result.ActualQuotaAfterGroup != 1 {
t.Errorf("after group = %d, want 1 (round 0.6 up)", result.ActualQuotaAfterGroup)
}
}
func TestComputeTieredQuotaWithRequest_ProbeAffectsQuota(t *testing.T) {
exprStr := `param("fast") == true ? tier("fast", p * 4) : tier("normal", p * 2)`
snap := &billingexpr.BillingSnapshot{
BillingMode: "tiered_expr",
ExprString: exprStr,
ExprHash: billingexpr.ExprHashString(exprStr),
GroupRatio: 1.0,
EstimatedTier: "normal",
QuotaPerUnit: 500_000,
}
// Without request: normal tier
r1, err := billingexpr.ComputeTieredQuota(snap, billingexpr.TokenParams{P: 1000})
if err != nil {
t.Fatal(err)
}
// normal: p*2 = 2000; quota = 2000 / 1M * 500K = 1000
if r1.ActualQuotaAfterGroup != 1000 {
t.Errorf("normal = %d, want 1000", r1.ActualQuotaAfterGroup)
}
// With request: fast tier
r2, err := billingexpr.ComputeTieredQuotaWithRequest(snap, billingexpr.TokenParams{P: 1000}, billingexpr.RequestInput{
Body: []byte(`{"fast":true}`),
})
if err != nil {
t.Fatal(err)
}
// fast: p*4 = 4000; quota = 4000 / 1M * 500K = 2000
if r2.ActualQuotaAfterGroup != 2000 {
t.Errorf("fast = %d, want 2000", r2.ActualQuotaAfterGroup)
}
if !r2.CrossedTier {
t.Error("expected CrossedTier = true when probe changes tier")
}
}
func TestComputeTieredQuota_BoundaryTierCrossing(t *testing.T) {
exprStr := `p <= 100000 ? tier("small", p * 1) : tier("large", p * 2)`
snap := &billingexpr.BillingSnapshot{
BillingMode: "tiered_expr",
ExprString: exprStr,
ExprHash: billingexpr.ExprHashString(exprStr),
GroupRatio: 1.0,
EstimatedTier: "small",
QuotaPerUnit: 500_000,
}
// At boundary: small, p*1 = 100000; quota = 100000 / 1M * 500K = 50000
r1, err := billingexpr.ComputeTieredQuota(snap, billingexpr.TokenParams{P: 100000})
if err != nil {
t.Fatal(err)
}
if r1.MatchedTier != "small" {
t.Errorf("at boundary: tier = %s, want small", r1.MatchedTier)
}
if r1.ActualQuotaAfterGroup != 50000 {
t.Errorf("at boundary: quota = %d, want 50000", r1.ActualQuotaAfterGroup)
}
// Past boundary: large, p*2 = 200002; quota = 200002 / 1M * 500K = 100001
r2, err := billingexpr.ComputeTieredQuota(snap, billingexpr.TokenParams{P: 100001})
if err != nil {
t.Fatal(err)
}
if r2.MatchedTier != "large" {
t.Errorf("past boundary: tier = %s, want large", r2.MatchedTier)
}
if r2.ActualQuotaAfterGroup != 100001 {
t.Errorf("past boundary: quota = %d, want 100001", r2.ActualQuotaAfterGroup)
}
if !r2.CrossedTier {
t.Error("expected CrossedTier = true")
}
}
// ---------------------------------------------------------------------------
// Time function tests
// ---------------------------------------------------------------------------
func TestTimeFunctions_ValidTimezone(t *testing.T) {
exprStr := `tier("default", p) * (hour("UTC") >= 0 ? 1 : 1)`
cost, _, err := billingexpr.RunExpr(exprStr, billingexpr.TokenParams{P: 100})
if err != nil {
t.Fatal(err)
}
if cost != 100 {
t.Errorf("cost = %f, want 100", cost)
}
}
func TestTimeFunctions_AllFunctionsCompile(t *testing.T) {
exprStr := `tier("default", p) * (hour("Asia/Shanghai") >= 0 ? 1 : 1) * (minute("UTC") >= 0 ? 1 : 1) * (weekday("UTC") >= 0 ? 1 : 1) * (month("UTC") >= 1 ? 1 : 1) * (day("UTC") >= 1 ? 1 : 1)`
cost, _, err := billingexpr.RunExpr(exprStr, billingexpr.TokenParams{P: 500})
if err != nil {
t.Fatal(err)
}
if cost != 500 {
t.Errorf("cost = %f, want 500", cost)
}
}
func TestTimeFunctions_InvalidTimezone(t *testing.T) {
exprStr := `tier("default", p) * (hour("Invalid/Zone") >= 0 ? 1 : 2)`
cost, _, err := billingexpr.RunExpr(exprStr, billingexpr.TokenParams{P: 100})
if err != nil {
t.Fatal(err)
}
// Invalid timezone falls back to UTC; hour is 0-23, so condition is always true
if cost != 100 {
t.Errorf("cost = %f, want 100 (fallback to UTC)", cost)
}
}
func TestTimeFunctions_EmptyTimezone(t *testing.T) {
exprStr := `tier("default", p) * (hour("") >= 0 ? 1 : 2)`
cost, _, err := billingexpr.RunExpr(exprStr, billingexpr.TokenParams{P: 100})
if err != nil {
t.Fatal(err)
}
if cost != 100 {
t.Errorf("cost = %f, want 100 (empty tz -> UTC)", cost)
}
}
func TestTimeFunctions_NightDiscountPattern(t *testing.T) {
exprStr := `tier("default", p * 2 + c * 10) * (hour("UTC") >= 21 || hour("UTC") < 6 ? 0.5 : 1)`
cost, _, err := billingexpr.RunExpr(exprStr, billingexpr.TokenParams{P: 1000, C: 500})
if err != nil {
t.Fatal(err)
}
// Base = 1000*2 + 500*10 = 7000; multiplier is either 0.5 or 1 depending on current UTC hour
if cost != 7000 && cost != 3500 {
t.Errorf("cost = %f, want 7000 or 3500", cost)
}
}
func TestTimeFunctions_WeekdayRange(t *testing.T) {
exprStr := `tier("default", p) * (weekday("UTC") >= 0 && weekday("UTC") <= 6 ? 1 : 999)`
cost, _, err := billingexpr.RunExpr(exprStr, billingexpr.TokenParams{P: 100})
if err != nil {
t.Fatal(err)
}
// weekday is always 0-6, so multiplier is always 1
if cost != 100 {
t.Errorf("cost = %f, want 100", cost)
}
}
func TestTimeFunctions_MonthDayPattern(t *testing.T) {
exprStr := `tier("default", p) * (month("Asia/Shanghai") == 1 && day("Asia/Shanghai") == 1 ? 0.5 : 1)`
cost, _, err := billingexpr.RunExpr(exprStr, billingexpr.TokenParams{P: 1000})
if err != nil {
t.Fatal(err)
}
// Either 1000 (not Jan 1) or 500 (Jan 1) — both are valid
if cost != 1000 && cost != 500 {
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 TestImageAudioVariables(t *testing.T) {
exprStr := `tier("base", p * 1 + img * 3 + ai * 5 + ao * 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)
}
}
// ---------------------------------------------------------------------------
// len variable tests — tier conditions based on context length
// ---------------------------------------------------------------------------
const lenTieredExpr = `len <= 200000 ? tier("standard", p * 3 + c * 15 + cr * 0.3) : tier("long_context", p * 6 + c * 22.5 + cr * 0.6)`
func TestLen_StandardTier(t *testing.T) {
params := billingexpr.TokenParams{P: 80000, C: 5000, Len: 100000, CR: 20000}
cost, trace, err := billingexpr.RunExpr(lenTieredExpr, params)
if err != nil {
t.Fatal(err)
}
want := 80000*3 + 5000*15 + 20000*0.3
if math.Abs(cost-want) > 1e-6 {
t.Errorf("cost = %f, want %f", cost, want)
}
if trace.MatchedTier != "standard" {
t.Errorf("tier = %q, want standard", trace.MatchedTier)
}
}
func TestLen_LongContextTier(t *testing.T) {
// p is low (cache subtracted), but len is high (full context)
params := billingexpr.TokenParams{P: 50000, C: 5000, Len: 300000, CR: 250000}
cost, trace, err := billingexpr.RunExpr(lenTieredExpr, params)
if err != nil {
t.Fatal(err)
}
want := 50000*6 + 5000*22.5 + 250000*0.6
if math.Abs(cost-want) > 1e-6 {
t.Errorf("cost = %f, want %f", cost, want)
}
if trace.MatchedTier != "long_context" {
t.Errorf("tier = %q, want long_context (len=300000 > 200000)", trace.MatchedTier)
}
}
func TestLen_BoundaryExact(t *testing.T) {
params := billingexpr.TokenParams{P: 100000, C: 1000, Len: 200000, CR: 100000}
_, trace, err := billingexpr.RunExpr(lenTieredExpr, params)
if err != nil {
t.Fatal(err)
}
if trace.MatchedTier != "standard" {
t.Errorf("tier = %q, want standard (len=200000 <= 200000)", trace.MatchedTier)
}
}
func TestLen_BoundaryPlusOne(t *testing.T) {
params := billingexpr.TokenParams{P: 100000, C: 1000, Len: 200001, CR: 100001}
_, trace, err := billingexpr.RunExpr(lenTieredExpr, params)
if err != nil {
t.Fatal(err)
}
if trace.MatchedTier != "long_context" {
t.Errorf("tier = %q, want long_context (len=200001 > 200000)", trace.MatchedTier)
}
}
func TestLen_ZeroDefaultsToZero(t *testing.T) {
// len defaults to 0 when not set
params := billingexpr.TokenParams{P: 1000, C: 500}
_, trace, err := billingexpr.RunExpr(lenTieredExpr, params)
if err != nil {
t.Fatal(err)
}
if trace.MatchedTier != "standard" {
t.Errorf("tier = %q, want standard (len=0 <= 200000)", trace.MatchedTier)
}
}
// ---------------------------------------------------------------------------
// Benchmarks: compile vs cached execution
// ---------------------------------------------------------------------------
const benchComplexExpr = `len <= 200000 ? tier("standard", p * 3 + c * 15 + cr * 0.3 + cc * 3.75 + cc1h * 6 + img * 3 + img_o * 30 + ai * 10 + ao * 40) : tier("long_context", p * 6 + c * 22.5 + cr * 0.6 + cc * 7.5 + cc1h * 12 + img * 6 + img_o * 60 + ai * 20 + ao * 80)`
func BenchmarkExprCompile(b *testing.B) {
for i := 0; i < b.N; i++ {
billingexpr.InvalidateCache()
billingexpr.CompileFromCache(benchComplexExpr)
}
}
func BenchmarkExprRunCached(b *testing.B) {
billingexpr.CompileFromCache(benchComplexExpr)
params := billingexpr.TokenParams{P: 150000, C: 10000, Len: 188000, CR: 30000, CC: 5000, Img: 2000, AI: 1000, AO: 500}
b.ResetTimer()
for i := 0; i < b.N; i++ {
billingexpr.RunExpr(benchComplexExpr, params)
}
}
// Shared fixtures protect the frontend simulator against drift from the real engine.
func TestFrontendSimulationContract(t *testing.T) {
data, err := os.ReadFile("testdata/frontend_simulation.json")
require.NoError(t, err)
var cases []struct {
Name string
Expression string
Tokens billingexpr.TokenParams
Body map[string]any
Headers map[string]string
Usage map[string]any
Cost float64
Tier string
Multipliers []float64
Matched []bool
BillingUnit billingexpr.BillingUnit
FixedPrice *float64
}
require.NoError(t, common.Unmarshal(data, &cases))
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
body, err := common.Marshal(tc.Body)
require.NoError(t, err)
cost, trace, err := billingexpr.RunExprWithRequest(tc.Expression, tc.Tokens, billingexpr.RequestInput{Body: body, Headers: tc.Headers, Usage: tc.Usage})
require.NoError(t, err)
assert.InDelta(t, tc.Cost, cost, 1e-9)
assert.Equal(t, tc.Tier, trace.MatchedTier)
if tc.BillingUnit != "" {
assert.Equal(t, tc.BillingUnit, trace.BillingUnit)
assert.Equal(t, tc.FixedPrice, trace.FixedPrice)
}
require.Len(t, trace.RequestRules, len(tc.Multipliers))
for i, rule := range trace.RequestRules {
assert.Equal(t, tc.Multipliers[i], rule.Multiplier)
assert.Equal(t, tc.Matched[i], rule.Matched)
}
})
}
}