feat(pricing): support processing tier multipliers

This commit is contained in:
elky
2026-07-16 23:30:42 +08:00
parent 312583f055
commit 0be380243b
40 changed files with 5012 additions and 565 deletions
@@ -0,0 +1,100 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { createApp, defineComponent, h, nextTick, ref, type App, type Ref } from 'vue'
import type { RequestDetail } from '@/api/dashboard'
import RequestDetailDrawer from '../RequestDetailDrawer.vue'
const apiMocks = vi.hoisted(() => ({
getRequestDetail: vi.fn(),
}))
vi.mock('@/api/dashboard', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/api/dashboard')>()
return {
...actual,
dashboardApi: {
...actual.dashboardApi,
getRequestDetail: apiMocks.getRequestDetail,
},
}
})
const mountedApps: Array<{ app: App, root: HTMLElement }> = []
afterEach(() => {
for (const { app, root } of mountedApps.splice(0)) {
app.unmount()
root.remove()
}
apiMocks.getRequestDetail.mockReset()
})
function buildEmbeddingDetail(): RequestDetail {
return {
id: 'usage-embedding-1',
request_id: 'req-embedding-1',
user: {
id: 'user-1',
username: 'embedding-user',
email: '[email protected]',
},
api_key: {
id: 'key-1',
name: 'test-key',
display: 'test-key',
},
provider: 'embedding-provider',
model: 'embedding-model',
tokens: { input: 100, output: 0, total: 100 },
cost: { input: 0.00001, output: 0, total: 0.00001 },
request_type: 'embedding',
is_stream: false,
status: 'completed',
status_code: 200,
response_time_ms: 10,
created_at: '2026-07-16T00:00:00Z',
request_headers: { 'content-type': 'application/json' },
settlement: {
settlement_snapshot: {
pricing_snapshot: {
pricing_source: 'global_default',
tiered_pricing: {
tiers: [{ up_to: null, input_price_per_1m: 0.1 }],
},
},
},
},
}
}
describe('RequestDetailDrawer settlement pricing', () => {
it('renders an input-only embedding tier without treating the missing output price as zero', async () => {
apiMocks.getRequestDetail.mockResolvedValue(buildEmbeddingDetail())
let isOpen!: Ref<boolean>
const Host = defineComponent({
setup() {
isOpen = ref(false)
return () => h(RequestDetailDrawer, {
isOpen: isOpen.value,
requestId: 'usage-embedding-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.1/M')
expect(document.body.textContent).toContain('输出 -')
})
expect(document.body.textContent).not.toContain('输出 $0/M')
})
})
@@ -33,9 +33,75 @@ describe('ServiceTierFacts', () => {
'计费层级',
])
expect([...root.querySelectorAll('dd')].map(node => node.textContent?.trim())).toEqual([
'priority',
'Fast',
'-',
'flex',
])
expect([...root.querySelectorAll('dd')].map(node => node.getAttribute('title'))).toEqual([
'Fast',
'-',
'flex',
])
})
it('uses the same Fast label for raw priority and fast facts', () => {
const root = document.createElement('div')
document.body.appendChild(root)
const app = createApp({
render: () => h(ServiceTierFacts, {
requested: 'priority',
actual: 'fast',
billing: 'priority',
}),
})
app.mount(root)
mountedApps.push({ app, root })
expect([...root.querySelectorAll('dd')].map(node => node.textContent?.trim())).toEqual([
'Fast',
'Fast',
'Fast',
])
expect([...root.querySelectorAll('dd')].map(node => node.getAttribute('title'))).toEqual([
'Fast',
'Fast',
'Fast',
])
})
it('renders the processing-tier multiplier with the billing tier label', () => {
const root = document.createElement('div')
document.body.appendChild(root)
const app = createApp({
render: () => h(ServiceTierFacts, {
requested: 'priority',
actual: 'fast',
billing: 'fast',
priceMultiplier: 2.5,
}),
})
app.mount(root)
mountedApps.push({ app, root })
const multiplier = root.querySelector('[data-testid="service-tier-price-multiplier"]')
expect(multiplier?.textContent).toContain('Fast 倍率')
expect(multiplier?.textContent).toContain('2.5×')
})
it('does not render an empty or invalid processing-tier multiplier', () => {
const root = document.createElement('div')
document.body.appendChild(root)
const app = createApp({
render: () => h(ServiceTierFacts, {
requested: 'priority',
actual: null,
billing: null,
priceMultiplier: null,
}),
})
app.mount(root)
mountedApps.push({ app, root })
expect(root.querySelector('[data-testid="service-tier-price-multiplier"]')).toBeNull()
})
})
@@ -162,11 +162,12 @@ function mountUsageRecordsTable(records: UsageRecord[], overrides: Record<string
return root
}
function expectServiceTierBadge(root: HTMLElement, label: string) {
const labels = [...root.querySelectorAll('span')]
.map((element) => element.textContent?.trim())
function expectServiceTierBadge(root: HTMLElement, label: string): HTMLElement {
const badge = [...root.querySelectorAll<HTMLElement>('span')]
.find(element => element.textContent?.trim() === label)
expect(labels).toContain(label)
expect(badge).toBeDefined()
return badge as HTMLElement
}
afterEach(() => {
@@ -295,13 +296,26 @@ describe('UsageRecordsTable', () => {
expect(root.textContent).toContain('xhigh')
})
it('shows confirmed fast when requested and actual service tiers are priority', () => {
it.each([
['priority', 'priority'],
['fast', 'fast'],
['priority', 'fast'],
['fast', 'priority'],
])('shows confirmed Fast for requested %s and actual %s', (requested, actual) => {
const root = mountUsageRecordsTable([buildRecord({
service_tier: 'priority',
actual_service_tier: 'priority',
service_tier: requested,
actual_service_tier: actual,
})])
expectServiceTierBadge(root, 'fast')
const badge = expectServiceTierBadge(root, 'Fast')
expect(badge.getAttribute('title')).toBe([
'请求档位:Fast',
'实际档位:Fast',
'计费档位:Fast',
].join('\n'))
expect(badge.getAttribute('aria-label')).toBe(
'请求档位:Fast,实际档位:Fast,计费档位:Fast',
)
})
it('shows fast to standard when the provider downgrades a priority request', () => {
@@ -310,7 +324,12 @@ describe('UsageRecordsTable', () => {
actual_service_tier: 'default',
})])
expectServiceTierBadge(root, 'fast → standard')
const badge = expectServiceTierBadge(root, 'Fast → standard')
expect(badge.getAttribute('title')).toBe([
'请求档位:Fast',
'实际档位:default',
'计费档位:standard',
].join('\n'))
})
it('shows fast to flex when the provider moves a priority request to flex', () => {
@@ -319,7 +338,7 @@ describe('UsageRecordsTable', () => {
actual_service_tier: 'flex',
})])
expectServiceTierBadge(root, 'fast → flex')
expectServiceTierBadge(root, 'Fast → flex')
})
it('shows standard to fast when the provider upgrades a default request', () => {
@@ -328,7 +347,7 @@ describe('UsageRecordsTable', () => {
actual_service_tier: 'priority',
})])
expectServiceTierBadge(root, 'standard → fast')
expectServiceTierBadge(root, 'standard → Fast')
})
it.each(['pending', 'streaming'] as const)(
@@ -340,7 +359,7 @@ describe('UsageRecordsTable', () => {
status,
})])
expectServiceTierBadge(root, 'fast · 待确认')
expectServiceTierBadge(root, 'Fast · 待确认')
},
)
@@ -351,7 +370,7 @@ describe('UsageRecordsTable', () => {
status: 'completed',
})])
expectServiceTierBadge(root, 'fast · 未确认')
expectServiceTierBadge(root, 'Fast · 未确认')
})
it('offers embedding API formats in the usage record filter', () => {