mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 18:07:47 +08:00
Merge remote-tracking branch 'origin/main' into codex/usage-pending-reasoning-reset-expiry-20260712
# Conflicts: # crates/aether-ai-formats/src/formats/openai/responses/mod.rs # crates/aether-usage/runtime/src/runtime.rs # frontend/src/features/usage/components/UsageRecordsTable.vue # frontend/src/features/usage/components/__tests__/UsageRecordsTable.spec.ts
This commit is contained in:
@@ -261,12 +261,14 @@
|
||||
{{ getReasoningEffort(record) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="getFastBadge(record)"
|
||||
v-if="getServiceTierBadge(record)"
|
||||
variant="outline"
|
||||
class="h-4 rounded-full px-1.5 text-[10px] leading-4 text-foreground flex-shrink-0"
|
||||
:title="getFastBadgeTitle(record)"
|
||||
class="h-4 whitespace-nowrap rounded-full px-1.5 text-[10px] leading-4 flex-shrink-0"
|
||||
:class="getServiceTierBadge(record)?.className"
|
||||
:title="getServiceTierBadge(record)?.title"
|
||||
:aria-label="getServiceTierBadge(record)?.ariaLabel"
|
||||
>
|
||||
fast
|
||||
{{ getServiceTierBadge(record)?.label }}
|
||||
</Badge>
|
||||
<!-- 状态 Badge -->
|
||||
<Badge
|
||||
@@ -778,12 +780,14 @@
|
||||
{{ getReasoningEffort(record) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="getFastBadge(record)"
|
||||
v-if="getServiceTierBadge(record)"
|
||||
variant="outline"
|
||||
class="h-4 rounded-full px-1.5 text-[10px] leading-4 text-foreground flex-shrink-0"
|
||||
:title="getFastBadgeTitle(record)"
|
||||
class="h-4 whitespace-nowrap rounded-full px-1.5 text-[10px] leading-4 flex-shrink-0"
|
||||
:class="getServiceTierBadge(record)?.className"
|
||||
:title="getServiceTierBadge(record)?.title"
|
||||
:aria-label="getServiceTierBadge(record)?.ariaLabel"
|
||||
>
|
||||
fast
|
||||
{{ getServiceTierBadge(record)?.label }}
|
||||
</Badge>
|
||||
</div>
|
||||
<span class="text-muted-foreground truncate">{{ getActualModel(record) }}</span>
|
||||
@@ -810,12 +814,14 @@
|
||||
{{ getReasoningEffort(record) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="getFastBadge(record)"
|
||||
v-if="getServiceTierBadge(record)"
|
||||
variant="outline"
|
||||
class="h-4 rounded-full px-1.5 text-[10px] leading-4 text-foreground flex-shrink-0"
|
||||
:title="getFastBadgeTitle(record)"
|
||||
class="h-4 whitespace-nowrap rounded-full px-1.5 text-[10px] leading-4 flex-shrink-0"
|
||||
:class="getServiceTierBadge(record)?.className"
|
||||
:title="getServiceTierBadge(record)?.title"
|
||||
:aria-label="getServiceTierBadge(record)?.ariaLabel"
|
||||
>
|
||||
fast
|
||||
{{ getServiceTierBadge(record)?.label }}
|
||||
</Badge>
|
||||
</span>
|
||||
</TableCell>
|
||||
@@ -1638,41 +1644,138 @@ function getReasoningEffortTitle(record: UsageRecord): string {
|
||||
return effort ? `Reasoning: ${effort}` : ''
|
||||
}
|
||||
|
||||
type ServiceTierBadgeState = 'confirmed' | 'downgraded' | 'upgraded' | 'pending' | 'unconfirmed'
|
||||
|
||||
interface ServiceTierBadgePresentation {
|
||||
label: string
|
||||
state: ServiceTierBadgeState
|
||||
className: string
|
||||
title: string
|
||||
ariaLabel: string
|
||||
}
|
||||
|
||||
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 (getActualServiceTier(record) ?? getRequestedServiceTier(record)) === 'priority'
|
||||
}
|
||||
|
||||
function getFastBadgeTitle(record: UsageRecord): string {
|
||||
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}`
|
||||
function canonicalServiceTier(value: string | null): string | null {
|
||||
if (value === 'auto' || value === 'default' || value === 'standard') {
|
||||
return 'standard'
|
||||
}
|
||||
if (actual) return `Actual service tier: ${actual}`
|
||||
return requested ? `Requested service tier: ${requested}` : ''
|
||||
return value
|
||||
}
|
||||
|
||||
function serviceTierBadgeClass(state: ServiceTierBadgeState): string {
|
||||
switch (state) {
|
||||
case 'confirmed':
|
||||
return 'border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300'
|
||||
case 'downgraded':
|
||||
return 'border-amber-500/50 bg-amber-500/10 text-amber-700 dark:text-amber-300'
|
||||
case 'upgraded':
|
||||
return 'border-sky-500/40 bg-sky-500/10 text-sky-700 dark:text-sky-300'
|
||||
case 'pending':
|
||||
return 'border-dashed border-muted-foreground/30 bg-muted/30 text-muted-foreground'
|
||||
case 'unconfirmed':
|
||||
return 'border-dashed border-amber-500/40 bg-amber-500/5 text-amber-700 dark:text-amber-300'
|
||||
}
|
||||
}
|
||||
|
||||
function buildServiceTierBadgePresentation(
|
||||
label: string,
|
||||
state: ServiceTierBadgeState,
|
||||
requestedRaw: string | null,
|
||||
actualRaw: string | null,
|
||||
billingTier: string | null,
|
||||
): ServiceTierBadgePresentation {
|
||||
const titleLines: string[] = []
|
||||
if (requestedRaw) titleLines.push(`请求档位:${requestedRaw}`)
|
||||
if (actualRaw) titleLines.push(`实际档位:${actualRaw}`)
|
||||
if (billingTier) {
|
||||
titleLines.push(`计费档位:${billingTier}`)
|
||||
} else {
|
||||
titleLines.push(`计费档位:${state === 'pending' ? '待上游确认' : '未确认'}`)
|
||||
}
|
||||
const title = titleLines.join('\n')
|
||||
return {
|
||||
label,
|
||||
state,
|
||||
className: serviceTierBadgeClass(state),
|
||||
title,
|
||||
ariaLabel: titleLines.join(','),
|
||||
}
|
||||
}
|
||||
|
||||
function getServiceTierBadge(record: UsageRecord): ServiceTierBadgePresentation | null {
|
||||
const requestedRaw = normalizeServiceTier(record.service_tier)
|
||||
const actualRaw = normalizeServiceTier(record.actual_service_tier)
|
||||
const requested = canonicalServiceTier(requestedRaw)
|
||||
const actual = canonicalServiceTier(actualRaw)
|
||||
const requestedFast = requested === 'priority'
|
||||
const actualFast = actual === 'priority'
|
||||
|
||||
if (actual) {
|
||||
if (requestedFast && !actualFast) {
|
||||
return buildServiceTierBadgePresentation(
|
||||
`fast → ${actual}`,
|
||||
'downgraded',
|
||||
requestedRaw,
|
||||
actualRaw,
|
||||
actual,
|
||||
)
|
||||
}
|
||||
if (!requestedFast && actualFast) {
|
||||
const requestedLabel = requested ?? 'standard'
|
||||
return buildServiceTierBadgePresentation(
|
||||
requested ? `${requestedLabel} → fast` : 'fast',
|
||||
requested ? 'upgraded' : 'confirmed',
|
||||
requestedRaw,
|
||||
actualRaw,
|
||||
actual,
|
||||
)
|
||||
}
|
||||
if (actualFast) {
|
||||
return buildServiceTierBadgePresentation(
|
||||
'fast',
|
||||
'confirmed',
|
||||
requestedRaw,
|
||||
actualRaw,
|
||||
actual,
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
if (!requestedFast) return null
|
||||
const displayStatus = getDisplayStatus(record)
|
||||
const isActive = displayStatus === 'pending' || displayStatus === 'streaming'
|
||||
return buildServiceTierBadgePresentation(
|
||||
isActive ? 'fast · 待确认' : 'fast · 未确认',
|
||||
isActive ? 'pending' : 'unconfirmed',
|
||||
requestedRaw,
|
||||
null,
|
||||
null,
|
||||
)
|
||||
}
|
||||
|
||||
function getServiceTierTitle(record: UsageRecord): string {
|
||||
const badge = getServiceTierBadge(record)
|
||||
if (badge) return badge.title
|
||||
|
||||
const requested = normalizeServiceTier(record.service_tier)
|
||||
const actual = normalizeServiceTier(record.actual_service_tier)
|
||||
return [
|
||||
requested ? `请求档位:${requested}` : null,
|
||||
actual ? `实际档位:${actual}` : null,
|
||||
].filter((line): line is string => Boolean(line)).join('\n')
|
||||
}
|
||||
|
||||
// 获取模型列的 tooltip
|
||||
function getModelTooltip(record: UsageRecord): string {
|
||||
const actualModel = getActualModel(record)
|
||||
const reasoningEffort = getReasoningEffort(record)
|
||||
const serviceTierTitle = getFastBadgeTitle(record)
|
||||
const requestType = getRequestTypeLabel(record)
|
||||
const serviceTierTitle = getServiceTierTitle(record)
|
||||
const tierSuffix = serviceTierTitle ? `\n${serviceTierTitle}` : ''
|
||||
const suffix = `${requestType ? `\n操作: ${requestType}` : ''}${reasoningEffort ? `\nReasoning: ${reasoningEffort}` : ''}${tierSuffix}`
|
||||
if (actualModel) {
|
||||
|
||||
@@ -162,6 +162,13 @@ 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())
|
||||
|
||||
expect(labels).toContain(label)
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const { app, root } of mountedApps.splice(0)) {
|
||||
app.unmount()
|
||||
@@ -306,27 +313,63 @@ describe('UsageRecordsTable', () => {
|
||||
expect(root.textContent).toContain('会话压缩')
|
||||
})
|
||||
|
||||
it('shows fast badge for priority service tier', () => {
|
||||
const root = mountUsageRecordsTable([buildRecord({ service_tier: 'priority' })])
|
||||
it('shows confirmed fast when requested and actual service tiers are priority', () => {
|
||||
const root = mountUsageRecordsTable([buildRecord({
|
||||
service_tier: 'priority',
|
||||
actual_service_tier: 'priority',
|
||||
})])
|
||||
|
||||
expect(root.textContent).toContain('gpt-5')
|
||||
expect(root.textContent).toContain('fast')
|
||||
expectServiceTierBadge(root, 'fast')
|
||||
})
|
||||
|
||||
it('uses the actual service tier when the provider changes processing class', () => {
|
||||
const downgraded = mountUsageRecordsTable([buildRecord({
|
||||
it('shows fast to standard when the provider downgrades a priority request', () => {
|
||||
const root = 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',
|
||||
expectServiceTierBadge(root, 'fast → standard')
|
||||
})
|
||||
|
||||
it('shows fast to flex when the provider moves a priority request to flex', () => {
|
||||
const root = mountUsageRecordsTable([buildRecord({
|
||||
service_tier: 'priority',
|
||||
actual_service_tier: 'flex',
|
||||
})])
|
||||
|
||||
expectServiceTierBadge(root, 'fast → flex')
|
||||
})
|
||||
|
||||
it('shows standard to fast when the provider upgrades a default request', () => {
|
||||
const root = mountUsageRecordsTable([buildRecord({
|
||||
service_tier: 'default',
|
||||
actual_service_tier: 'priority',
|
||||
})])
|
||||
expect(upgraded.textContent).toContain('fast')
|
||||
|
||||
expectServiceTierBadge(root, 'standard → fast')
|
||||
})
|
||||
|
||||
it.each(['pending', 'streaming'] as const)(
|
||||
'shows fast as pending confirmation while a priority request is %s',
|
||||
(status) => {
|
||||
const root = mountUsageRecordsTable([buildRecord({
|
||||
service_tier: 'priority',
|
||||
actual_service_tier: null,
|
||||
status,
|
||||
})])
|
||||
|
||||
expectServiceTierBadge(root, 'fast · 待确认')
|
||||
},
|
||||
)
|
||||
|
||||
it('shows fast as unconfirmed when a completed priority request has no actual tier', () => {
|
||||
const root = mountUsageRecordsTable([buildRecord({
|
||||
service_tier: 'priority',
|
||||
actual_service_tier: null,
|
||||
status: 'completed',
|
||||
})])
|
||||
|
||||
expectServiceTierBadge(root, 'fast · 未确认')
|
||||
})
|
||||
|
||||
it('offers embedding API formats in the usage record filter', () => {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { UsageRecord } from '../../types'
|
||||
import { syncUsageRecordStreamResolution } from '../recordSync'
|
||||
import {
|
||||
mergeUsageRecordFirstByteTimeMs,
|
||||
syncUsageRecordStreamResolution,
|
||||
} from '../recordSync'
|
||||
|
||||
function buildUsageRecord(overrides: Partial<UsageRecord> = {}): UsageRecord {
|
||||
return {
|
||||
@@ -59,3 +62,16 @@ describe('syncUsageRecordStreamResolution', () => {
|
||||
expect(nextRecords).toBe(records)
|
||||
})
|
||||
})
|
||||
|
||||
describe('mergeUsageRecordFirstByteTimeMs', () => {
|
||||
it('does not let a stale active update clear or reduce a resolved first-byte time', () => {
|
||||
expect(mergeUsageRecordFirstByteTimeMs(500, null)).toBe(500)
|
||||
expect(mergeUsageRecordFirstByteTimeMs(500, 320)).toBe(500)
|
||||
expect(mergeUsageRecordFirstByteTimeMs(500, 640)).toBe(640)
|
||||
expect(mergeUsageRecordFirstByteTimeMs(undefined, 320)).toBe(320)
|
||||
expect(mergeUsageRecordFirstByteTimeMs(undefined, 0)).toBe(0)
|
||||
expect(mergeUsageRecordFirstByteTimeMs(0, null)).toBe(0)
|
||||
expect(mergeUsageRecordFirstByteTimeMs(null, -1)).toBeNull()
|
||||
expect(mergeUsageRecordFirstByteTimeMs(-1, null)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -78,6 +78,11 @@ describe('usage status helpers', () => {
|
||||
status: 'streaming',
|
||||
first_byte_time_ms: 320,
|
||||
}))).toBe('streaming')
|
||||
|
||||
expect(resolveDisplayRequestStatus(buildUsageRecord({
|
||||
status: 'streaming',
|
||||
first_byte_time_ms: 0,
|
||||
}))).toBe('streaming')
|
||||
})
|
||||
|
||||
it('treats active lifecycle records with failure signals as failed for display', () => {
|
||||
|
||||
@@ -5,6 +5,30 @@ export type UsageRecordStreamResolution = Pick<
|
||||
'id' | 'is_stream' | 'upstream_is_stream' | 'client_requested_stream' | 'client_is_stream'
|
||||
>
|
||||
|
||||
export function mergeUsageRecordFirstByteTimeMs(
|
||||
existingValue: number | null | undefined,
|
||||
nextValue: number | null | undefined
|
||||
): number | null | undefined {
|
||||
// Millisecond timing is floored, so 0 still means the first byte was observed.
|
||||
const existingIsResolved = typeof existingValue === 'number' &&
|
||||
Number.isFinite(existingValue) &&
|
||||
existingValue >= 0
|
||||
const nextIsResolved = typeof nextValue === 'number' &&
|
||||
Number.isFinite(nextValue) &&
|
||||
nextValue >= 0
|
||||
|
||||
if (existingIsResolved && nextIsResolved) {
|
||||
return Math.max(existingValue, nextValue)
|
||||
}
|
||||
if (existingIsResolved) {
|
||||
return existingValue
|
||||
}
|
||||
if (nextIsResolved) {
|
||||
return nextValue
|
||||
}
|
||||
return existingValue == null ? existingValue : undefined
|
||||
}
|
||||
|
||||
export function syncUsageRecordStreamResolution(
|
||||
records: UsageRecord[],
|
||||
resolved: UsageRecordStreamResolution
|
||||
|
||||
Reference in New Issue
Block a user