/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
/**
* Billing expression parsing utilities.
*
* Parses the dynamic billing expression format so that the pricing breakdown
* UI can be rendered from the same backend expressions.
*
* The grammar is intentionally narrow: we only support the shapes that the
* server emits (tiered pricing + request-rule conditional multipliers), so
* the regular expressions are exact rather than tolerant of arbitrary
* expression syntax.
*/
// ---------------------------------------------------------------------------
// Variable registry
// ---------------------------------------------------------------------------
export type BillingVar = {
key: string
field: string | null
tierField: string | null
label: string
shortLabel: string
side: 'input' | 'output' | 'condition'
isBase?: boolean
isConditionOnly?: boolean
group?: string
}
export const BILLING_VARS: BillingVar[] = [
{
key: 'p',
field: 'inputPrice',
tierField: 'input_unit_cost',
label: 'Input price',
shortLabel: 'Input',
side: 'input',
isBase: true,
},
{
key: 'c',
field: 'outputPrice',
tierField: 'output_unit_cost',
label: 'Completion price',
shortLabel: 'Output',
side: 'output',
isBase: true,
},
{
key: 'len',
field: null,
tierField: null,
label: 'Input length',
shortLabel: 'Length',
side: 'condition',
isConditionOnly: true,
},
{
key: 'cr',
field: 'cacheReadPrice',
tierField: 'cache_read_unit_cost',
label: 'Cache read price',
shortLabel: 'Cache Read',
side: 'input',
group: 'cache',
},
{
key: 'cc',
field: 'cacheCreatePrice',
tierField: 'cache_create_unit_cost',
label: 'Cache create price',
shortLabel: 'Cache Write',
side: 'input',
group: 'cache',
},
{
key: 'cc1h',
field: 'cacheCreate1hPrice',
tierField: 'cache_create_1h_unit_cost',
label: 'Cache create (1h) price',
shortLabel: 'Cache Write (1h)',
side: 'input',
group: 'cache',
},
{
key: 'img',
field: 'imagePrice',
tierField: 'image_unit_cost',
label: 'Image input price',
shortLabel: 'Image In',
side: 'input',
group: 'media',
},
{
key: 'img_o',
field: 'imageOutputPrice',
tierField: 'image_output_unit_cost',
label: 'Image output price',
shortLabel: 'Image Out',
side: 'output',
group: 'media',
},
{
key: 'ai',
field: 'audioInputPrice',
tierField: 'audio_input_unit_cost',
label: 'Audio input price',
shortLabel: 'Audio In',
side: 'input',
group: 'media',
},
{
key: 'ao',
field: 'audioOutputPrice',
tierField: 'audio_output_unit_cost',
label: 'Audio output price',
shortLabel: 'Audio Out',
side: 'output',
group: 'media',
},
]
/** Vars that have real price fields (excludes condition-only vars like `len`) */
export const BILLING_PRICING_VARS: BillingVar[] = BILLING_VARS.filter(
(v) => !v.isConditionOnly
)
/** Vars valid in tier conditions (`p`, `c`, `len`) */
export const BILLING_CONDITION_VARS: string[] = BILLING_VARS.filter(
(v) => v.isBase || v.isConditionOnly
).map((v) => v.key)
const BILLING_VAR_KEY_TO_FIELD = Object.fromEntries(
BILLING_PRICING_VARS.map((v) => [v.key, v.field as string])
) as Record
export const BILLING_EXTRA_VARS: BillingVar[] = BILLING_VARS.filter(
(v) => !v.isBase && !v.isConditionOnly
)
export const BILLING_CACHE_VAR_MAP = BILLING_EXTRA_VARS.map((v) => ({
field: v.tierField as string,
exprVar: v.key,
}))
const BILLING_VAR_REGEX = new RegExp(
`\\b(${BILLING_PRICING_VARS.map((v) => v.key).join('|')})\\s*\\*\\s*([\\d.eE+-]+)`,
'g'
)
// ---------------------------------------------------------------------------
// Request rule constants
// ---------------------------------------------------------------------------
export const SOURCE_PARAM = 'param'
export const SOURCE_HEADER = 'header'
export const SOURCE_TIME = 'time'
export const MATCH_EQ = 'eq'
export const MATCH_CONTAINS = 'contains'
export const MATCH_GT = 'gt'
export const MATCH_GTE = 'gte'
export const MATCH_LT = 'lt'
export const MATCH_LTE = 'lte'
export const MATCH_EXISTS = 'exists'
export const MATCH_RANGE = 'range'
export const TIME_FUNCS = ['hour', 'minute', 'weekday', 'month', 'day'] as const
export type TimeFunc = (typeof TIME_FUNCS)[number]
export const COMMON_TIMEZONES: { value: string; label: string }[] = [
{ value: 'Asia/Shanghai', label: 'UTC+8 Shanghai (Asia/Shanghai)' },
{ value: 'UTC', label: 'UTC' },
{ value: 'America/New_York', label: 'UTC-5 New York (America/New_York)' },
{
value: 'America/Los_Angeles',
label: 'UTC-8 Los Angeles (America/Los_Angeles)',
},
{ value: 'America/Chicago', label: 'UTC-6 Chicago (America/Chicago)' },
{ value: 'Europe/London', label: 'UTC+0 London (Europe/London)' },
{ value: 'Europe/Berlin', label: 'UTC+1 Berlin (Europe/Berlin)' },
{ value: 'Asia/Tokyo', label: 'UTC+9 Tokyo (Asia/Tokyo)' },
{ value: 'Asia/Singapore', label: 'UTC+8 Singapore (Asia/Singapore)' },
{ value: 'Asia/Seoul', label: 'UTC+9 Seoul (Asia/Seoul)' },
{ value: 'Australia/Sydney', label: 'UTC+10 Sydney (Australia/Sydney)' },
]
const NUMERIC_LITERAL_REGEX = /^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/
export type ParamHeaderCondition = {
source: 'param' | 'header'
path: string
mode: string
value: string
}
export type TimeCondition = {
source: 'time'
timeFunc: TimeFunc
timezone: string
mode: string
value: string
rangeStart: string
rangeEnd: string
}
export type RequestCondition = TimeCondition | ParamHeaderCondition
export type RequestRuleGroup = {
conditions: RequestCondition[]
multiplier: string
}
export type TierCondition = {
var: 'p' | 'c' | 'len'
op: '<' | '<=' | '>' | '>='
value: number
}
export type ParsedTier = {
label: string
conditions: TierCondition[]
[field: string]: unknown
}
// ---------------------------------------------------------------------------
// Tier parser
// ---------------------------------------------------------------------------
function stripExprVersion(exprStr: string): { version: number; body: string } {
if (!exprStr) return { version: 1, body: '' }
const m = exprStr.match(/^v(\d+):([\s\S]*)$/)
if (m) return { version: Number(m[1]), body: m[2] }
return { version: 1, body: exprStr }
}
function parseTierBody(bodyStr: string): Record {
const coeffs: Record = {}
const re = new RegExp(BILLING_VAR_REGEX.source, 'g')
let m
while ((m = re.exec(bodyStr)) !== null) {
if (!(m[1] in coeffs)) coeffs[m[1]] = Number(m[2])
}
const tier: Record = {}
for (const [varName, field] of Object.entries(BILLING_VAR_KEY_TO_FIELD)) {
tier[field] = coeffs[varName] || 0
}
return tier
}
export function parseTiersFromExpr(exprStr: string): ParsedTier[] {
if (!exprStr) return []
try {
const { body } = stripExprVersion(exprStr)
const condGroup =
`((?:(?:p|c|len)\\s*(?:<|<=|>|>=)\\s*[\\d.eE+]+)` +
`(?:\\s*&&\\s*(?:p|c|len)\\s*(?:<|<=|>|>=)\\s*[\\d.eE+]+)*)`
const tierRe = new RegExp(
`(?:${condGroup}\\s*\\?\\s*)?tier\\("([^"]*)",\\s*([^)]+)\\)`,
'g'
)
const tiers: ParsedTier[] = []
let m
while ((m = tierRe.exec(body)) !== null) {
const condStr = m[1] || ''
const conditions: TierCondition[] = []
if (condStr) {
for (const cp of condStr.split(/\s*&&\s*/)) {
const cm = cp.trim().match(/^(p|c|len)\s*(<|<=|>|>=)\s*([\d.eE+]+)$/)
if (cm) {
conditions.push({
var: cm[1] as TierCondition['var'],
op: cm[2] as TierCondition['op'],
value: Number(cm[3]),
})
}
}
}
const tier = parseTierBody(m[3]) as ParsedTier
tier.label = m[2]
tier.conditions = conditions
tiers.push(tier)
}
return tiers
} catch {
return []
}
}
export function normalizeTierLabel(label: string | undefined): string {
if (!label) return ''
return label
.replace(/<[==]?|≤|<[==]?/g, '<')
.replace(/>[==]?|≥|>[==]?/g, '>')
.replace(/\s+/g, '')
.toLowerCase()
}
// ---------------------------------------------------------------------------
// Request rule parser
// ---------------------------------------------------------------------------
function splitTopLevelMultiply(expr: string): string[] {
const parts: string[] = []
let start = 0
let depth = 0
for (let index = 0; index < expr.length; index += 1) {
const char = expr[index]
if (char === '(') depth += 1
if (char === ')') depth -= 1
if (depth === 0 && expr.slice(index, index + 3) === ' * ') {
parts.push(expr.slice(start, index).trim())
start = index + 3
index += 2
}
}
parts.push(expr.slice(start).trim())
return parts.filter(Boolean)
}
function splitTopLevelAnd(expr: string): string[] {
const parts: string[] = []
let start = 0
let depth = 0
for (let i = 0; i < expr.length; i += 1) {
const c = expr[i]
if (c === '(') depth += 1
if (c === ')') depth -= 1
if (depth === 0 && expr.slice(i, i + 4) === ' && ') {
parts.push(expr.slice(start, i).trim())
start = i + 4
i += 3
}
}
parts.push(expr.slice(start).trim())
return parts.filter(Boolean)
}
function parseExprLiteral(raw: string): string | null {
const text = raw.trim()
if (text === 'true' || text === 'false') return text
if (NUMERIC_LITERAL_REGEX.test(text)) return text
try {
return JSON.parse(text) as string
} catch {
return null
}
}
function tryParseTimeCondition(expr: string): RequestCondition | null {
let m = expr.match(
/^(hour|minute|weekday|month|day)\("([^"]+)"\) >= ([\d.eE+-]+) \|\| \1\("\2"\) < ([\d.eE+-]+)$/
)
if (m) {
return {
source: 'time',
timeFunc: m[1] as TimeFunc,
timezone: m[2],
mode: MATCH_RANGE,
value: '',
rangeStart: m[3],
rangeEnd: m[4],
}
}
m = expr.match(
/^\((hour|minute|weekday|month|day)\("([^"]+)"\) >= ([\d.eE+-]+) \|\| \1\("\2"\) < ([\d.eE+-]+)\)$/
)
if (m) {
return {
source: 'time',
timeFunc: m[1] as TimeFunc,
timezone: m[2],
mode: MATCH_RANGE,
value: '',
rangeStart: m[3],
rangeEnd: m[4],
}
}
m = expr.match(
/^(hour|minute|weekday|month|day)\("([^"]+)"\) (==|>=|<) ([\d.eE+-]+)$/
)
if (m) {
const opMap: Record = {
'==': MATCH_EQ,
'>=': MATCH_GTE,
'<': MATCH_LT,
}
return {
source: 'time',
timeFunc: m[1] as TimeFunc,
timezone: m[2],
mode: opMap[m[3]] || MATCH_EQ,
value: m[4],
rangeStart: '',
rangeEnd: '',
}
}
return null
}
function tryParseRequestCondition(expr: string): RequestCondition | null {
const tc = tryParseTimeCondition(expr)
if (tc) return tc
let m = expr.match(/^header\("([^"]+)"\) != ""$/)
if (m) return { source: 'header', path: m[1], mode: MATCH_EXISTS, value: '' }
m = expr.match(/^param\("([^"]+)"\) != nil$/)
if (m) return { source: 'param', path: m[1], mode: MATCH_EXISTS, value: '' }
m = expr.match(/^has\(header\("([^"]+)"\), ((?:"(?:[^"\\]|\\.)*"))\)$/)
if (m)
return {
source: 'header',
path: m[1],
mode: MATCH_CONTAINS,
value: JSON.parse(m[2]) as string,
}
m = expr.match(
/^param\("([^"]+)"\) != nil && has\(param\("([^"]+)"\), ((?:"(?:[^"\\]|\\.)*"))\)$/
)
if (m && m[1] === m[2])
return {
source: 'param',
path: m[1],
mode: MATCH_CONTAINS,
value: JSON.parse(m[3]) as string,
}
m = expr.match(
/^param\("([^"]+)"\) != nil && param\("([^"]+)"\) (>|>=|<|<=) ([\d.eE+-]+)$/
)
if (m && m[1] === m[2]) {
const opMap: Record = {
'>': MATCH_GT,
'>=': MATCH_GTE,
'<': MATCH_LT,
'<=': MATCH_LTE,
}
return { source: 'param', path: m[1], mode: opMap[m[3]], value: m[4] }
}
m = expr.match(/^(param|header)\("([^"]+)"\) == (.+)$/)
if (m) {
const parsedValue = parseExprLiteral(m[3])
if (parsedValue === null) return null
return {
source: m[1] as 'param' | 'header',
path: m[2],
mode: MATCH_EQ,
value: String(parsedValue),
}
}
return null
}
function tryParseRuleGroupFactor(part: string): RequestRuleGroup | null {
const m = part.match(/^\((.+) \? ([\d.eE+-]+) : 1\)$/s)
if (!m) return null
const conditionStr = m[1]
const multiplier = m[2]
const andParts = splitTopLevelAnd(conditionStr)
const conditions: RequestCondition[] = []
for (const ap of andParts) {
const cond = tryParseRequestCondition(ap.trim())
if (!cond) return null
conditions.push(cond)
}
if (conditions.length === 0) return null
return { conditions, multiplier }
}
export function tryParseRequestRuleExpr(
expr: string
): RequestRuleGroup[] | null {
const trimmed = (expr || '').trim()
if (!trimmed) return []
const parts = splitTopLevelMultiply(trimmed)
const groups: RequestRuleGroup[] = []
for (const part of parts) {
const group = tryParseRuleGroupFactor(part)
if (!group) return null
groups.push(group)
}
return groups
}
// ---------------------------------------------------------------------------
// Combine / split billing expr and request rules
// ---------------------------------------------------------------------------
function hasFullOuterParens(expr: string): boolean {
if (!expr.startsWith('(') || !expr.endsWith(')')) return false
let depth = 0
for (let i = 0; i < expr.length; i += 1) {
if (expr[i] === '(') depth += 1
if (expr[i] === ')') depth -= 1
if (depth === 0 && i < expr.length - 1) return false
}
return depth === 0
}
function unwrapOuterParens(expr: string): string {
let current = (expr || '').trim()
while (hasFullOuterParens(current)) {
current = current.slice(1, -1).trim()
}
return current
}
export function splitBillingExprAndRequestRules(expr: string): {
billingExpr: string
requestRuleExpr: string
} {
const trimmed = (expr || '').trim()
if (!trimmed) return { billingExpr: '', requestRuleExpr: '' }
const parts = splitTopLevelMultiply(trimmed)
if (parts.length <= 1) return { billingExpr: trimmed, requestRuleExpr: '' }
const ruleParts: string[] = []
const baseParts: string[] = []
parts.forEach((part) => {
const parsed = tryParseRequestRuleExpr(part)
if (parsed && parsed.length > 0) {
ruleParts.push(part)
} else {
baseParts.push(part)
}
})
if (ruleParts.length === 0 || baseParts.length !== 1) {
return { billingExpr: trimmed, requestRuleExpr: '' }
}
return {
billingExpr: unwrapOuterParens(baseParts[0]),
requestRuleExpr: ruleParts.join(' * '),
}
}
export function combineBillingExpr(
baseExpr: string,
requestRuleExpr: string
): string {
const base = (baseExpr || '').trim()
const rules = (requestRuleExpr || '').trim()
if (!base) return ''
if (!rules) return base
return `(${base}) * ${rules}`
}
// ---------------------------------------------------------------------------
// Editor: empty constructors
// ---------------------------------------------------------------------------
export function createEmptyCondition(): ParamHeaderCondition {
return { source: 'param', path: '', mode: MATCH_EQ, value: '' }
}
export function createEmptyTimeCondition(): TimeCondition {
return {
source: 'time',
timeFunc: 'hour',
timezone: 'Asia/Shanghai',
mode: MATCH_GTE,
value: '',
rangeStart: '',
rangeEnd: '',
}
}
export function createEmptyRuleGroup(): RequestRuleGroup {
return { conditions: [createEmptyCondition()], multiplier: '' }
}
export function createEmptyTimeRuleGroup(): RequestRuleGroup {
return { conditions: [createEmptyTimeCondition()], multiplier: '' }
}
// ---------------------------------------------------------------------------
// Editor: match option helpers
// ---------------------------------------------------------------------------
export type MatchOption = { value: string; labelKey: string }
export function getRequestRuleMatchOptions(source: string): MatchOption[] {
if (source === SOURCE_TIME) {
return [
{ value: MATCH_EQ, labelKey: 'Equals' },
{ value: MATCH_GTE, labelKey: 'Greater than or equal' },
{ value: MATCH_LT, labelKey: 'Less than' },
{ value: MATCH_RANGE, labelKey: 'Overnight range' },
]
}
const base: MatchOption[] = [
{ value: MATCH_EQ, labelKey: 'Equals' },
{ value: MATCH_CONTAINS, labelKey: 'Contains' },
{ value: MATCH_EXISTS, labelKey: 'Exists' },
]
if (source === SOURCE_HEADER) return base
return [
...base,
{ value: MATCH_GT, labelKey: 'Greater than' },
{ value: MATCH_GTE, labelKey: 'Greater than or equal' },
{ value: MATCH_LT, labelKey: 'Less than' },
{ value: MATCH_LTE, labelKey: 'Less than or equal' },
]
}
// ---------------------------------------------------------------------------
// Editor: normalize a single condition
// ---------------------------------------------------------------------------
function isTimeFunc(value: unknown): value is TimeFunc {
return typeof value === 'string' && TIME_FUNCS.includes(value as TimeFunc)
}
export function normalizeCondition(
cond: Partial | null | undefined
): RequestCondition {
const source =
cond?.source === 'time'
? 'time'
: cond?.source === 'header'
? 'header'
: 'param'
if (source === 'time') {
const timeCond = cond as Partial | null | undefined
const timeFunc: TimeFunc = isTimeFunc(timeCond?.timeFunc)
? timeCond.timeFunc
: 'hour'
const options = getRequestRuleMatchOptions(SOURCE_TIME)
const mode = options.some((item) => item.value === timeCond?.mode)
? (timeCond?.mode as string)
: MATCH_GTE
return {
source: 'time',
timeFunc,
timezone: timeCond?.timezone || 'Asia/Shanghai',
mode,
value: timeCond?.value == null ? '' : String(timeCond.value),
rangeStart:
timeCond?.rangeStart == null ? '' : String(timeCond.rangeStart),
rangeEnd: timeCond?.rangeEnd == null ? '' : String(timeCond.rangeEnd),
}
}
const phCond = cond as Partial | null | undefined
const options = getRequestRuleMatchOptions(source)
const mode = options.some((item) => item.value === phCond?.mode)
? (phCond?.mode as string)
: MATCH_EQ
return {
source,
path: phCond?.path || '',
mode,
value: phCond?.value == null ? '' : String(phCond.value),
}
}
// ---------------------------------------------------------------------------
// Editor: build expression strings
// ---------------------------------------------------------------------------
function buildExprLiteral(mode: string, value: string): string {
const text = String(value || '').trim()
if (mode === MATCH_CONTAINS) return JSON.stringify(text)
if (text === 'true' || text === 'false') return text
if (NUMERIC_LITERAL_REGEX.test(text)) return text
return JSON.stringify(text)
}
function buildTimeConditionExpr(cond: TimeCondition): string {
const normalized = normalizeCondition(cond) as TimeCondition
const { timeFunc, timezone, mode } = normalized
const tz = JSON.stringify(timezone)
const fn = `${timeFunc}(${tz})`
if (mode === MATCH_RANGE) {
const s = normalized.rangeStart.trim()
const e = normalized.rangeEnd.trim()
if (!NUMERIC_LITERAL_REGEX.test(s) || !NUMERIC_LITERAL_REGEX.test(e)) {
return ''
}
return `${fn} >= ${s} || ${fn} < ${e}`
}
const v = normalized.value.trim()
if (!NUMERIC_LITERAL_REGEX.test(v)) return ''
const opMap: Record = {
[MATCH_EQ]: '==',
[MATCH_GTE]: '>=',
[MATCH_LT]: '<',
}
return `${fn} ${opMap[mode] || '=='} ${v}`
}
function buildRequestConditionExpr(cond: RequestCondition): string {
if (cond.source === 'time') return buildTimeConditionExpr(cond)
const normalized = normalizeCondition(cond) as ParamHeaderCondition
const path = normalized.path.trim()
if (!path) return ''
const sourceExpr =
normalized.source === 'header'
? `header(${JSON.stringify(path)})`
: `param(${JSON.stringify(path)})`
switch (normalized.mode) {
case MATCH_EXISTS:
return normalized.source === 'header'
? `${sourceExpr} != ""`
: `${sourceExpr} != nil`
case MATCH_CONTAINS:
return normalized.source === 'header'
? `has(${sourceExpr}, ${buildExprLiteral(normalized.mode, normalized.value)})`
: `${sourceExpr} != nil && has(${sourceExpr}, ${buildExprLiteral(normalized.mode, normalized.value)})`
case MATCH_GT:
case MATCH_GTE:
case MATCH_LT:
case MATCH_LTE: {
const opMap: Record = {
[MATCH_GT]: '>',
[MATCH_GTE]: '>=',
[MATCH_LT]: '<',
[MATCH_LTE]: '<=',
}
const numText = String(normalized.value).trim()
if (!NUMERIC_LITERAL_REGEX.test(numText)) return ''
return `${sourceExpr} != nil && ${sourceExpr} ${opMap[normalized.mode]} ${numText}`
}
case MATCH_EQ:
default:
return `${sourceExpr} == ${buildExprLiteral(normalized.mode, normalized.value)}`
}
}
function buildRuleGroupFactor(group: RequestRuleGroup): string {
const multiplier = (group.multiplier || '').trim()
if (!NUMERIC_LITERAL_REGEX.test(multiplier)) return ''
const condExprs = (group.conditions || [])
.map(buildRequestConditionExpr)
.filter(Boolean)
if (condExprs.length === 0) return ''
const combined =
condExprs.length === 1
? condExprs[0]
: condExprs.map((e) => (e.includes(' || ') ? `(${e})` : e)).join(' && ')
return `(${combined} ? ${multiplier} : 1)`
}
export function buildRequestRuleExpr(groups: RequestRuleGroup[]): string {
return (groups || []).map(buildRuleGroupFactor).filter(Boolean).join(' * ')
}