fix(web): 修复阶梯计费 Base64 解码失败与标签不匹配导致的显示错误 (#4530)
* fix(web): 修复阶梯计费表达式解析与匹配逻辑 - 优化 Base64 解码逻辑:引入 UTF-8 感知的解码方法(使用 TextDecoder/Uint8Array),替换原有的简单 `atob`,修复包含非拉丁字符时解码失败的问题。 - 增强阶梯标签匹配机制:新增标签规范化处理(移除空格、统一大小写、转换 `<`/`≤`/`<=` 等符号),确保日志记录中的标签能够与配置中的标签准确匹配。 - 将上述修复同步应用于 default 和 classic 两套前端主题。 * refactor(web): 完善 Base64 解码函数的类型声明 - 根据 CodeRabbitAI 的代码审查建议,将 `decodeBillingExprB64` 方法中 `Array.prototype.map` 回调函数的参数类型由 `any` 替换为更精确的 `number`。 - 提高了代码的类型安全性与可读性。 * fix(web): 修复动态价格明细表中阶梯高亮未能正确匹配的问题 - 在 default 主题的 `DynamicPricingBreakdown` 组件中,引入 `normalizeTierLabel` 函数。 - 替换原有对 `matchedTierLabel` 的严格相等判定,确保在包含全半角符号(如 `≤`/`<=`)或存在空格等格式不一致的场景下,日志详情中的表格依然能准确高亮(Matched)当前命中的对应计费阶梯。 * refactor(web): 移除阶梯计费标签不匹配时的强制兜底逻辑 - 在 default 和 classic 主题中,修改 `resolveMatchedTier` 和相关的阶梯匹配方法,当日志中 `matched_tier` 无法与表达式中的阶梯标签严格对应时,直接返回 `null` 而不再默认退化展示第一阶梯(`tiers[0]`)的价格。 - 遵循“数据准确性优先”的计费展示准则,防止因匹配失败而向用户展示猜测出的单价,避免产生账单误导及客诉风险。 - 在 Classic 主题账单卡片中,对于无法匹配的异常账单明确展示“未匹配到对应阶梯”的提示。 * fix(web): 修复阶梯计费标签正则匹配的短路问题 - 根据 CodeRabbitAI 的代码审查反馈,修正了 `normalizeLabel`(以及 `normalizeTierLabel`)函数中的正则表达式分支顺序。 - 将原本的 `/<|≤|<=/` 调整为 `/<=|≤|</`,以修复 JavaScript 正则引擎从左到右匹配时,会将 `<=` 中的 `<` 优先短路匹配,导致残留 `=` 号的问题。 - 确保了双字符操作符(如 `<=`、`>=`)现在能够被正确完整地替换为单字符(`<`、`>`),保证了计费阶梯日志匹配的准确性。 * fix(web): 完善阶梯计费未匹配展示 --------- Co-authored-by: CaIon <i@caion.me>
This commit is contained in:
Vendored
+57
-5
@@ -2270,6 +2270,37 @@ export function parseTiersFromExpr(exprStr) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const decodeFromBase64 = (base64) => {
|
||||||
|
if (!base64) return '';
|
||||||
|
|
||||||
|
const binaryString =
|
||||||
|
typeof window !== 'undefined' ? window.atob(base64) : Buffer.from(base64, 'base64').toString('binary');
|
||||||
|
const bytes = new Uint8Array(binaryString.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < binaryString.length; i++) {
|
||||||
|
bytes[i] = binaryString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof TextDecoder !== 'undefined') {
|
||||||
|
return new TextDecoder().decode(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return decodeURIComponent(
|
||||||
|
Array.prototype.map
|
||||||
|
.call(bytes, (byte) => '%' + byte.toString(16).padStart(2, '0'))
|
||||||
|
.join(''),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizeLabel = (label) => {
|
||||||
|
if (!label) return '';
|
||||||
|
return label
|
||||||
|
.replace(/<[==]?|≤|<[==]?/g, '<')
|
||||||
|
.replace(/>[==]?|≥|>[==]?/g, '>')
|
||||||
|
.replace(/\s+/g, '')
|
||||||
|
.toLowerCase();
|
||||||
|
};
|
||||||
|
|
||||||
export function renderTieredModelPrice(opts) {
|
export function renderTieredModelPrice(opts) {
|
||||||
const {
|
const {
|
||||||
prompt_tokens: inputTokens = 0,
|
prompt_tokens: inputTokens = 0,
|
||||||
@@ -2283,13 +2314,22 @@ export function renderTieredModelPrice(opts) {
|
|||||||
cache_creation_tokens_1h: cacheCreationTokens1h = 0,
|
cache_creation_tokens_1h: cacheCreationTokens1h = 0,
|
||||||
} = opts;
|
} = opts;
|
||||||
let exprStr = '';
|
let exprStr = '';
|
||||||
try { exprStr = atob(exprB64); } catch { /* ignore */ }
|
try { exprStr = decodeFromBase64(exprB64); } catch { /* ignore */ }
|
||||||
const tiers = parseTiersFromExpr(exprStr);
|
const tiers = parseTiersFromExpr(exprStr);
|
||||||
if (tiers.length === 0) {
|
if (tiers.length === 0) {
|
||||||
return i18next.t('阶梯计费(表达式解析失败)');
|
return i18next.t('阶梯计费(表达式解析失败)');
|
||||||
}
|
}
|
||||||
|
|
||||||
const tier = tiers.find((t) => t.label === matchedTier) || tiers[0];
|
const tier =
|
||||||
|
tiers.find((t) => {
|
||||||
|
const l1 = normalizeLabel(t.label);
|
||||||
|
const l2 = normalizeLabel(matchedTier);
|
||||||
|
return l1 === l2 && l1 !== '';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!tier) {
|
||||||
|
return i18next.t('阶梯计费(未匹配到对应阶梯)');
|
||||||
|
}
|
||||||
const { symbol, rate } = getCurrencyConfig();
|
const { symbol, rate } = getCurrencyConfig();
|
||||||
const gr = groupRatio || 1;
|
const gr = groupRatio || 1;
|
||||||
|
|
||||||
@@ -2326,9 +2366,14 @@ export function renderTieredModelPriceSimple(opts) {
|
|||||||
outputMode = 'segments',
|
outputMode = 'segments',
|
||||||
} = opts;
|
} = opts;
|
||||||
let exprStr = '';
|
let exprStr = '';
|
||||||
try { exprStr = atob(exprB64); } catch { /* ignore */ }
|
try { exprStr = decodeFromBase64(exprB64); } catch { /* ignore */ }
|
||||||
const tiers = parseTiersFromExpr(exprStr);
|
const tiers = parseTiersFromExpr(exprStr);
|
||||||
const tier = tiers.find((t) => t.label === matchedTier) || tiers[0];
|
const tier =
|
||||||
|
tiers.find((t) => {
|
||||||
|
const l1 = normalizeLabel(t.label);
|
||||||
|
const l2 = normalizeLabel(matchedTier);
|
||||||
|
return l1 === l2 && l1 !== '';
|
||||||
|
});
|
||||||
|
|
||||||
if (outputMode === 'segments') {
|
if (outputMode === 'segments') {
|
||||||
const segments = [
|
const segments = [
|
||||||
@@ -2338,7 +2383,14 @@ export function renderTieredModelPriceSimple(opts) {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (tier && isPriceDisplayMode(displayMode)) {
|
if (!tier) {
|
||||||
|
segments.push({
|
||||||
|
tone: 'secondary',
|
||||||
|
text: tiers.length === 0
|
||||||
|
? i18next.t('阶梯计费(表达式解析失败)')
|
||||||
|
: i18next.t('阶梯计费(未匹配到对应阶梯)'),
|
||||||
|
});
|
||||||
|
} else if (isPriceDisplayMode(displayMode)) {
|
||||||
const hasAnyCacheTokens = cacheTokens > 0 || cacheCreationTokens > 0
|
const hasAnyCacheTokens = cacheTokens > 0 || cacheCreationTokens > 0
|
||||||
|| cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0;
|
|| cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0;
|
||||||
const priceSegments = BILLING_PRICING_VARS
|
const priceSegments = BILLING_PRICING_VARS
|
||||||
|
|||||||
Vendored
+2
@@ -3689,6 +3689,8 @@
|
|||||||
"缓存创建-5分钟 (cc5)": "Cache Creation-5min (cc5)",
|
"缓存创建-5分钟 (cc5)": "Cache Creation-5min (cc5)",
|
||||||
"缓存创建-1小时 (cc1h)": "Cache Creation-1hour (cc1h)",
|
"缓存创建-1小时 (cc1h)": "Cache Creation-1hour (cc1h)",
|
||||||
"阶梯计费": "Tiered Billing",
|
"阶梯计费": "Tiered Billing",
|
||||||
|
"阶梯计费(表达式解析失败)": "Tiered Billing (expression parse failed)",
|
||||||
|
"阶梯计费(未匹配到对应阶梯)": "Tiered Billing (no matching tier)",
|
||||||
"输入 Tokens 阶梯": "Input Token Tiers",
|
"输入 Tokens 阶梯": "Input Token Tiers",
|
||||||
"输出 Tokens 阶梯": "Output Token Tiers",
|
"输出 Tokens 阶梯": "Output Token Tiers",
|
||||||
"固定阶梯": "Fixed Tier",
|
"固定阶梯": "Fixed Tier",
|
||||||
|
|||||||
Vendored
+3
-1
@@ -3642,6 +3642,8 @@
|
|||||||
"默认折叠侧边栏": "Réduire la barre latérale par défaut",
|
"默认折叠侧边栏": "Réduire la barre latérale par défaut",
|
||||||
"默认测试模型": "Modèle de test par défaut",
|
"默认测试模型": "Modèle de test par défaut",
|
||||||
"默认用户消息": "Bonjour",
|
"默认用户消息": "Bonjour",
|
||||||
"默认补全倍率": "Taux de complétion par défaut"
|
"默认补全倍率": "Taux de complétion par défaut",
|
||||||
|
"阶梯计费(表达式解析失败)": "Facturation par paliers (échec de l'analyse de l'expression)",
|
||||||
|
"阶梯计费(未匹配到对应阶梯)": "Facturation par paliers (aucun palier correspondant)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+3
-1
@@ -3611,6 +3611,8 @@
|
|||||||
"默认折叠侧边栏": "サイドバーをデフォルトで折りたたむ",
|
"默认折叠侧边栏": "サイドバーをデフォルトで折りたたむ",
|
||||||
"默认测试模型": "デフォルトテストモデル",
|
"默认测试模型": "デフォルトテストモデル",
|
||||||
"默认用户消息": "こんにちは",
|
"默认用户消息": "こんにちは",
|
||||||
"默认补全倍率": "デフォルト補完倍率"
|
"默认补全倍率": "デフォルト補完倍率",
|
||||||
|
"阶梯计费(表达式解析失败)": "段階課金(式の解析に失敗)",
|
||||||
|
"阶梯计费(未匹配到对应阶梯)": "段階課金(一致する階層なし)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+3
-1
@@ -3662,6 +3662,8 @@
|
|||||||
"默认折叠侧边栏": "Сворачивать боковую панель по умолчанию",
|
"默认折叠侧边栏": "Сворачивать боковую панель по умолчанию",
|
||||||
"默认测试模型": "Модель для тестирования по умолчанию",
|
"默认测试模型": "Модель для тестирования по умолчанию",
|
||||||
"默认用户消息": "Здравствуйте",
|
"默认用户消息": "Здравствуйте",
|
||||||
"默认补全倍率": "Коэффициент завершения по умолчанию"
|
"默认补全倍率": "Коэффициент завершения по умолчанию",
|
||||||
|
"阶梯计费(表达式解析失败)": "Многоуровневая тарификация (ошибка разбора выражения)",
|
||||||
|
"阶梯计费(未匹配到对应阶梯)": "Многоуровневая тарификация (подходящий уровень не найден)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+3
-1
@@ -4176,6 +4176,8 @@
|
|||||||
"默认折叠侧边栏": "Mặc định thu gọn thanh bên",
|
"默认折叠侧边栏": "Mặc định thu gọn thanh bên",
|
||||||
"默认测试模型": "Mô hình kiểm tra mặc định",
|
"默认测试模型": "Mô hình kiểm tra mặc định",
|
||||||
"默认用户消息": "Xin chào",
|
"默认用户消息": "Xin chào",
|
||||||
"默认补全倍率": "Tỷ lệ hoàn thành mặc định"
|
"默认补全倍率": "Tỷ lệ hoàn thành mặc định",
|
||||||
|
"阶梯计费(表达式解析失败)": "Thanh toán theo bậc (không phân tích được biểu thức)",
|
||||||
|
"阶梯计费(未匹配到对应阶梯)": "Thanh toán theo bậc (không tìm thấy bậc phù hợp)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -3676,6 +3676,8 @@
|
|||||||
"缓存创建-5分钟 (cc5)": "缓存创建-5分钟 (cc5)",
|
"缓存创建-5分钟 (cc5)": "缓存创建-5分钟 (cc5)",
|
||||||
"缓存创建-1小时 (cc1h)": "缓存创建-1小时 (cc1h)",
|
"缓存创建-1小时 (cc1h)": "缓存创建-1小时 (cc1h)",
|
||||||
"阶梯计费": "阶梯计费",
|
"阶梯计费": "阶梯计费",
|
||||||
|
"阶梯计费(表达式解析失败)": "阶梯计费(表达式解析失败)",
|
||||||
|
"阶梯计费(未匹配到对应阶梯)": "阶梯计费(未匹配到对应阶梯)",
|
||||||
"输入 Tokens 阶梯": "输入 Tokens 阶梯",
|
"输入 Tokens 阶梯": "输入 Tokens 阶梯",
|
||||||
"输出 Tokens 阶梯": "输出 Tokens 阶梯",
|
"输出 Tokens 阶梯": "输出 Tokens 阶梯",
|
||||||
"固定阶梯": "固定阶梯",
|
"固定阶梯": "固定阶梯",
|
||||||
|
|||||||
+3
-1
@@ -3635,6 +3635,8 @@
|
|||||||
"默认折叠侧边栏": "預設摺疊側邊欄",
|
"默认折叠侧边栏": "預設摺疊側邊欄",
|
||||||
"默认测试模型": "預設測試模型",
|
"默认测试模型": "預設測試模型",
|
||||||
"默认用户消息": "你好",
|
"默认用户消息": "你好",
|
||||||
"默认补全倍率": "預設補全倍率"
|
"默认补全倍率": "預設補全倍率",
|
||||||
|
"阶梯计费(表达式解析失败)": "階梯計費(表達式解析失敗)",
|
||||||
|
"阶梯计费(未匹配到对应阶梯)": "階梯計費(未匹配到對應階梯)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+3
-1
@@ -2588,6 +2588,8 @@
|
|||||||
"关闭后将不再显示此提示(仅对当前浏览器生效)。确定要关闭吗?": "关闭后将不再显示此提示(仅对当前浏览器生效)。确定要关闭吗?",
|
"关闭后将不再显示此提示(仅对当前浏览器生效)。确定要关闭吗?": "关闭后将不再显示此提示(仅对当前浏览器生效)。确定要关闭吗?",
|
||||||
"关闭提示": "关闭提示",
|
"关闭提示": "关闭提示",
|
||||||
"说明:本页测试为非流式请求;若渠道仅支持流式返回,可能出现测试失败,请以实际使用为准。": "说明:本页测试为非流式请求;若渠道仅支持流式返回,可能出现测试失败,请以实际使用为准。",
|
"说明:本页测试为非流式请求;若渠道仅支持流式返回,可能出现测试失败,请以实际使用为准。": "说明:本页测试为非流式请求;若渠道仅支持流式返回,可能出现测试失败,请以实际使用为准。",
|
||||||
"提示:端点映射仅用于模型广场展示,不会影响模型真实调用。如需配置真实调用,请前往「渠道管理」。": "提示:端点映射仅用于模型广场展示,不会影响模型真实调用。如需配置真实调用,请前往「渠道管理」。"
|
"提示:端点映射仅用于模型广场展示,不会影响模型真实调用。如需配置真实调用,请前往「渠道管理」。": "提示:端点映射仅用于模型广场展示,不会影响模型真实调用。如需配置真实调用,请前往「渠道管理」。",
|
||||||
|
"阶梯计费(表达式解析失败)": "阶梯计费(表达式解析失败)",
|
||||||
|
"阶梯计费(未匹配到对应阶梯)": "阶梯计费(未匹配到对应阶梯)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
MATCH_LT,
|
MATCH_LT,
|
||||||
MATCH_RANGE,
|
MATCH_RANGE,
|
||||||
SOURCE_TIME,
|
SOURCE_TIME,
|
||||||
|
normalizeTierLabel,
|
||||||
parseTiersFromExpr,
|
parseTiersFromExpr,
|
||||||
splitBillingExprAndRequestRules,
|
splitBillingExprAndRequestRules,
|
||||||
tryParseRequestRuleExpr,
|
tryParseRequestRuleExpr,
|
||||||
@@ -168,6 +169,9 @@ export function DynamicPricingBreakdown({
|
|||||||
|
|
||||||
const hasTiers = tiers.length > 0
|
const hasTiers = tiers.length > 0
|
||||||
const hasRules = ruleGroups.length > 0
|
const hasRules = ruleGroups.length > 0
|
||||||
|
const normalizedMatchedTierLabel = normalizeTierLabel(
|
||||||
|
matchedTierLabel ?? undefined
|
||||||
|
)
|
||||||
|
|
||||||
if (!expr) return null
|
if (!expr) return null
|
||||||
|
|
||||||
@@ -307,9 +311,9 @@ export function DynamicPricingBreakdown({
|
|||||||
{tiers.map((tier, i) => {
|
{tiers.map((tier, i) => {
|
||||||
const condSummary = formatConditionSummary(tier.conditions, t)
|
const condSummary = formatConditionSummary(tier.conditions, t)
|
||||||
const isMatched =
|
const isMatched =
|
||||||
matchedTierLabel != null &&
|
normalizedMatchedTierLabel !== '' &&
|
||||||
matchedTierLabel !== '' &&
|
normalizeTierLabel(tier.label) ===
|
||||||
tier.label === matchedTierLabel
|
normalizedMatchedTierLabel
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={`tier-${i}`}
|
key={`tier-${i}`}
|
||||||
|
|||||||
@@ -286,6 +286,15 @@ export function parseTiersFromExpr(exprStr: string): ParsedTier[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function normalizeTierLabel(label: string | undefined): string {
|
||||||
|
if (!label) return ''
|
||||||
|
return label
|
||||||
|
.replace(/<[==]?|≤|<[==]?/g, '<')
|
||||||
|
.replace(/>[==]?|≥|>[==]?/g, '>')
|
||||||
|
.replace(/\s+/g, '')
|
||||||
|
.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Request rule parser
|
// Request rule parser
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
+8
@@ -115,7 +115,9 @@ function buildDetailSegments(
|
|||||||
const text = prices.join(' / ')
|
const text = prices.join(' / ')
|
||||||
return showUnit ? `${text}/M` : text
|
return showUnit ? `${text}/M` : text
|
||||||
}
|
}
|
||||||
|
const isTieredExpr = other.billing_mode === 'tiered_expr'
|
||||||
const tieredSummary = getTieredBillingSummary(other)
|
const tieredSummary = getTieredBillingSummary(other)
|
||||||
|
if (isTieredExpr) {
|
||||||
if (tieredSummary) {
|
if (tieredSummary) {
|
||||||
const baseEntries = tieredSummary.priceEntries
|
const baseEntries = tieredSummary.priceEntries
|
||||||
.filter((entry) => ['inputPrice', 'outputPrice'].includes(entry.field))
|
.filter((entry) => ['inputPrice', 'outputPrice'].includes(entry.field))
|
||||||
@@ -163,6 +165,12 @@ function buildDetailSegments(
|
|||||||
muted: true,
|
muted: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
segments.push({
|
||||||
|
text: `${t('Dynamic Pricing')} · ${t('No matching results')}`,
|
||||||
|
muted: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const isPerCall = isPerCallBilling(other.model_price)
|
const isPerCall = isPerCallBilling(other.model_price)
|
||||||
if (isPerCall) {
|
if (isPerCall) {
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ function BillingBreakdown(props: {
|
|||||||
const { log, other, isAdmin } = props
|
const { log, other, isAdmin } = props
|
||||||
const isPerCall = isPerCallBilling(other.model_price)
|
const isPerCall = isPerCallBilling(other.model_price)
|
||||||
const isClaude = other.claude === true
|
const isClaude = other.claude === true
|
||||||
|
const isTieredExpr = other.billing_mode === 'tiered_expr'
|
||||||
const tieredSummary = getTieredBillingSummary(other)
|
const tieredSummary = getTieredBillingSummary(other)
|
||||||
|
|
||||||
const rows: Array<{ label: string; value: string }> = []
|
const rows: Array<{ label: string; value: string }> = []
|
||||||
@@ -133,11 +134,12 @@ function BillingBreakdown(props: {
|
|||||||
const fmtPrice = (usd: number) => formatBillingCurrencyFromUSD(usd, priceOpts)
|
const fmtPrice = (usd: number) => formatBillingCurrencyFromUSD(usd, priceOpts)
|
||||||
const baseInputUSD = other.model_ratio != null ? other.model_ratio * 2.0 : 0
|
const baseInputUSD = other.model_ratio != null ? other.model_ratio * 2.0 : 0
|
||||||
|
|
||||||
if (tieredSummary) {
|
if (isTieredExpr) {
|
||||||
rows.push({
|
rows.push({
|
||||||
label: t('Billing Mode'),
|
label: t('Billing Mode'),
|
||||||
value: t('Dynamic Pricing'),
|
value: t('Dynamic Pricing'),
|
||||||
})
|
})
|
||||||
|
if (tieredSummary) {
|
||||||
if (tieredSummary.tier.label) {
|
if (tieredSummary.tier.label) {
|
||||||
rows.push({
|
rows.push({
|
||||||
label: t('Matched Tier'),
|
label: t('Matched Tier'),
|
||||||
@@ -150,6 +152,12 @@ function BillingBreakdown(props: {
|
|||||||
value: `${fmtPrice(entry.price)}/M`,
|
value: `${fmtPrice(entry.price)}/M`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
rows.push({
|
||||||
|
label: t('Matched Tier'),
|
||||||
|
value: t('No matching results'),
|
||||||
|
})
|
||||||
|
}
|
||||||
} else if (isPerCall) {
|
} else if (isPerCall) {
|
||||||
rows.push({ label: t('Billing Mode'), value: t('Per-call') })
|
rows.push({ label: t('Billing Mode'), value: t('Per-call') })
|
||||||
if (other.model_price != null) {
|
if (other.model_price != null) {
|
||||||
@@ -184,7 +192,7 @@ function BillingBreakdown(props: {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tieredSummary && isClaude && hasAnyCacheTokens(other)) {
|
if (!isTieredExpr && isClaude && hasAnyCacheTokens(other)) {
|
||||||
if (other.cache_ratio != null && other.cache_ratio !== 1) {
|
if (other.cache_ratio != null && other.cache_ratio !== 1) {
|
||||||
rows.push({
|
rows.push({
|
||||||
label: t('Cache Read'),
|
label: t('Cache Read'),
|
||||||
@@ -220,7 +228,7 @@ function BillingBreakdown(props: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!tieredSummary) {
|
if (!isTieredExpr) {
|
||||||
if (other.audio_ratio != null && other.audio_ratio !== 1) {
|
if (other.audio_ratio != null && other.audio_ratio !== 1) {
|
||||||
rows.push({
|
rows.push({
|
||||||
label: t('Audio input'),
|
label: t('Audio input'),
|
||||||
|
|||||||
+31
-8
@@ -1,12 +1,15 @@
|
|||||||
import type { StatusBadgeProps } from '@/components/status-badge'
|
import type { StatusBadgeProps } from '@/components/status-badge'
|
||||||
import {
|
import {
|
||||||
BILLING_PRICING_VARS,
|
BILLING_PRICING_VARS,
|
||||||
|
normalizeTierLabel,
|
||||||
parseTiersFromExpr,
|
parseTiersFromExpr,
|
||||||
type ParsedTier,
|
type ParsedTier,
|
||||||
} from '@/features/pricing/lib/billing-expr'
|
} from '@/features/pricing/lib/billing-expr'
|
||||||
import type { UsageLog } from '../data/schema'
|
import type { UsageLog } from '../data/schema'
|
||||||
import type { LogOtherData } from '../types'
|
import type { LogOtherData } from '../types'
|
||||||
|
|
||||||
|
export { normalizeTierLabel }
|
||||||
|
|
||||||
const PARAM_OVERRIDE_ACTION_MAP: Record<string, string> = {
|
const PARAM_OVERRIDE_ACTION_MAP: Record<string, string> = {
|
||||||
set: 'Set',
|
set: 'Set',
|
||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
@@ -157,7 +160,25 @@ export function formatModelName(log: UsageLog): {
|
|||||||
export function decodeBillingExprB64(exprB64: string | undefined): string {
|
export function decodeBillingExprB64(exprB64: string | undefined): string {
|
||||||
if (!exprB64) return ''
|
if (!exprB64) return ''
|
||||||
try {
|
try {
|
||||||
return atob(exprB64)
|
const binaryString =
|
||||||
|
typeof window !== 'undefined'
|
||||||
|
? window.atob(exprB64)
|
||||||
|
: Buffer.from(exprB64, 'base64').toString('binary')
|
||||||
|
const bytes = new Uint8Array(binaryString.length)
|
||||||
|
|
||||||
|
for (let i = 0; i < binaryString.length; i++) {
|
||||||
|
bytes[i] = binaryString.charCodeAt(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof TextDecoder !== 'undefined') {
|
||||||
|
return new TextDecoder().decode(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return decodeURIComponent(
|
||||||
|
Array.prototype.map
|
||||||
|
.call(bytes, (byte: number) => '%' + byte.toString(16).padStart(2, '0'))
|
||||||
|
.join('')
|
||||||
|
)
|
||||||
} catch {
|
} catch {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@@ -165,19 +186,21 @@ export function decodeBillingExprB64(exprB64: string | undefined): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve which parsed tier corresponds to the matched_tier label in a log
|
* Resolve which parsed tier corresponds to the matched_tier label in a log
|
||||||
* entry. Falls back to the first tier when the label is missing or unknown,
|
* entry. Missing or unknown labels do not fall back to another tier because
|
||||||
* which mirrors the behaviour of the classic frontend renderer.
|
* that would display guessed unit prices.
|
||||||
*/
|
*/
|
||||||
export function resolveMatchedTier(
|
export function resolveMatchedTier(
|
||||||
tiers: ParsedTier[],
|
tiers: ParsedTier[],
|
||||||
matchedLabel: string | undefined
|
matchedLabel: string | undefined
|
||||||
): ParsedTier | null {
|
): ParsedTier | null {
|
||||||
if (tiers.length === 0) return null
|
if (tiers.length === 0) return null
|
||||||
if (matchedLabel) {
|
if (!matchedLabel) return null
|
||||||
const found = tiers.find((tier) => tier.label === matchedLabel)
|
const found = tiers.find((tier) => {
|
||||||
if (found) return found
|
const l1 = normalizeTierLabel(tier.label)
|
||||||
}
|
const l2 = normalizeTierLabel(matchedLabel)
|
||||||
return tiers[0]
|
return l1 === l2 && l1 !== ''
|
||||||
|
})
|
||||||
|
return found || null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user