feat: enhance tiered billing logic and improve variable handling in pricing calculations

This commit is contained in:
CaIon
2026-03-16 22:00:36 +08:00
parent f6c0852da9
commit 5b03b39db2
10 changed files with 431 additions and 240 deletions
+71 -87
View File
@@ -2210,24 +2210,47 @@ export function renderLogContent(opts) {
}
}
function parseTiersFromExpr(exprStr) {
const TIER_VAR_KEYS = ['p', 'c', 'cr', 'cc', 'cc1h', 'img', 'ai', 'ao'];
const TIER_VAR_TO_FIELD = {
p: 'inputPrice', c: 'outputPrice',
cr: 'cacheReadPrice', cc: 'cacheCreatePrice', cc1h: 'cacheCreate1hPrice',
img: 'imagePrice', ai: 'audioInputPrice', ao: 'audioOutputPrice',
};
function parseTierBody(bodyStr) {
const coeffs = {};
const re = new RegExp(`\\b(${TIER_VAR_KEYS.join('|')})\\s*\\*\\s*([\\d.eE+-]+)`, 'g');
let m;
while ((m = re.exec(bodyStr)) !== null) {
if (!(m[1] in coeffs)) coeffs[m[1]] = Number(m[2]);
}
const tier = {};
for (const [varName, field] of Object.entries(TIER_VAR_TO_FIELD)) {
tier[field] = coeffs[varName] || 0;
}
return tier;
}
export function parseTiersFromExpr(exprStr) {
if (!exprStr) return [];
try {
const cacheVars = ['cr', 'cc', 'cc1h'];
const optCache = cacheVars.map((v) => `(?:\\s*\\+\\s*${v}\\s*\\*\\s*([\\d.eE+-]+))?`).join('');
const bodyPat = `p\\s*\\*\\s*([\\d.eE+-]+)\\s*\\+\\s*c\\s*\\*\\s*([\\d.eE+-]+)${optCache}`;
const tierRe = new RegExp(`tier\\("([^"]*)",\\s*${bodyPat}\\)`, 'g');
const condGroup = `((?:(?:p|c)\\s*(?:<|<=|>|>=)\\s*[\\d.eE+]+)(?:\\s*&&\\s*(?:p|c)\\s*(?:<|<=|>|>=)\\s*[\\d.eE+]+)*)`;
const tierRe = new RegExp(`(?:${condGroup}\\s*\\?\\s*)?tier\\("([^"]*)",\\s*([^)]+)\\)`, 'g');
const tiers = [];
let m;
while ((m = tierRe.exec(exprStr)) !== null) {
tiers.push({
label: m[1],
inputPrice: Number(m[2]),
outputPrice: Number(m[3]),
cacheReadPrice: m[4] ? Number(m[4]) : 0,
cacheCreatePrice: m[5] ? Number(m[5]) : 0,
cacheCreate1hPrice: m[6] ? Number(m[6]) : 0,
});
const condStr = m[1] || '';
const conditions = [];
if (condStr) {
for (const cp of condStr.split(/\s*&&\s*/)) {
const cm = cp.trim().match(/^(p|c)\s*(<|<=|>|>=)\s*([\d.eE+]+)$/);
if (cm) conditions.push({ var: cm[1], op: cm[2], value: Number(cm[3]) });
}
}
const tier = parseTierBody(m[3]);
tier.label = m[2];
tier.conditions = conditions;
tiers.push(tier);
}
return tiers;
} catch {
@@ -2258,45 +2281,24 @@ export function renderTieredModelPrice(opts) {
const { symbol, rate } = getCurrencyConfig();
const gr = groupRatio || 1;
const inputCost = (inputTokens / 1000000) * tier.inputPrice;
const outputCost = (completionTokens / 1000000) * tier.outputPrice;
const cacheReadCost = (cacheTokens / 1000000) * tier.cacheReadPrice;
const hasSplitCacheCreation = cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0;
let cacheCreateCost = 0;
if (hasSplitCacheCreation) {
cacheCreateCost = (cacheCreationTokens5m / 1000000) * tier.cacheCreatePrice
+ (cacheCreationTokens1h / 1000000) * tier.cacheCreate1hPrice;
} else if (cacheCreationTokens > 0) {
cacheCreateCost = (cacheCreationTokens / 1000000) * tier.cacheCreatePrice;
}
const totalBeforeGroup = inputCost + outputCost + cacheReadCost + cacheCreateCost;
const total = totalBeforeGroup * gr;
const priceLines = [
['inputPrice', '输入价格'],
['outputPrice', '补全价格'],
['cacheReadPrice', '缓存读取价格'],
['cacheCreatePrice', '缓存创建价格'],
['cacheCreate1hPrice', '1h缓存创建价格'],
['imagePrice', '图片输入价格'],
['audioInputPrice', '音频输入价格'],
['audioOutputPrice', '音频输出价格'],
];
const lines = [
buildBillingText('命中档位:{{tier}}', { tier: matchedTier || tier.label }),
buildBillingPriceText('输入价格:{{symbol}}{{price}} / 1M tokens', { symbol, usdAmount: tier.inputPrice, rate }),
buildBillingPriceText('输出价格:{{symbol}}{{price}} / 1M tokens', { symbol, usdAmount: tier.outputPrice, rate }),
cacheTokens > 0 && tier.cacheReadPrice > 0
? buildBillingPriceText('缓存读取价格{{symbol}}{{price}} / 1M tokens', { symbol, usdAmount: tier.cacheReadPrice, rate })
: null,
hasSplitCacheCreation && cacheCreationTokens5m > 0 && tier.cacheCreatePrice > 0
? buildBillingPriceText('5m缓存创建价格:{{symbol}}{{price}} / 1M tokens', { symbol, usdAmount: tier.cacheCreatePrice, rate })
: null,
hasSplitCacheCreation && cacheCreationTokens1h > 0 && tier.cacheCreate1hPrice > 0
? buildBillingPriceText('1h缓存创建价格:{{symbol}}{{price}} / 1M tokens', { symbol, usdAmount: tier.cacheCreate1hPrice, rate })
: null,
!hasSplitCacheCreation && cacheCreationTokens > 0 && tier.cacheCreatePrice > 0
? buildBillingPriceText('缓存创建价格:{{symbol}}{{price}} / 1M tokens', { symbol, usdAmount: tier.cacheCreatePrice, rate })
: null,
buildBillingText(
'(输入 {{input}} tokens / 1M tokens * {{symbol}}{{inputPrice}} + 输出 {{output}} tokens / 1M tokens * {{symbol}}{{outputPrice}}) * 分组倍率 {{ratio}} = {{symbol}}{{total}}',
{
input: inputTokens, output: completionTokens, symbol,
inputPrice: formatBillingDisplayPrice(tier.inputPrice, rate),
outputPrice: formatBillingDisplayPrice(tier.outputPrice, rate),
ratio: gr, total: formatBillingDisplayPrice(total, rate),
},
),
...priceLines
.filter(([field]) => tier[field] > 0)
.map(([field, label]) =>
buildBillingPriceText(`${label}{{symbol}}{{price}} / 1M tokens`, { symbol, usdAmount: tier[field], rate }),
),
];
return renderBillingArticle(lines);
@@ -2329,44 +2331,26 @@ export function renderTieredModelPriceSimple(opts) {
];
if (tier && isPriceDisplayMode(displayMode)) {
segments.push({
tone: 'secondary',
text: i18next.t('输入 {{price}} / 1M tokens', {
price: formatCompactDisplayPrice(tier.inputPrice),
}),
});
if (cacheTokens > 0 && tier.cacheReadPrice > 0) {
segments.push({
tone: 'secondary',
text: i18next.t('缓存读 {{price}} / 1M tokens', {
price: formatCompactDisplayPrice(tier.cacheReadPrice),
}),
});
}
const hasSplitCacheCreation = cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0;
if (hasSplitCacheCreation && cacheCreationTokens5m > 0 && tier.cacheCreatePrice > 0) {
segments.push({
tone: 'secondary',
text: i18next.t('5m缓存创建 {{price}} / 1M tokens', {
price: formatCompactDisplayPrice(tier.cacheCreatePrice),
}),
});
}
if (hasSplitCacheCreation && cacheCreationTokens1h > 0 && tier.cacheCreate1hPrice > 0) {
segments.push({
tone: 'secondary',
text: i18next.t('1h缓存创建 {{price}} / 1M tokens', {
price: formatCompactDisplayPrice(tier.cacheCreate1hPrice),
}),
});
}
if (!hasSplitCacheCreation && cacheCreationTokens > 0 && tier.cacheCreatePrice > 0) {
segments.push({
tone: 'secondary',
text: i18next.t('缓存创建 {{price}} / 1M tokens', {
price: formatCompactDisplayPrice(tier.cacheCreatePrice),
}),
});
const priceSegments = [
['inputPrice', '输入'],
['outputPrice', '补全'],
['cacheReadPrice', '缓存读'],
['cacheCreatePrice', '缓存创建'],
['cacheCreate1hPrice', '1h缓存创建'],
['imagePrice', '图片输入'],
['audioInputPrice', '音频输入'],
['audioOutputPrice', '音频输出'],
];
for (const [field, label] of priceSegments) {
if (tier[field] > 0) {
segments.push({
tone: 'secondary',
text: i18next.t('{{label}} {{price}} / 1M tokens', {
label: i18next.t(label),
price: formatCompactDisplayPrice(tier[field]),
}),
});
}
}
}
+27 -19
View File
@@ -904,9 +904,24 @@ export const formatDynamicPriceSummary = (billingExpr, t, groupRatio = 1) => {
const tierMatches = billingExpr.match(/tier\(/g) || [];
const tierCount = tierMatches.length;
const firstTierMatch = billingExpr.match(
/tier\("[^"]*",\s*p\s*\*\s*([\d.eE+-]+)\s*\+\s*c\s*\*\s*([\d.eE+-]+)(?:\s*\+\s*cr\s*\*\s*([\d.eE+-]+))?(?:\s*\+\s*cc\s*\*\s*([\d.eE+-]+))?/,
);
const varCoeffs = {};
const varRe = /\b(p|c|cr|cc|cc1h|img|ai|ao)\s*\*\s*([\d.eE+-]+)/g;
let vm;
while ((vm = varRe.exec(billingExpr)) !== null) {
if (!(vm[1] in varCoeffs)) varCoeffs[vm[1]] = Number(vm[2]);
}
const hasCoeffs = 'p' in varCoeffs || 'c' in varCoeffs;
const varLabels = [
['p', '输入价格'],
['c', '补全价格'],
['cr', '缓存读取价格'],
['cc', '缓存创建价格'],
['cc1h', '1h缓存创建价格'],
['img', '图片输入价格'],
['ai', '音频输入价格'],
['ao', '音频输出价格'],
];
const hasTimeCondition = /\b(?:hour|weekday|month|day)\(/.test(billingExpr);
const hasRequestCondition = /\b(?:param|header)\(/.test(billingExpr);
@@ -921,26 +936,18 @@ export const formatDynamicPriceSummary = (billingExpr, t, groupRatio = 1) => {
return (
<>
{firstTierMatch && (
{hasCoeffs && (
<>
<span style={lineStyle}>
{t('输入价格')} ${(Number(firstTierMatch[1]) * gr).toFixed(4)}{unitSuffix}
</span>
<span style={lineStyle}>
{t('输出价格')} ${(Number(firstTierMatch[2]) * gr).toFixed(4)}{unitSuffix}
</span>
{firstTierMatch[3] && (
<span style={lineStyle}>
{t('缓存读取价格')} ${(Number(firstTierMatch[3]) * gr).toFixed(4)}{unitSuffix}
</span>
)}
{firstTierMatch[4] && (
<span style={lineStyle}>
{t('缓存创建价格')} ${(Number(firstTierMatch[4]) * gr).toFixed(4)}{unitSuffix}
</span>
{varLabels.map(([key, label]) =>
key in varCoeffs ? (
<span key={key} style={lineStyle}>
{t(label)} ${(varCoeffs[key] * gr).toFixed(4)}{unitSuffix}
</span>
) : null,
)}
</>
)}
{(tierCount > 1 || hasTimeCondition || hasRequestCondition) && (
<span style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
<span
style={{
@@ -970,6 +977,7 @@ export const formatDynamicPriceSummary = (billingExpr, t, groupRatio = 1) => {
</span>
))}
</span>
)}
</>
);
};