| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023 |
- package billingexpr_test
- import (
- "math"
- "math/rand"
- "testing"
- "github.com/QuantumNous/new-api/pkg/billingexpr"
- )
- // ---------------------------------------------------------------------------
- // 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 TestRequestProbeMultipleRulesMultiply(t *testing.T) {
- cost, _, err := billingexpr.RunExprWithRequest(
- `(param("service_tier") == "fast" ? 2 : 1) * (has(header("anthropic-beta"), "fast-mode-2026-02-01") ? 2.5 : 1)`,
- billingexpr.TokenParams{},
- billingexpr.RequestInput{
- Headers: map[string]string{
- "Anthropic-Beta": "fast-mode-2026-02-01",
- },
- Body: []byte(`{"service_tier":"fast"}`),
- },
- )
- if err != nil {
- t.Fatal(err)
- }
- if math.Abs(cost-5) > 1e-6 {
- t.Errorf("cost = %f, want 5", cost)
- }
- }
- 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},
- }
- 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)")
- }
- }
- // ---------------------------------------------------------------------------
- // Fuzz: random p/c/cache, verify non-negative result
- // ---------------------------------------------------------------------------
- func TestFuzz_NonNegativeResults(t *testing.T) {
- exprs := []string{
- claudeExpr,
- claudeWithCacheExpr,
- glmExpr,
- "p * 0.5 + c * 1.0",
- "max(p, c) * 0.1",
- "p * 0.5 + cr * 0.1 + cc * 0.2",
- }
- rng := rand.New(rand.NewSource(42))
- for _, exprStr := range exprs {
- for i := 0; i < 500; i++ {
- params := billingexpr.TokenParams{
- P: math.Round(rng.Float64() * 1000000),
- C: math.Round(rng.Float64() * 500000),
- CR: math.Round(rng.Float64() * 200000),
- CC: math.Round(rng.Float64() * 50000),
- CC1h: math.Round(rng.Float64() * 10000),
- }
- cost, _, err := billingexpr.RunExpr(exprStr, params)
- if err != nil {
- t.Fatalf("expr=%q params=%+v: %v", exprStr, params, err)
- }
- if cost < 0 {
- t.Errorf("expr=%q params=%+v: negative cost %f", exprStr, params, cost)
- }
- }
- }
- }
- func TestFuzz_SettlementConsistency(t *testing.T) {
- rng := rand.New(rand.NewSource(99))
- for i := 0; i < 200; i++ {
- estParams := billingexpr.TokenParams{
- P: math.Round(rng.Float64() * 500000),
- C: math.Round(rng.Float64() * 100000),
- CR: math.Round(rng.Float64() * 100000),
- CC: math.Round(rng.Float64() * 30000),
- }
- actParams := billingexpr.TokenParams{
- P: math.Round(rng.Float64() * 500000),
- C: math.Round(rng.Float64() * 100000),
- CR: math.Round(rng.Float64() * 100000),
- CC: math.Round(rng.Float64() * 30000),
- }
- groupRatio := 0.5 + rng.Float64()*2.0
- estCost, estTrace, _ := billingexpr.RunExpr(claudeWithCacheExpr, estParams)
- const qpu = 500_000.0
- snap := &billingexpr.BillingSnapshot{
- BillingMode: "tiered_expr",
- ExprString: claudeWithCacheExpr,
- ExprHash: billingexpr.ExprHashString(claudeWithCacheExpr),
- GroupRatio: groupRatio,
- EstimatedPromptTokens: int(estParams.P),
- EstimatedCompletionTokens: int(estParams.C),
- EstimatedQuotaBeforeGroup: estCost / 1_000_000 * qpu,
- EstimatedQuotaAfterGroup: billingexpr.QuotaRound(estCost / 1_000_000 * qpu * groupRatio),
- EstimatedTier: estTrace.MatchedTier,
- QuotaPerUnit: qpu,
- }
- result, err := billingexpr.ComputeTieredQuota(snap, actParams)
- if err != nil {
- t.Fatalf("iter %d: %v", i, err)
- }
- directCost, _, _ := billingexpr.RunExpr(claudeWithCacheExpr, actParams)
- directQuota := billingexpr.QuotaRound(directCost / 1_000_000 * qpu * groupRatio)
- if result.ActualQuotaAfterGroup != directQuota {
- t.Errorf("iter %d: settlement %d != direct %d", i, result.ActualQuotaAfterGroup, directQuota)
- }
- }
- }
- // ---------------------------------------------------------------------------
- // 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)
- }
- }
- // ---------------------------------------------------------------------------
- // Benchmarks: compile vs cached execution
- // ---------------------------------------------------------------------------
- const benchComplexExpr = `p <= 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, CR: 30000, CC: 5000, Img: 2000, AI: 1000, AO: 500}
- b.ResetTimer()
- for i := 0; i < b.N; i++ {
- billingexpr.RunExpr(benchComplexExpr, params)
- }
- }
|