Files
new-api/pkg/billingexpr/testdata/frontend_simulation.json
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

227 lines
4.9 KiB
JSON

[
{
"name": "versioned tokens and cache",
"expression": "v1:tier(\"base\", p * 2 + c * 8 + cr * 0.2)",
"tokens": {
"p": 900,
"c": 100,
"cr": 100
},
"cost": 2620,
"tier": "base"
},
{
"name": "nil false and zero",
"expression": "param(\"missing\") == nil && param(\"zero\") == 0 && param(\"enabled\") == false ? tier(\"ok\", 3.5) : tier(\"bad\", 99)",
"body": {
"zero": 0,
"enabled": false
},
"cost": 3.5,
"tier": "ok"
},
{
"name": "normalized headers and two rules",
"expression": "tier(\"base\", p * 2) * (header(\" X-Priority \") == \"high\" ? 2 : 1) * (param(\"service_tier\") == \"flex\" ? 0.5 : 1)",
"tokens": {
"p": 100
},
"headers": {
" X-Priority ": " high "
},
"body": {
"service_tier": "flex"
},
"cost": 200,
"tier": "base",
"multipliers": [
2,
0.5
],
"matched": [
true,
true
]
},
{
"name": "unmatched rule",
"expression": "tier(\"base\", 10) * (param(\"priority\") == true ? 2 : 1)",
"body": {},
"cost": 10,
"tier": "base",
"multipliers": [
2
],
"matched": [
false
]
},
{
"name": "rules skipped by short circuit",
"expression": "param(\"enabled\") == true ? tier(\"yes\", 10) * (header(\"tier\") == \"fast\" ? 3 : 1) : tier(\"no\", 2)",
"body": {},
"headers": {
"tier": "fast"
},
"cost": 2,
"tier": "no",
"multipliers": [
3
],
"matched": [
false
]
},
{
"name": "noncanonical factor still evaluates",
"expression": "tier(\"base\", 10) * (param(\"priority\") == true ? 2 : 1.5)",
"body": {},
"cost": 15,
"tier": "base"
},
{
"name": "path escapes array indices and length",
"expression": "param(\"a\\\\.b.items.0.price\") + param(\"a\\\\.b.items.#\")",
"body": {
"a.b": {
"items": [
{
"price": 4
},
{
"price": 8
}
]
}
},
"cost": 6,
"tier": ""
},
{
"name": "literal path to constructor",
"expression": "param(\"constructor\") == nil ? 4 : 99",
"body": {},
"cost": 4,
"tier": ""
},
{
"name": "contains scalars and objects",
"expression": "has(param(\"n\"), \"12\") && has(param(\"object\"), \"map[a:1 b:[true <nil>]]\") && !has(nil, \"x\") && !has(\"abc\", \"\") ? 8 : 99",
"body": {
"n": 123,
"object": {
"b": [
true,
null
],
"a": 1
}
},
"cost": 8,
"tier": ""
},
{
"name": "math and numeric division",
"expression": "min(10, max(2, 5)) + abs(-2) + ceil(1.1) + floor(2.9) + 5 / 2 + 7 % 3",
"cost": 14.5,
"tier": ""
},
{
"name": "task token scale and request rule",
"expression": "tier(\"base\", 0.2 + u(\"tokens\") * 10 / 1000000) * (header(\"tier\") == \"fast\" ? 2 : 1)",
"usage": {
"tokens": 500000
},
"headers": {
"tier": "fast"
},
"cost": 10.4,
"tier": "base",
"multipliers": [
2
],
"matched": [
true
]
},
{
"name": "task enum conditions",
"expression": "u(\"mode\") == \"pro\" ? tier(\"pro\", u(\"seconds\") * 0.8) : tier(\"std\", u(\"seconds\") * 0.4)",
"usage": {
"mode": "pro",
"seconds": 5
},
"cost": 4,
"tier": "pro"
},
{
"name": "scientific rates and parentheses",
"expression": "(tier(\"base\", (p * 2e-3 + c * 1E+1)))",
"tokens": {
"p": 1000,
"c": 2
},
"cost": 22,
"tier": "base"
},
{
"name": "fixed request with zero usage",
"expression": "tier(\"request\", fixed(0.01))",
"cost": 10000,
"tier": "request",
"billingUnit": "request",
"fixedPrice": 0.01
},
{
"name": "explicit free request",
"expression": "tier(\"free\", fixed(0))",
"tokens": {
"p": 1000
},
"cost": 0,
"tier": "free",
"billingUnit": "request",
"fixedPrice": 0
},
{
"name": "mixed expression token fallback",
"expression": "len <= 32000 ? tier(\"short\", fixed(0.01)) : tier(\"long\", p * 2 + c * 8)",
"tokens": {
"p": 32001,
"c": 100,
"len": 32001
},
"cost": 64802,
"tier": "long",
"billingUnit": "token"
},
{
"name": "fixed request with multiplier",
"expression": "(len <= 32000 ? tier(\"short\", fixed(0.01)) : tier(\"long\", p * 2 + c * 8)) * (param(\"fast\") == true ? 2 : 1)",
"tokens": {
"len": 1
},
"body": {
"fast": true
},
"cost": 20000,
"tier": "short",
"multipliers": [
2
],
"matched": [
true
],
"billingUnit": "request",
"fixedPrice": 0.01
},
{
"name": "fixed time condition",
"expression": "hour(\"Asia/Shanghai\") >= 0 && hour(\"Asia/Shanghai\") < 24 ? tier(\"today\", fixed(0.02)) : tier(\"fallback\", p * 2)",
"cost": 20000,
"tier": "today",
"billingUnit": "request",
"fixedPrice": 0.02
}
]