From e46bcf02489969e1b6de79bd163b619801a8a05b Mon Sep 17 00:00:00 2001 From: Jeff Gu Date: Tue, 14 Apr 2026 13:45:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(pricing):=20=E5=A2=9E=E5=8A=A0=E4=BC=B0?= =?UTF-8?q?=E7=AE=97=E5=AE=9A=E4=BB=B7=E6=88=90=E6=9C=AC=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=8F=8A=E7=9B=B8=E5=85=B3=E6=A0=BC=E5=BC=8F=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/Pricing.tsx | 50 ++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/frontend/src/pages/Pricing.tsx b/frontend/src/pages/Pricing.tsx index 04cedaf..e1eef31 100644 --- a/frontend/src/pages/Pricing.tsx +++ b/frontend/src/pages/Pricing.tsx @@ -47,6 +47,8 @@ type TokenRef = { const PRICING_DECIMALS = 6; const PRICING_INPUT_STEP = "0.000001"; const AUTO_SAVE_DELAY_MS = 800; +const VOLUME_ESTIMATE_INPUT_WEIGHT = 3; +const VOLUME_ESTIMATE_OUTPUT_WEIGHT = 1; type SaveSource = "auto" | "manual"; type SaveState = "idle" | "saving" | "saved" | "error" | "invalid"; type BuildPricingRowsOptions = { @@ -239,6 +241,29 @@ const createDefaultPricingRow = (model: string, pricing?: Partial }; }; +const calculateEstimatedPricingCost = ( + pricing: Pick, +): number => { + if (pricing.billingMode === "request") { + return normalizePricingNumber(pricing.input + pricing.output + pricing.cache, 0); + } + + const blendedRate = + (pricing.input * VOLUME_ESTIMATE_INPUT_WEIGHT + pricing.output * VOLUME_ESTIMATE_OUTPUT_WEIGHT) / + (VOLUME_ESTIMATE_INPUT_WEIGHT + VOLUME_ESTIMATE_OUTPUT_WEIGHT); + + return normalizePricingNumber(blendedRate, 0); +}; + +const formatEstimatedPricingCost = ( + pricing: Pick, +): string => { + const estimatedCost = calculateEstimatedPricingCost(pricing).toFixed(PRICING_DECIMALS); + const unitLabel = pricing.billingMode === "request" ? "/ 次" : "/ 1M tokens"; + + return `$${estimatedCost} ${unitLabel}`; +}; + const buildModelChannelMap = (channels: Channel[]): Map => { const channelMap = new Map(); @@ -884,13 +909,18 @@ export function Pricing() {
{filteredCards.map((card) => ( - +
{card.model}
+ {card.isCustomized ? ( +

预估成本 ≈ {formatEstimatedPricingCost(card)}

+ ) : ( +

免费

+ )}
@@ -919,7 +949,10 @@ export function Pricing() { key={option.value} type="button" size="sm" - className={cn("h-6 px-2 text-xs shadow-none! bg-transparent! text-muted-foreground", card.billingMode === option.value && "bg-green-500/5! text-green-600")} + className={cn( + "h-6 px-2 text-xs shadow-none! bg-transparent! text-muted-foreground", + card.billingMode === option.value && "bg-green-500/5! text-green-600", + )} onClick={() => updateBillingMode(card.model, option.value)} > {option.label} @@ -931,7 +964,8 @@ export function Pricing() { {card.legacyRequest > 0 && (

- 兼容旧版独立按次价格 {card.legacyRequest.toFixed(PRICING_DECIMALS)},修改后会切换到新版计费规则。 + 兼容旧版独立按次价格 {card.legacyRequest.toFixed(PRICING_DECIMALS)} + ,修改后会切换到新版计费规则。

)} @@ -942,9 +976,7 @@ export function Pricing() { {channel.label} - - {channel.weight} - + {channel.weight}
)) ) : ( @@ -961,7 +993,8 @@ export function Pricing() { JSON 配置 - 默认只会保存大于 0 的价格字段;但 `billingMode: "request"` 即使价格全为 0,也会作为有效配置保留。切回卡片模式时,会与当前渠道中的模型集合自动合并。 + 默认只会保存大于 0 的价格字段;但 `billingMode: "request"` 即使价格全为 + 0,也会作为有效配置保留。切回卡片模式时,会与当前渠道中的模型集合自动合并。 @@ -973,7 +1006,8 @@ export function Pricing() { placeholder='{"gpt-4.1": {"billingMode": "volume", "input": 0.8, "output": 3.2}}' />

- 格式:模型名称 → {"{"} billingMode?: "volume" | "request", input?: 输入价格, output?: 输出价格, cache?: 缓存价格, request?: 旧版独立按次价格 {"}"} + 格式:模型名称 → {"{"} billingMode?: "volume" | "request", input?: 输入价格, output?: 输出价格, cache?: + 缓存价格, request?: 旧版独立按次价格 {"}"}