mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 01:47:47 +08:00
feat(frontend): expose processing tier pricing
This commit is contained in:
@@ -249,6 +249,13 @@
|
||||
<span class="ml-1 font-bold text-primary">{{ formatOutputRateValue(detailOutputRate) }}tps</span>
|
||||
</span>
|
||||
</div>
|
||||
<ServiceTierFacts
|
||||
v-if="hasServiceTierFacts"
|
||||
class="mt-3"
|
||||
:requested="serviceTierFacts.requested"
|
||||
:actual="serviceTierFacts.actual"
|
||||
:billing="serviceTierFacts.billing"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
@@ -907,6 +914,8 @@ import JsonContentPanel from './JsonContentPanel.vue'
|
||||
import ConversationView from './RequestDetailDrawer/ConversationView.vue'
|
||||
import HorizontalRequestTimeline from './HorizontalRequestTimeline.vue'
|
||||
import ReplayDialog from './ReplayDialog.vue'
|
||||
import ServiceTierFacts from './ServiceTierFacts.vue'
|
||||
import { hasServiceTierFact, resolveServiceTierFacts } from '../utils/service-tier'
|
||||
|
||||
// 对话解析器
|
||||
import {
|
||||
@@ -952,6 +961,7 @@ const emit = defineEmits<{
|
||||
targetModel?: string | null
|
||||
reasoningEffort?: string | null
|
||||
serviceTier?: string | null
|
||||
actualServiceTier?: string | null
|
||||
imageProgress?: ImageProgress | null
|
||||
errorMessage?: string | null
|
||||
}]
|
||||
@@ -1134,6 +1144,7 @@ function emitDetailRequestState(nextDetail: RequestDetail) {
|
||||
targetModel: nextDetail.target_model ?? null,
|
||||
reasoningEffort: nextDetail.reasoning_effort ?? null,
|
||||
serviceTier: nextDetail.service_tier ?? null,
|
||||
actualServiceTier: nextDetail.actual_service_tier ?? null,
|
||||
errorMessage: nextDetail.error_message ?? undefined,
|
||||
})
|
||||
}
|
||||
@@ -1342,6 +1353,9 @@ const metadataPanelData = computed<Record<string, unknown> | null>(() => {
|
||||
|
||||
const failureNotice = computed(() => resolveRequestFailureNotice(detail.value))
|
||||
|
||||
const serviceTierFacts = computed(() => resolveServiceTierFacts(detail.value))
|
||||
const hasServiceTierFacts = computed(() => hasServiceTierFact(serviceTierFacts.value))
|
||||
|
||||
const settlementInfo = computed<JsonRecord | null>(() =>
|
||||
asRecord(detail.value?.settlement ?? null),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<dl
|
||||
class="grid grid-cols-1 gap-x-4 gap-y-1.5 text-xs sm:grid-cols-3"
|
||||
data-testid="service-tier-facts"
|
||||
>
|
||||
<div class="flex min-w-0 items-baseline justify-between gap-3 sm:block">
|
||||
<dt class="text-muted-foreground">
|
||||
请求层级
|
||||
</dt>
|
||||
<dd
|
||||
class="truncate font-mono font-medium text-foreground sm:mt-0.5"
|
||||
:title="requested || '-'"
|
||||
>
|
||||
{{ requested || '-' }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex min-w-0 items-baseline justify-between gap-3 sm:block">
|
||||
<dt class="text-muted-foreground">
|
||||
实际层级
|
||||
</dt>
|
||||
<dd
|
||||
class="truncate font-mono font-medium text-foreground sm:mt-0.5"
|
||||
:title="actual || '-'"
|
||||
>
|
||||
{{ actual || '-' }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex min-w-0 items-baseline justify-between gap-3 sm:block">
|
||||
<dt class="text-muted-foreground">
|
||||
计费层级
|
||||
</dt>
|
||||
<dd
|
||||
class="truncate font-mono font-medium text-foreground sm:mt-0.5"
|
||||
:title="billing || '-'"
|
||||
>
|
||||
{{ billing || '-' }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
requested: string | null
|
||||
actual: string | null
|
||||
billing: string | null
|
||||
}>()
|
||||
</script>
|
||||
@@ -1610,26 +1610,42 @@ function getReasoningEffortTitle(record: UsageRecord): string {
|
||||
return effort ? `Reasoning: ${effort}` : ''
|
||||
}
|
||||
|
||||
function getServiceTier(record: UsageRecord): string | null {
|
||||
const serviceTier = record.service_tier?.trim().toLowerCase()
|
||||
function normalizeServiceTier(value: string | null | undefined): string | null {
|
||||
const serviceTier = value?.trim().toLowerCase()
|
||||
return serviceTier || null
|
||||
}
|
||||
|
||||
function getRequestedServiceTier(record: UsageRecord): string | null {
|
||||
return normalizeServiceTier(record.service_tier)
|
||||
}
|
||||
|
||||
function getActualServiceTier(record: UsageRecord): string | null {
|
||||
return normalizeServiceTier(record.actual_service_tier)
|
||||
}
|
||||
|
||||
function getFastBadge(record: UsageRecord): boolean {
|
||||
return getServiceTier(record) === 'priority'
|
||||
return (getActualServiceTier(record) ?? getRequestedServiceTier(record)) === 'priority'
|
||||
}
|
||||
|
||||
function getFastBadgeTitle(record: UsageRecord): string {
|
||||
const serviceTier = getServiceTier(record)
|
||||
return serviceTier ? `Service tier: ${serviceTier}` : ''
|
||||
const requested = getRequestedServiceTier(record)
|
||||
const actual = getActualServiceTier(record)
|
||||
if (requested && actual) {
|
||||
return requested === actual
|
||||
? `Requested and actual service tier: ${actual}`
|
||||
: `Requested service tier: ${requested}\nActual service tier: ${actual}`
|
||||
}
|
||||
if (actual) return `Actual service tier: ${actual}`
|
||||
return requested ? `Requested service tier: ${requested}` : ''
|
||||
}
|
||||
|
||||
// 获取模型列的 tooltip
|
||||
function getModelTooltip(record: UsageRecord): string {
|
||||
const actualModel = getActualModel(record)
|
||||
const reasoningEffort = getReasoningEffort(record)
|
||||
const fastSuffix = getFastBadge(record) ? '\nService tier: priority' : ''
|
||||
const suffix = `${reasoningEffort ? `\nReasoning: ${reasoningEffort}` : ''}${fastSuffix}`
|
||||
const serviceTierTitle = getFastBadgeTitle(record)
|
||||
const tierSuffix = serviceTierTitle ? `\n${serviceTierTitle}` : ''
|
||||
const suffix = `${reasoningEffort ? `\nReasoning: ${reasoningEffort}` : ''}${tierSuffix}`
|
||||
if (actualModel) {
|
||||
return `${record.model} -> ${actualModel}${suffix}`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { createApp, h, type App } from 'vue'
|
||||
|
||||
import ServiceTierFacts from '../ServiceTierFacts.vue'
|
||||
|
||||
const mountedApps: Array<{ app: App, root: HTMLElement }> = []
|
||||
|
||||
afterEach(() => {
|
||||
for (const { app, root } of mountedApps.splice(0)) {
|
||||
app.unmount()
|
||||
root.remove()
|
||||
}
|
||||
})
|
||||
|
||||
describe('ServiceTierFacts', () => {
|
||||
it('renders all three facts and marks a missing actual tier explicitly', () => {
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
const app = createApp({
|
||||
render: () => h(ServiceTierFacts, {
|
||||
requested: 'priority',
|
||||
actual: null,
|
||||
billing: 'flex',
|
||||
}),
|
||||
})
|
||||
app.mount(root)
|
||||
mountedApps.push({ app, root })
|
||||
|
||||
expect(root.querySelector('[data-testid="service-tier-facts"]')).not.toBeNull()
|
||||
expect([...root.querySelectorAll('dt')].map(node => node.textContent?.trim())).toEqual([
|
||||
'请求层级',
|
||||
'实际层级',
|
||||
'计费层级',
|
||||
])
|
||||
expect([...root.querySelectorAll('dd')].map(node => node.textContent?.trim())).toEqual([
|
||||
'priority',
|
||||
'-',
|
||||
'flex',
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -295,6 +295,22 @@ describe('UsageRecordsTable', () => {
|
||||
expect(root.textContent).toContain('fast')
|
||||
})
|
||||
|
||||
it('uses the actual service tier when the provider changes processing class', () => {
|
||||
const downgraded = mountUsageRecordsTable([buildRecord({
|
||||
service_tier: 'priority',
|
||||
actual_service_tier: 'default',
|
||||
})])
|
||||
expect(downgraded.textContent).not.toContain('fast')
|
||||
expect(downgraded.querySelector('[title*="Requested service tier: priority"]')).not.toBeNull()
|
||||
expect(downgraded.querySelector('[title*="Actual service tier: default"]')).not.toBeNull()
|
||||
|
||||
const upgraded = mountUsageRecordsTable([buildRecord({
|
||||
service_tier: 'flex',
|
||||
actual_service_tier: 'priority',
|
||||
})])
|
||||
expect(upgraded.textContent).toContain('fast')
|
||||
})
|
||||
|
||||
it('offers embedding API formats in the usage record filter', () => {
|
||||
const root = mountUsageRecordsTable([buildRecord({ api_format: 'openai:chat' })])
|
||||
|
||||
|
||||
@@ -218,6 +218,7 @@ describe('useUsageData', () => {
|
||||
target_model: 'gpt-5.5',
|
||||
reasoning_effort: 'xhigh',
|
||||
service_tier: 'auto',
|
||||
actual_service_tier: 'priority',
|
||||
})
|
||||
|
||||
getAllUsageRecordsMock.mockResolvedValueOnce({
|
||||
@@ -246,6 +247,7 @@ describe('useUsageData', () => {
|
||||
target_model: null,
|
||||
reasoning_effort: null,
|
||||
service_tier: null,
|
||||
actual_service_tier: null,
|
||||
})],
|
||||
total: 1,
|
||||
limit: 20,
|
||||
@@ -279,6 +281,7 @@ describe('useUsageData', () => {
|
||||
target_model: 'gpt-5.5',
|
||||
reasoning_effort: 'xhigh',
|
||||
service_tier: 'auto',
|
||||
actual_service_tier: 'priority',
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -631,7 +631,8 @@ export function useUsageData(options: UseUsageDataOptions) {
|
||||
rate_multiplier: record.rate_multiplier ?? existing.rate_multiplier,
|
||||
target_model: record.target_model ?? existing.target_model,
|
||||
reasoning_effort: record.reasoning_effort ?? existing.reasoning_effort,
|
||||
service_tier: record.service_tier ?? existing.service_tier
|
||||
service_tier: record.service_tier ?? existing.service_tier,
|
||||
actual_service_tier: record.actual_service_tier ?? existing.actual_service_tier
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ export interface UsageRecord {
|
||||
model_version?: string | null // Provider 返回的实际模型版本(列表轻量字段)
|
||||
reasoning_effort?: string | null // 从发送给 Provider 的请求体提取的 reasoning 级别
|
||||
service_tier?: string | null // 从发送给 Provider 的请求体提取的服务层级
|
||||
actual_service_tier?: string | null // Provider 响应确认的实际服务层级
|
||||
api_format?: string
|
||||
endpoint_api_format?: string // 端点原生格式
|
||||
has_format_conversion?: boolean // 是否发生了格式转换
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
hasServiceTierFact,
|
||||
normalizeServiceTierFact,
|
||||
resolveServiceTierFacts,
|
||||
} from '../service-tier'
|
||||
|
||||
describe('service tier facts', () => {
|
||||
it('keeps requested, actual and billing tiers independent', () => {
|
||||
const facts = resolveServiceTierFacts({
|
||||
service_tier: 'priority',
|
||||
actual_service_tier: 'default',
|
||||
settlement: {
|
||||
settlement_snapshot: {
|
||||
pricing_snapshot: {
|
||||
requested_processing_tier: 'ignored-requested-snapshot',
|
||||
actual_processing_tier: 'ignored-actual-snapshot',
|
||||
billing_processing_tier: 'standard',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(facts).toEqual({ requested: 'priority', actual: 'default', billing: 'standard' })
|
||||
expect(hasServiceTierFact(facts)).toBe(true)
|
||||
})
|
||||
|
||||
it('does not infer billing from requested or actual tiers', () => {
|
||||
expect(resolveServiceTierFacts({
|
||||
service_tier: 'priority',
|
||||
actual_service_tier: 'flex',
|
||||
})).toEqual({ requested: 'priority', actual: 'flex', billing: null })
|
||||
})
|
||||
|
||||
it('normalizes only non-empty string facts', () => {
|
||||
expect(normalizeServiceTierFact(' Batch ')).toBe('Batch')
|
||||
expect(normalizeServiceTierFact(' ')).toBeNull()
|
||||
expect(normalizeServiceTierFact(0)).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,40 @@
|
||||
export interface ServiceTierFacts {
|
||||
requested: string | null
|
||||
actual: string | null
|
||||
billing: string | null
|
||||
}
|
||||
|
||||
export interface ServiceTierFactSource {
|
||||
service_tier?: unknown
|
||||
actual_service_tier?: unknown
|
||||
settlement?: unknown
|
||||
}
|
||||
|
||||
export function resolveServiceTierFacts(
|
||||
source: ServiceTierFactSource | null | undefined,
|
||||
): ServiceTierFacts {
|
||||
const settlement = asRecord(source?.settlement)
|
||||
const settlementSnapshot = asRecord(settlement?.settlement_snapshot)
|
||||
const pricingSnapshot = asRecord(settlementSnapshot?.pricing_snapshot)
|
||||
return {
|
||||
requested: normalizeServiceTierFact(source?.service_tier),
|
||||
actual: normalizeServiceTierFact(source?.actual_service_tier),
|
||||
billing: normalizeServiceTierFact(pricingSnapshot?.billing_processing_tier),
|
||||
}
|
||||
}
|
||||
|
||||
export function hasServiceTierFact(facts: ServiceTierFacts): boolean {
|
||||
return facts.requested !== null || facts.actual !== null || facts.billing !== null
|
||||
}
|
||||
|
||||
export function normalizeServiceTierFact(value: unknown): string | null {
|
||||
if (typeof value !== 'string') return null
|
||||
const normalized = value.trim()
|
||||
return normalized || null
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: null
|
||||
}
|
||||
Reference in New Issue
Block a user