mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-10 13:10:21 +08:00
fix(frontend): clarify processing tier pricing
This commit is contained in:
@@ -398,6 +398,7 @@
|
||||
:show-token-pricing="billingMode === 'token'"
|
||||
:show-image-pricing="isImageGenerationEnabled"
|
||||
:show-image-editor="billingMode === 'image'"
|
||||
:show-processing-tier-controls="false"
|
||||
:show-processing-tier-multiplier-controls="true"
|
||||
/>
|
||||
|
||||
|
||||
+2
-2
@@ -167,8 +167,8 @@ describe('GlobalModelFormDialog preset replacement', () => {
|
||||
findButton('Stale Model').click()
|
||||
await settle()
|
||||
|
||||
expect(document.body.querySelector('[data-processing-tier="standard"]')).not.toBeNull()
|
||||
expect(document.body.querySelector('[data-processing-tier="priority"]')).not.toBeNull()
|
||||
expect(document.body.querySelector('[data-processing-tier]')).toBeNull()
|
||||
expect(document.body.textContent).not.toContain('处理层级')
|
||||
expect(document.body.textContent).toContain('自定义价格')
|
||||
|
||||
await setInput(
|
||||
|
||||
@@ -207,7 +207,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<ServiceTierFacts
|
||||
v-if="hasServiceTierFacts || processingTierPriceMultiplier !== null"
|
||||
v-if="hasServiceTierFacts && processingTierPriceMultiplier === null"
|
||||
class="mt-3"
|
||||
:requested="serviceTierFacts.requested"
|
||||
:price-multiplier="processingTierPriceMultiplier"
|
||||
@@ -225,7 +225,14 @@
|
||||
<!-- 阶梯标题 -->
|
||||
<div class="text-xs text-muted-foreground flex items-center gap-2 flex-wrap">
|
||||
<span class="font-medium text-foreground">Token 计费</span>
|
||||
<span class="font-mono font-medium text-foreground">${{ tokenCostTotal.toFixed(6) }}</span>
|
||||
<span class="font-mono font-medium text-foreground">
|
||||
<template v-if="processingTierPriceMultiplier !== null">
|
||||
${{ tokenCostBaseTotal.toFixed(6) }} × {{ processingTierPriceMultiplier }} ({{ processingTierLabel }} 层级)
|
||||
</template>
|
||||
<template v-else>
|
||||
${{ tokenCostTotal.toFixed(6) }}
|
||||
</template>
|
||||
</span>
|
||||
<span class="text-muted-foreground/60">(输入 {{ formatNumber(displayInputTokens) }} + 缓存创建 {{ cacheCreationSummaryText }} + 缓存读取 {{ formatNumber(detail.cache_read_input_tokens || 0) }})</span>
|
||||
<Badge
|
||||
v-if="displayTiers.length > 1"
|
||||
@@ -315,22 +322,22 @@
|
||||
<div class="grid grid-cols-[64px_minmax(0,1fr)_92px] items-center gap-x-2">
|
||||
<span class="text-xs text-muted-foreground">输入</span>
|
||||
<span class="text-sm font-semibold font-mono text-right tabular-nums">{{ displayInputTokens }}</span>
|
||||
<span class="text-xs font-mono text-right tabular-nums">${{ effectiveInputCost.toFixed(6) }}</span>
|
||||
<span class="text-xs font-mono text-right tabular-nums">${{ displayInputCost.toFixed(6) }}</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-[64px_minmax(0,1fr)_92px] items-center gap-x-2">
|
||||
<span class="text-xs text-muted-foreground">输出</span>
|
||||
<span class="text-sm font-semibold font-mono text-right tabular-nums">{{ detail.tokens?.output || detail.output_tokens || 0 }}</span>
|
||||
<span class="text-xs font-mono text-right tabular-nums">${{ effectiveOutputCost.toFixed(6) }}</span>
|
||||
<span class="text-xs font-mono text-right tabular-nums">${{ displayOutputCost.toFixed(6) }}</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-[64px_minmax(0,1fr)_92px] items-center gap-x-2">
|
||||
<span class="text-xs text-muted-foreground">缓存创建</span>
|
||||
<span class="text-sm font-semibold font-mono text-right tabular-nums">{{ totalCacheCreationTokens }}</span>
|
||||
<span class="text-xs font-mono text-right tabular-nums">${{ effectiveCacheCreationCost.toFixed(6) }}</span>
|
||||
<span class="text-xs font-mono text-right tabular-nums">${{ displayCacheCreationCost.toFixed(6) }}</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-[64px_minmax(0,1fr)_92px] items-center gap-x-2">
|
||||
<span class="text-xs text-muted-foreground">缓存读取</span>
|
||||
<span class="text-sm font-semibold font-mono text-right tabular-nums">{{ detail.cache_read_input_tokens || 0 }}</span>
|
||||
<span class="text-xs font-mono text-right tabular-nums">${{ effectiveCacheReadCost.toFixed(6) }}</span>
|
||||
<span class="text-xs font-mono text-right tabular-nums">${{ displayCacheReadCost.toFixed(6) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 输入 输出 -->
|
||||
@@ -338,7 +345,7 @@
|
||||
<div class="flex items-center flex-1">
|
||||
<span class="text-xs text-muted-foreground w-[56px]">输入</span>
|
||||
<span class="text-sm font-semibold font-mono flex-1 text-center">{{ displayInputTokens }}</span>
|
||||
<span class="text-xs font-mono">${{ effectiveInputCost.toFixed(6) }}</span>
|
||||
<span class="text-xs font-mono">${{ displayInputCost.toFixed(6) }}</span>
|
||||
</div>
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
@@ -347,7 +354,7 @@
|
||||
<div class="flex items-center flex-1">
|
||||
<span class="text-xs text-muted-foreground w-[56px]">输出</span>
|
||||
<span class="text-sm font-semibold font-mono flex-1 text-center">{{ detail.tokens?.output || detail.output_tokens || 0 }}</span>
|
||||
<span class="text-xs font-mono">${{ effectiveOutputCost.toFixed(6) }}</span>
|
||||
<span class="text-xs font-mono">${{ displayOutputCost.toFixed(6) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 缓存创建 缓存读取 -->
|
||||
@@ -355,7 +362,7 @@
|
||||
<div class="flex items-center flex-1">
|
||||
<span class="text-xs text-muted-foreground w-[56px]">{{ cacheCreationSplitRows.length > 0 ? '创建合计' : '缓存创建' }}</span>
|
||||
<span class="text-sm font-semibold font-mono flex-1 text-center">{{ totalCacheCreationTokens }}</span>
|
||||
<span class="text-xs font-mono">${{ effectiveCacheCreationCost.toFixed(6) }}</span>
|
||||
<span class="text-xs font-mono">${{ displayCacheCreationCost.toFixed(6) }}</span>
|
||||
</div>
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
@@ -364,7 +371,7 @@
|
||||
<div class="flex items-center flex-1">
|
||||
<span class="text-xs text-muted-foreground w-[56px]">缓存读取</span>
|
||||
<span class="text-sm font-semibold font-mono flex-1 text-center">{{ detail.cache_read_input_tokens || 0 }}</span>
|
||||
<span class="text-xs font-mono">${{ effectiveCacheReadCost.toFixed(6) }}</span>
|
||||
<span class="text-xs font-mono">${{ displayCacheReadCost.toFixed(6) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 缓存创建 5m/1h 细分 -->
|
||||
@@ -870,6 +877,7 @@ import {
|
||||
import {
|
||||
formatPricePerMillion,
|
||||
resolveProcessingTierPriceMultiplier,
|
||||
resolveSettlementPricingSnapshot,
|
||||
resolveSettlementPricingSourceLabel,
|
||||
resolveSettlementPricingTiers,
|
||||
} from '../utils/settlement-pricing'
|
||||
@@ -884,6 +892,7 @@ import ReplayDialog from './ReplayDialog.vue'
|
||||
import ServiceTierFacts from './ServiceTierFacts.vue'
|
||||
import UsageModelDisplay from './UsageModelDisplay.vue'
|
||||
import {
|
||||
formatServiceTierFact,
|
||||
hasServiceTierFact,
|
||||
resolveServiceTierFacts,
|
||||
} from '../utils/service-tier'
|
||||
@@ -1529,6 +1538,12 @@ const headerModelRecord = computed(() => {
|
||||
})
|
||||
const serviceTierFacts = computed(() => resolveServiceTierFacts(headerModelRecord.value))
|
||||
const hasServiceTierFacts = computed(() => hasServiceTierFact(serviceTierFacts.value))
|
||||
const settlementPricingSnapshot = computed(() => resolveSettlementPricingSnapshot(detail.value))
|
||||
const processingTierLabel = computed(() => (
|
||||
formatServiceTierFact(serviceTierFacts.value.requested)
|
||||
?? formatServiceTierFact(getNestedString(settlementPricingSnapshot.value, 'billing_processing_tier'))
|
||||
?? '处理'
|
||||
))
|
||||
const detailCyberPolicyError = computed(() => {
|
||||
const summaryError = props.summaryRecord?.error_message
|
||||
const currentDetail = detailForCurrentRequest.value
|
||||
@@ -1920,6 +1935,20 @@ const effectiveCacheReadCost = computed(() =>
|
||||
?? 0,
|
||||
)
|
||||
|
||||
// Fast/Priority 的倍率目录是由后端从 Standard 目录物化出来的。详情页的阶梯与
|
||||
// 分项成本保留 Standard 基准值,倍率统一显示在 Token 计费标题中,避免把倍率
|
||||
// 隐藏在每个单价和每个分项成本里。
|
||||
function removeProcessingTierMultiplier(value: number): number {
|
||||
const multiplier = processingTierPriceMultiplier.value
|
||||
if (multiplier === null || multiplier <= 0) return value
|
||||
return value / multiplier
|
||||
}
|
||||
|
||||
const displayInputCost = computed(() => removeProcessingTierMultiplier(effectiveInputCost.value))
|
||||
const displayOutputCost = computed(() => removeProcessingTierMultiplier(effectiveOutputCost.value))
|
||||
const displayCacheCreationCost = computed(() => removeProcessingTierMultiplier(effectiveCacheCreationCost.value))
|
||||
const displayCacheReadCost = computed(() => removeProcessingTierMultiplier(effectiveCacheReadCost.value))
|
||||
|
||||
const effectiveRequestCost = computed(() => {
|
||||
const snapshotCost = getNestedNumber(billingCostBreakdown.value, 'request_cost')
|
||||
if (snapshotCost !== null) return snapshotCost
|
||||
@@ -2078,22 +2107,49 @@ const activeCacheTtlMinutes = computed(() => {
|
||||
|
||||
// 统一的阶梯显示数据
|
||||
// 如果有 tiered_pricing,使用它;否则用历史价格构建单阶梯
|
||||
function restoreBaseTierPricing(tier: PricingTierLike): PricingTierLike {
|
||||
const multiplier = processingTierPriceMultiplier.value
|
||||
if (multiplier === null || multiplier <= 0) return tier
|
||||
|
||||
const restore = (value: number | null | undefined): number | null | undefined => {
|
||||
if (value === null || value === undefined) return value
|
||||
return value / multiplier
|
||||
}
|
||||
|
||||
return {
|
||||
...tier,
|
||||
input_price_per_1m: restore(tier.input_price_per_1m),
|
||||
output_price_per_1m: restore(tier.output_price_per_1m),
|
||||
cache_creation_price_per_1m: restore(tier.cache_creation_price_per_1m),
|
||||
cache_read_price_per_1m: restore(tier.cache_read_price_per_1m),
|
||||
cache_ttl_pricing: Array.isArray(tier.cache_ttl_pricing)
|
||||
? tier.cache_ttl_pricing.map(entry => ({
|
||||
...entry,
|
||||
cache_creation_price_per_1m: restore(entry.cache_creation_price_per_1m),
|
||||
cache_read_price_per_1m: restore(entry.cache_read_price_per_1m),
|
||||
}))
|
||||
: tier.cache_ttl_pricing,
|
||||
}
|
||||
}
|
||||
|
||||
const displayTiers = computed(() => {
|
||||
if (!detail.value) return []
|
||||
|
||||
// 优先展示结算快照中已解析的价格目录,旧字段仅作回退。
|
||||
const resolvedTiers = resolveSettlementPricingTiers(detail.value)
|
||||
if (resolvedTiers) return resolvedTiers as PricingTierLike[]
|
||||
if (resolvedTiers) {
|
||||
return (resolvedTiers as PricingTierLike[]).map(restoreBaseTierPricing)
|
||||
}
|
||||
|
||||
// 否则用历史价格构建单阶梯(无上限)
|
||||
return [{
|
||||
return [restoreBaseTierPricing({
|
||||
up_to: null,
|
||||
input_price_per_1m: effectiveInputPricePer1M.value ?? 0,
|
||||
output_price_per_1m: effectiveOutputPricePer1M.value ?? 0,
|
||||
cache_creation_price_per_1m: effectiveCacheCreationPricePer1M.value,
|
||||
cache_read_price_per_1m: effectiveCacheReadPricePer1M.value,
|
||||
cache_ttl_pricing: fallbackCacheTtlPricing.value,
|
||||
}]
|
||||
})]
|
||||
})
|
||||
|
||||
// 当前命中的阶梯索引
|
||||
@@ -2203,6 +2259,13 @@ const tokenCostTotal = computed(() => {
|
||||
+ effectiveCacheReadCost.value
|
||||
})
|
||||
|
||||
const tokenCostBaseTotal = computed(() => (
|
||||
displayInputCost.value
|
||||
+ displayOutputCost.value
|
||||
+ displayCacheCreationCost.value
|
||||
+ displayCacheReadCost.value
|
||||
))
|
||||
|
||||
// 按次计费费用(非视频任务时)
|
||||
const perRequestCost = computed(() => {
|
||||
if (!detail.value) return 0
|
||||
|
||||
@@ -67,6 +67,42 @@ function buildEmbeddingDetail(): RequestDetail {
|
||||
}
|
||||
}
|
||||
|
||||
function buildFastTierDetail(): RequestDetail {
|
||||
return {
|
||||
...buildEmbeddingDetail(),
|
||||
id: 'usage-fast-tier-1',
|
||||
request_id: 'req-fast-tier-1',
|
||||
model: 'gpt-5.6-sol',
|
||||
service_tier: 'fast',
|
||||
tokens: { input: 1116, output: 3028, total: 4144 },
|
||||
input_tokens: 1116,
|
||||
output_tokens: 3028,
|
||||
cache_read_input_tokens: 205568,
|
||||
input_cost: 0.01395,
|
||||
output_cost: 0.2271,
|
||||
cache_creation_cost: 0,
|
||||
cache_read_cost: 0.25696,
|
||||
cost: { input: 0.01395, output: 0.2271, total: 0.49801 },
|
||||
settlement: {
|
||||
settlement_snapshot: {
|
||||
pricing_snapshot: {
|
||||
billing_processing_tier: 'fast',
|
||||
processing_tier_price_multiplier: 2.5,
|
||||
tiered_pricing: {
|
||||
tiers: [{
|
||||
up_to: null,
|
||||
input_price_per_1m: 12.5,
|
||||
output_price_per_1m: 75,
|
||||
cache_creation_price_per_1m: 15.625,
|
||||
cache_read_price_per_1m: 1.25,
|
||||
}],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe('RequestDetailDrawer settlement pricing', () => {
|
||||
it('renders an input-only embedding tier without treating the missing output price as zero', async () => {
|
||||
apiMocks.getRequestDetail.mockResolvedValue(buildEmbeddingDetail())
|
||||
@@ -98,6 +134,38 @@ describe('RequestDetailDrawer settlement pricing', () => {
|
||||
expect(document.body.textContent).not.toContain('输出 $0/M')
|
||||
})
|
||||
|
||||
it('shows Fast pricing as the base token cost multiplied by the Fast tier', async () => {
|
||||
apiMocks.getRequestDetail.mockResolvedValue(buildFastTierDetail())
|
||||
|
||||
let isOpen!: Ref<boolean>
|
||||
const Host = defineComponent({
|
||||
setup() {
|
||||
isOpen = ref(false)
|
||||
return () => h(RequestDetailDrawer, {
|
||||
isOpen: isOpen.value,
|
||||
requestId: 'usage-fast-tier-1',
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
const app = createApp(Host)
|
||||
app.mount(root)
|
||||
mountedApps.push({ app, root })
|
||||
|
||||
isOpen.value = true
|
||||
await nextTick()
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(document.body.textContent).toContain('$0.199204 × 2.5 (Fast 层级)')
|
||||
expect(document.body.textContent).toContain('输入 $5/M')
|
||||
expect(document.body.textContent).toContain('输出 $30/M')
|
||||
expect(document.body.textContent).toContain('缓存读取 $0.5/M')
|
||||
expect(document.body.querySelector('[data-testid="service-tier-facts"]')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
it('shows the compact request badge in the model header', async () => {
|
||||
apiMocks.getRequestDetail.mockResolvedValue({
|
||||
...buildEmbeddingDetail(),
|
||||
|
||||
Reference in New Issue
Block a user