Files
new-api/web/default/src/features/pricing/lib/dynamic-price.ts
T
t0ng7u d146e45e2f ⚖️ chore(web/default): add reusable copyright header tooling
Add a Bun script to apply and normalize AGPL copyright headers across the default frontend source files.

The script keeps headers idempotent, upgrades existing headers to the 2023-2026 QuantumNous range, and is exposed through `bun run copyright` for future maintenance.
2026-05-09 11:35:07 +08:00

190 lines
5.3 KiB
TypeScript
Vendored

/*
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 <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { formatBillingCurrencyFromUSD } from '@/lib/currency'
import { TOKEN_UNIT_DIVISORS } from '../constants'
import type { PricingModel, TokenUnit } from '../types'
import {
BILLING_PRICING_VARS,
parseTiersFromExpr,
splitBillingExprAndRequestRules,
tryParseRequestRuleExpr,
type BillingVar,
type ParsedTier,
} from './billing-expr'
type DynamicPriceOptions = {
tokenUnit: TokenUnit
showRechargePrice?: boolean
priceRate?: number
usdExchangeRate?: number
groupRatioMultiplier?: number
}
export type DynamicPriceEntry = {
key: string
field: string
label: string
shortLabel: string
value: number
formatted: string
variable: BillingVar
}
export type DynamicPricingSummary = {
tiers: ParsedTier[]
tier: ParsedTier | null
tierCount: number
hasRequestRules: boolean
isSpecialExpression: boolean
rawExpression: string
entries: DynamicPriceEntry[]
primaryEntries: DynamicPriceEntry[]
secondaryEntries: DynamicPriceEntry[]
}
const PRIMARY_DYNAMIC_FIELDS = new Set(['inputPrice', 'outputPrice'])
export function isDynamicPricingModel(model: PricingModel): boolean {
return model.billing_mode === 'tiered_expr' && Boolean(model.billing_expr)
}
export function getDynamicDisplayGroupRatio(model: PricingModel): number {
const groups = Array.isArray(model.enable_groups) ? model.enable_groups : []
const ratios = model.group_ratio || {}
if (groups.length === 0) return 1
let minRatio = Number.POSITIVE_INFINITY
for (const group of groups) {
const ratio = ratios[group]
if (ratio !== undefined && ratio < minRatio) {
minRatio = ratio
}
}
return minRatio === Number.POSITIVE_INFINITY ? 1 : minRatio
}
function applyRechargeRate(
price: number,
showWithRecharge: boolean,
priceRate: number,
usdExchangeRate: number
): number {
if (!showWithRecharge) return price
return (price * priceRate) / usdExchangeRate
}
export function formatDynamicUnitPrice(
valuePerMillionTokens: number,
options: DynamicPriceOptions
): string {
const groupRatio = options.groupRatioMultiplier ?? 1
const priceRate = options.priceRate ?? 1
const usdExchangeRate = options.usdExchangeRate ?? 1
const priceUSD =
(valuePerMillionTokens * groupRatio) /
TOKEN_UNIT_DIVISORS[options.tokenUnit]
const displayPrice = applyRechargeRate(
priceUSD,
options.showRechargePrice ?? false,
priceRate,
usdExchangeRate
)
return formatBillingCurrencyFromUSD(displayPrice, {
digitsLarge: 4,
digitsSmall: 6,
abbreviate: false,
})
}
export function getDynamicPricingTiers(model: PricingModel): ParsedTier[] {
if (!isDynamicPricingModel(model)) return []
const { billingExpr } = splitBillingExprAndRequestRules(
model.billing_expr || ''
)
return parseTiersFromExpr(billingExpr)
}
export function hasDynamicRequestRules(model: PricingModel): boolean {
if (!isDynamicPricingModel(model)) return false
const { requestRuleExpr } = splitBillingExprAndRequestRules(
model.billing_expr || ''
)
return Boolean(tryParseRequestRuleExpr(requestRuleExpr || '')?.length)
}
export function getDynamicPriceEntries(
tier: ParsedTier | null,
options: DynamicPriceOptions
): DynamicPriceEntry[] {
if (!tier) return []
return BILLING_PRICING_VARS.flatMap((variable) => {
if (!variable.field) return []
const value = Number(tier[variable.field])
if (!Number.isFinite(value) || value <= 0) return []
return [
{
key: variable.key,
field: variable.field,
label: variable.label,
shortLabel: variable.shortLabel,
value,
formatted: formatDynamicUnitPrice(value, options),
variable,
},
]
}).sort((a, b) => {
const aPrimary = PRIMARY_DYNAMIC_FIELDS.has(a.field)
const bPrimary = PRIMARY_DYNAMIC_FIELDS.has(b.field)
if (aPrimary !== bPrimary) return aPrimary ? -1 : 1
return 0
})
}
export function getDynamicPricingSummary(
model: PricingModel,
options: DynamicPriceOptions
): DynamicPricingSummary | null {
if (!isDynamicPricingModel(model)) return null
const tiers = getDynamicPricingTiers(model)
const tier = tiers[0] || null
const entries = getDynamicPriceEntries(tier, options)
const rawExpression = model.billing_expr || ''
return {
tiers,
tier,
tierCount: tiers.length,
hasRequestRules: hasDynamicRequestRules(model),
isSpecialExpression: rawExpression.trim().length > 0 && tiers.length === 0,
rawExpression,
entries,
primaryEntries: entries.filter((entry) =>
PRIMARY_DYNAMIC_FIELDS.has(entry.field)
),
secondaryEntries: entries.filter(
(entry) => !PRIMARY_DYNAMIC_FIELDS.has(entry.field)
),
}
}